Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,26 @@ This is the result of a merger of two services: [Telemetry](https://github.com/D

## Authentication

This service accepts subject and permission-scoped tokens. These are generally short-lived and must be signed by a trusted issuer. The important claims beyond the standard ones are
This service accepts the access tokens dauth's `/exchange` mints (design spec §11.1): short-lived RS256 tokens, verified against `TOKEN_EXCHANGE_JWK_KEY_SET_URL` with issuer `TOKEN_EXCHANGE_ISSUER_URL` and audience `TOKEN_AUDIENCE` (default `dq`). Beyond the standard claims a token carries

```json
{
"asset": "did:erc721:137:0xbA5738a18d83D41847dfFbDC6101d37C69c9B0cF:42",
"permissions": ["privilege:GetNonLocationHistory", "privilege:GetRawData"]
"cnf": { "jkt": "<thumbprint of the caller's DPoP key>" },
"grants": [
{
"subject": "did:dimo:veh…",
"abilities": ["telemetry:read", "location:precise"],
"windows": [["2026-09-01T00:00:00Z", "2026-09-08T00:00:00Z"]]
},
{ "subject": "did:dimo:veh…", "abilities": ["raw:read"] }
]
}
```

Every authenticated request must also carry a `DPoP` header: an RFC 9449 proof for the request's method and URL, with `ath` over the token, signed by the key `cnf.jkt` names (`PUBLIC_BASE_URL` is the origin the proof's `htu` is checked against when dq sits behind a proxy). The token may be presented as `Authorization: DPoP <token>` or `Bearer <token>`.

A query's `subject` must be a grant subject, and each field's privileges (`@requiresAllOfPrivileges`, `@requiresOneOfPrivilege`) must be abilities the token holds for it. A grant's `windows` are the data timestamps its abilities may read: a query with `from`/`to` is clamped to the union of the subject's windows, a query with nothing covered is refused, and a query that straddles a gap in coverage is refused with the covered ranges named, so the caller splits it. A field's own abilities must then cover the whole clamped range; a read with no range (latest, snapshot, summary) needs the ability to cover the current time. Cloud-event reads take `raw:read` and have `after`/`before` clamped the same way. The fetch gRPC port takes the same token as a bearer without the DPoP check and scopes each read to the subjects it names.

## Migrating

### From Telemetry
Expand Down Expand Up @@ -55,6 +66,15 @@ The service reads from a DuckLake catalog: decoded signals/events in `lake.signa

Configure the catalog with `DUCKLAKE_CATALOG_DSN` (a Postgres DSN in prod for concurrent writers, or a local catalog-file path for single-node/tests) and `DUCKLAKE_DATA_PATH` (where parquet data files live — an `s3://` prefix in prod, a local directory in tests). `BLOB_BUCKET` is the bucket the fetch path presigns/downloads externalized cloudevent payloads from.

### Seeding a local catalog

`cmd/dq-seed` plays din for a local run: it writes `dimo.status` events carrying a speed signal into `lake.raw_events` in din's shape and decodes them once, so dq boots over a catalog that already has signals. The catalog file has one writer, so run it before starting dq. did-directory's `scripts/demo.sh` uses it.

```bash
go run ./cmd/dq-seed -catalog /data/catalog.ducklake -data-path /data/lake \
-subject did:dimo:… -from 2026-09-22T17:00:00Z -every 2s
```

### Single-node quickstart

Run against a local DuckLake catalog file; the materializer decodes din's raw_events into it:
Expand Down
148 changes: 148 additions & 0 deletions cmd/dq-seed/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Command dq-seed writes dimo.status raw events into a DuckLake catalog the
// way din's sink does, then runs the materializer over them once, so a local
// dq boots with decoded signals already in place. It is a development tool:
// it stands in for din when the whole DIMO stack is run on one machine
// (did-directory's scripts/demo.sh) and for nothing else.
//
// The catalog is a DuckDB file with one writer at a time, so run this before
// starting dq, or against a catalog no dq holds open. Decoding here rather
// than leaving it to dq's own materializer also means the decoded tables
// exist before dq's query connections attach the catalog, which is the state
// a production query pod finds.
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"math"
"os"
"time"

"github.com/DIMO-Network/cloudevent"
"github.com/DIMO-Network/dq/internal/materializer"
"github.com/DIMO-Network/dq/internal/service/duck"
"github.com/rs/zerolog"
)

func main() {
catalog := flag.String("catalog", "", "DuckLake catalog file (dq's DUCKLAKE_CATALOG_DSN)")
dataPath := flag.String("data-path", "", "DuckLake data directory (dq's DUCKLAKE_DATA_PATH)")
subject := flag.String("subject", "", "vehicle DID the events are about")
fromFlag := flag.String("from", "", "first event time, RFC 3339")
toFlag := flag.String("to", "", "last event time, RFC 3339 (default now)")
every := flag.Duration("every", 5*time.Second, "spacing between events")
source := flag.String("source", "0xDemoConnection", "cloud event source")
flag.Parse()

if *catalog == "" || *dataPath == "" || *subject == "" || *fromFlag == "" {
fmt.Fprintln(os.Stderr, "Error: -catalog, -data-path, -subject and -from are required")
os.Exit(1)
}
from, err := time.Parse(time.RFC3339, *fromFlag)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: -from: %v\n", err)
os.Exit(1)
}
to := time.Now().UTC()
if *toFlag != "" {
if to, err = time.Parse(time.RFC3339, *toFlag); err != nil {
fmt.Fprintf(os.Stderr, "Error: -to: %v\n", err)
os.Exit(1)
}
}
if !to.After(from) || *every <= 0 {
fmt.Fprintln(os.Stderr, "Error: -to must be after -from and -every must be positive")
os.Exit(1)
}

n, decoded, err := seed(context.Background(), *catalog, *dataPath, *subject, *source, from, to, *every)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("seeded %d dimo.status events for %s from %s to %s every %s; decoded %d into lake.signals\n",
n, *subject, from.UTC().Format(time.RFC3339), to.UTC().Format(time.RFC3339), *every, decoded)
}

// seed opens the catalog, creates lake.raw_events in din's shape if it is not
// there, inserts one status event per tick carrying a speed signal that
// swings between 0 and 100 km/h, so a time-series query has something to
// show, and then decodes everything the materializer has not seen yet.
// Returns the number of events written and the number of raw rows decoded.
func seed(ctx context.Context, catalog, dataPath, subject, source string, from, to time.Time, every time.Duration) (int, int, error) {
svc, err := duck.NewService(duck.Config{DuckLakeEnabled: true, CatalogDSN: catalog, DataPath: dataPath})
if err != nil {
return 0, 0, fmt.Errorf("open catalog: %w", err)
}
defer func() { _ = svc.Close() }()
db := svc.DB()

// din's DDL: RawEventColumns in internal/service/duck is the projection dq
// reads back, in this column order.
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS lake.raw_events (
subject VARCHAR, "time" TIMESTAMP WITH TIME ZONE, type VARCHAR, id VARCHAR,
source VARCHAR, producer VARCHAR, data_content_type VARCHAR, data_version VARCHAR,
extras VARCHAR, data VARCHAR, data_base64 BLOB, data_index_key VARCHAR, voids_id VARCHAR)`); err != nil {
return 0, 0, fmt.Errorf("create lake.raw_events: %w", err)
}

tx, err := db.BeginTx(ctx, nil)
if err != nil {
return 0, 0, err
}
defer func() { _ = tx.Rollback() }()
n := 0
for ts := from.UTC(); !ts.After(to); ts = ts.Add(every) {
// One swing from 0 to 100 km/h and back over about an hour.
speed := 50 + 50*math.Sin(float64(ts.Unix())/600)
payload, err := json.Marshal(map[string]any{"signals": []map[string]any{{
"name": "speed", "timestamp": ts.Format(time.RFC3339Nano), "value": math.Round(speed*10) / 10,
}}})
if err != nil {
return 0, 0, err
}
id := fmt.Sprintf("seed-%s-%d", ts.Format("20060102T150405"), n)
// din's appender writes empty strings, not NULLs, for the header columns.
if _, err := tx.ExecContext(ctx,
`INSERT INTO lake.raw_events (subject, "time", type, id, source, producer, data_content_type, data_version, extras, data)
VALUES (?, ?, ?, ?, ?, ?, '', ?, '{}', ?)`,
subject, ts, cloudevent.TypeStatus, id, source, subject, "default/v1.0", string(payload)); err != nil {
return 0, 0, fmt.Errorf("insert event %s: %w", id, err)
}
n++
}
if err := tx.Commit(); err != nil {
return 0, 0, err
}

// Decode as dq's materializer would: the decoded tables are created if
// they are missing, every new raw row becomes signal rows, and the latest
// rollups are flushed so signalsLatest answers too.
log := zerolog.New(os.Stderr).Level(zerolog.WarnLevel)
mat, err := materializer.NewDuckLakeMaterializer(ctx, db, log)
if err != nil {
return 0, 0, fmt.Errorf("materializer: %w", err)
}
// No vehicle contract configured: every subject decodes, did:dimo included.
runner := materializer.New(materializer.Config{}, log).WithDuckLake(mat)
decoded := 0
for {
got, err := runner.RunOnce(ctx)
if err != nil {
return 0, 0, fmt.Errorf("decode: %w", err)
}
decoded += got
if got == 0 {
break
}
}
if err := runner.FlushRollup(ctx); err != nil {
return 0, 0, fmt.Errorf("flush signals_latest: %w", err)
}
if err := runner.FlushEventRollup(ctx); err != nil {
return 0, 0, fmt.Errorf("flush events_latest: %w", err)
}
return n, decoded, nil
}
28 changes: 13 additions & 15 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
module github.com/DIMO-Network/dq

go 1.26.2

toolchain go1.26.4
go 1.27

require (
github.com/99designs/gqlgen v0.17.89
github.com/DIMO-Network/cloudevent v1.1.0
github.com/DIMO-Network/dauth v0.0.1
github.com/DIMO-Network/dauth v0.0.2-0.20260923165546-17aea928e5ad
github.com/DIMO-Network/model-garage v1.0.11
github.com/DIMO-Network/server-garage v0.4.0
github.com/DIMO-Network/shared v1.1.9
Expand All @@ -30,7 +28,7 @@ require (
github.com/uber/h3-go/v4 v4.3.0
github.com/vektah/gqlparser/v2 v2.5.32
go.uber.org/mock v0.6.0
golang.org/x/sync v0.20.0
golang.org/x/sync v0.23.0
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
)
Expand Down Expand Up @@ -77,10 +75,10 @@ require (
github.com/holiman/uint256 v1.3.2 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
Expand Down Expand Up @@ -113,16 +111,16 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect
Expand Down
51 changes: 26 additions & 25 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/DIMO-Network/cloudevent v1.1.0 h1:pDJxwO3/Zom/U8BOZqKtMQ05Ye6Pgn6LAh/Dlr8VwWA=
github.com/DIMO-Network/cloudevent v1.1.0/go.mod h1:I/9NcpMozV5Fw194WimhbkAsJtKVZf5UKYJ9hgc8Cdg=
github.com/DIMO-Network/dauth v0.0.1 h1:ZDPNOfCRci1+RhpgMz/2LXUywXjYdSeXRDl+J3M+/5A=
github.com/DIMO-Network/dauth v0.0.1/go.mod h1:RWy1bbPI0KWJ/Q5eS5teKo7jP6sNPWLomK0SXKkhdoo=
github.com/DIMO-Network/dauth v0.0.2-0.20260922180207-703cdfb21455 h1:wyuxFjODl6HLEPbUpFn4fWFa7OjQmD/dffnki7WNQ4E=
github.com/DIMO-Network/dauth v0.0.2-0.20260922180207-703cdfb21455/go.mod h1:Q1xZDvdmU9pkp6+7JSjGWXPEVInE7URcyMFY8iduAa8=
github.com/DIMO-Network/dauth v0.0.2-0.20260923165546-17aea928e5ad h1:boMz9aJJ+5khnwo31Zn4Ls/0a11KrXTDb0EeKeBI3is=
github.com/DIMO-Network/dauth v0.0.2-0.20260923165546-17aea928e5ad/go.mod h1:Q1xZDvdmU9pkp6+7JSjGWXPEVInE7URcyMFY8iduAa8=
github.com/DIMO-Network/model-garage v1.0.11 h1:aLvIyeo58p9pVgz+d3DnU5k5Fxvxh6mq/jE2s3LxXoc=
github.com/DIMO-Network/model-garage v1.0.11/go.mod h1:oi7EGKQVxFVpXRsu2H+YbizbKcx06aQg2N1Yu4GqOp8=
github.com/DIMO-Network/server-garage v0.4.0 h1:3ukXvtldIhLldn9AtbtQBooBjT2gicvB3WphrsjNQho=
Expand Down Expand Up @@ -142,18 +144,18 @@ github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
Expand Down Expand Up @@ -266,30 +268,29 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4=
golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q=
golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
Expand Down
Loading
Loading