relay filter/appview bootstrap

Switch to TAP for backfill. Use rust instead of typescript.

+18 -5
.env.example
···
-
DATABASE_URL=postgres://prism:prism@localhost:5432/prism
-
DB_POOL_SIZE=10
-
NODE_ENV=development
-
LOG_LEVEL=info
-
PLC_DIRECTORY_URL=https://plc.directory
+
# Database connection
+
DATABASE_URL=postgres://postgres:postgres@localhost:5432/prism
+
+
# TAP WebSocket URL
+
TAP_WS_URL=ws://localhost:2480/channel
+
+
# Server configuration
+
HOST=0.0.0.0
+
PORT=3000
+
+
# Logging level
+
RUST_LOG=prism=info,tower_http=info
+
# TAP configuration (for docker-compose)
+
TAP_UPSTREAM_RELAY=wss://bsky.network
+
# Signal collection identifies repos to track (must be a full NSID)
+
TAP_SIGNAL_COLLECTION=systems.gmstn.development.lattice
+
# Collection filters determine which records to deliver (wildcards supported)
+
TAP_COLLECTION_FILTERS=systems.gmstn.development.*
+1 -6
.gitignore
···
-
.DS_Store
-
/node_modules
-
/dist
.env
-
*.tsbuildinfo
+
target/
-
pgdata_podman
-
postgres.log
+70
.sqlx/query-10bfde55d646787e5bb669f822b8d034e3c700e3bd95a7cdb206e13cd855c45e.json
···
+
{
+
"db_name": "PostgreSQL",
+
"query": "\n SELECT uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids\n FROM record\n WHERE cid = $1\n ",
+
"describe": {
+
"columns": [
+
{
+
"ordinal": 0,
+
"name": "uri",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 1,
+
"name": "cid",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 2,
+
"name": "collection",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 3,
+
"name": "creator_did",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 4,
+
"name": "created_at",
+
"type_info": "Timestamptz"
+
},
+
{
+
"ordinal": 5,
+
"name": "indexed_at",
+
"type_info": "Timestamptz"
+
},
+
{
+
"ordinal": 6,
+
"name": "data",
+
"type_info": "Jsonb"
+
},
+
{
+
"ordinal": 7,
+
"name": "target_did",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 8,
+
"name": "ref_cids",
+
"type_info": "TextArray"
+
}
+
],
+
"parameters": {
+
"Left": [
+
"Text"
+
]
+
},
+
"nullable": [
+
false,
+
false,
+
false,
+
false,
+
false,
+
false,
+
false,
+
true,
+
false
+
]
+
},
+
"hash": "10bfde55d646787e5bb669f822b8d034e3c700e3bd95a7cdb206e13cd855c45e"
+
}
+73
.sqlx/query-763e8ff802d925e26afc798d1db7d41f68610e2b3ac8082f90507bfe948b42c4.json
···
+
{
+
"db_name": "PostgreSQL",
+
"query": "\n SELECT uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids\n FROM record\n WHERE collection = $1 AND creator_did = $2 AND indexed_at < $3\n ORDER BY indexed_at DESC\n LIMIT $4\n ",
+
"describe": {
+
"columns": [
+
{
+
"ordinal": 0,
+
"name": "uri",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 1,
+
"name": "cid",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 2,
+
"name": "collection",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 3,
+
"name": "creator_did",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 4,
+
"name": "created_at",
+
"type_info": "Timestamptz"
+
},
+
{
+
"ordinal": 5,
+
"name": "indexed_at",
+
"type_info": "Timestamptz"
+
},
+
{
+
"ordinal": 6,
+
"name": "data",
+
"type_info": "Jsonb"
+
},
+
{
+
"ordinal": 7,
+
"name": "target_did",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 8,
+
"name": "ref_cids",
+
"type_info": "TextArray"
+
}
+
],
+
"parameters": {
+
"Left": [
+
"Text",
+
"Text",
+
"Timestamptz",
+
"Int8"
+
]
+
},
+
"nullable": [
+
false,
+
false,
+
false,
+
false,
+
false,
+
false,
+
false,
+
true,
+
false
+
]
+
},
+
"hash": "763e8ff802d925e26afc798d1db7d41f68610e2b3ac8082f90507bfe948b42c4"
+
}
+73
.sqlx/query-7c39455a09e10dd38d417d6db9768eccfc0f2266a7b70fa1acba0e450d9a6b76.json
···
+
{
+
"db_name": "PostgreSQL",
+
"query": "\n SELECT uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids\n FROM record\n WHERE collection = $1 AND target_did = $2 AND indexed_at < $3\n ORDER BY indexed_at DESC\n LIMIT $4\n ",
+
"describe": {
+
"columns": [
+
{
+
"ordinal": 0,
+
"name": "uri",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 1,
+
"name": "cid",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 2,
+
"name": "collection",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 3,
+
"name": "creator_did",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 4,
+
"name": "created_at",
+
"type_info": "Timestamptz"
+
},
+
{
+
"ordinal": 5,
+
"name": "indexed_at",
+
"type_info": "Timestamptz"
+
},
+
{
+
"ordinal": 6,
+
"name": "data",
+
"type_info": "Jsonb"
+
},
+
{
+
"ordinal": 7,
+
"name": "target_did",
+
"type_info": "Text"
+
},
+
{
+
"ordinal": 8,
+
"name": "ref_cids",
+
"type_info": "TextArray"
+
}
+
],
+
"parameters": {
+
"Left": [
+
"Text",
+
"Text",
+
"Timestamptz",
+
"Int8"
+
]
+
},
+
"nullable": [
+
false,
+
false,
+
false,
+
false,
+
false,
+
false,
+
false,
+
true,
+
false
+
]
+
},
+
"hash": "7c39455a09e10dd38d417d6db9768eccfc0f2266a7b70fa1acba0e450d9a6b76"
+
}
+22
.sqlx/query-9459981b303de9f8eef6f1e588a335e26d303ee12593f72e205fc00e65b5105c.json
···
+
{
+
"db_name": "PostgreSQL",
+
"query": "\n INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (uri) DO UPDATE SET\n cid = EXCLUDED.cid,\n data = EXCLUDED.data,\n indexed_at = EXCLUDED.indexed_at\n ",
+
"describe": {
+
"columns": [],
+
"parameters": {
+
"Left": [
+
"Text",
+
"Text",
+
"Text",
+
"Text",
+
"Timestamptz",
+
"Timestamptz",
+
"Jsonb",
+
"Text",
+
"TextArray"
+
]
+
},
+
"nullable": []
+
},
+
"hash": "9459981b303de9f8eef6f1e588a335e26d303ee12593f72e205fc00e65b5105c"
+
}
+16
.sqlx/query-adefe01bb311389a378b5598649524fac0d58af8319626e1cadb3b8105764b3e.json
···
+
{
+
"db_name": "PostgreSQL",
+
"query": "\n INSERT INTO account (did, handle, created_at)\n VALUES ($1, $2, $3)\n ON CONFLICT (did) DO NOTHING\n ",
+
"describe": {
+
"columns": [],
+
"parameters": {
+
"Left": [
+
"Text",
+
"Text",
+
"Timestamptz"
+
]
+
},
+
"nullable": []
+
},
+
"hash": "adefe01bb311389a378b5598649524fac0d58af8319626e1cadb3b8105764b3e"
+
}
+3812
Cargo.lock
···
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 4
+
+
[[package]]
+
name = "adler2"
+
version = "2.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+
[[package]]
+
name = "aho-corasick"
+
version = "1.1.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+
dependencies = [
+
"memchr",
+
]
+
+
[[package]]
+
name = "allocator-api2"
+
version = "0.2.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+
[[package]]
+
name = "android_system_properties"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+
dependencies = [
+
"libc",
+
]
+
+
[[package]]
+
name = "anyhow"
+
version = "1.0.100"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
+
[[package]]
+
name = "astral-tokio-tar"
+
version = "0.5.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ec179a06c1769b1e42e1e2cbe74c7dcdb3d6383c838454d063eaac5bbb7ebbe5"
+
dependencies = [
+
"filetime",
+
"futures-core",
+
"libc",
+
"portable-atomic",
+
"rustc-hash",
+
"tokio",
+
"tokio-stream",
+
"xattr",
+
]
+
+
[[package]]
+
name = "async-compression"
+
version = "0.4.36"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37"
+
dependencies = [
+
"compression-codecs",
+
"compression-core",
+
"futures-core",
+
"pin-project-lite",
+
"tokio",
+
]
+
+
[[package]]
+
name = "async-stream"
+
version = "0.3.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+
dependencies = [
+
"async-stream-impl",
+
"futures-core",
+
"pin-project-lite",
+
]
+
+
[[package]]
+
name = "async-stream-impl"
+
version = "0.3.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "async-trait"
+
version = "0.1.89"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "atoi"
+
version = "2.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+
dependencies = [
+
"num-traits",
+
]
+
+
[[package]]
+
name = "atomic-waker"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+
[[package]]
+
name = "autocfg"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+
[[package]]
+
name = "axum"
+
version = "0.8.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
+
dependencies = [
+
"axum-core",
+
"base64 0.22.1",
+
"bytes",
+
"form_urlencoded",
+
"futures-util",
+
"http",
+
"http-body",
+
"http-body-util",
+
"hyper",
+
"hyper-util",
+
"itoa",
+
"matchit",
+
"memchr",
+
"mime",
+
"percent-encoding",
+
"pin-project-lite",
+
"serde_core",
+
"serde_json",
+
"serde_path_to_error",
+
"serde_urlencoded",
+
"sha1",
+
"sync_wrapper",
+
"tokio",
+
"tokio-tungstenite",
+
"tower",
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "axum-core"
+
version = "0.5.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22"
+
dependencies = [
+
"bytes",
+
"futures-core",
+
"http",
+
"http-body",
+
"http-body-util",
+
"mime",
+
"pin-project-lite",
+
"sync_wrapper",
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "base64"
+
version = "0.21.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+
[[package]]
+
name = "base64"
+
version = "0.22.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+
[[package]]
+
name = "base64ct"
+
version = "1.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a"
+
+
[[package]]
+
name = "bitflags"
+
version = "2.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
dependencies = [
+
"serde_core",
+
]
+
+
[[package]]
+
name = "block-buffer"
+
version = "0.10.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+
dependencies = [
+
"generic-array",
+
]
+
+
[[package]]
+
name = "bollard"
+
version = "0.19.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "87a52479c9237eb04047ddb94788c41ca0d26eaff8b697ecfbb4c32f7fdc3b1b"
+
dependencies = [
+
"async-stream",
+
"base64 0.22.1",
+
"bitflags",
+
"bollard-buildkit-proto",
+
"bollard-stubs",
+
"bytes",
+
"chrono",
+
"futures-core",
+
"futures-util",
+
"hex",
+
"home",
+
"http",
+
"http-body-util",
+
"hyper",
+
"hyper-named-pipe",
+
"hyper-rustls",
+
"hyper-util",
+
"hyperlocal",
+
"log",
+
"num",
+
"pin-project-lite",
+
"rand 0.9.2",
+
"rustls",
+
"rustls-native-certs",
+
"rustls-pemfile",
+
"rustls-pki-types",
+
"serde",
+
"serde_derive",
+
"serde_json",
+
"serde_repr",
+
"serde_urlencoded",
+
"thiserror",
+
"tokio",
+
"tokio-stream",
+
"tokio-util",
+
"tonic",
+
"tower-service",
+
"url",
+
"winapi",
+
]
+
+
[[package]]
+
name = "bollard-buildkit-proto"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad"
+
dependencies = [
+
"prost",
+
"prost-types",
+
"tonic",
+
"tonic-prost",
+
"ureq",
+
]
+
+
[[package]]
+
name = "bollard-stubs"
+
version = "1.49.1-rc.28.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5731fe885755e92beff1950774068e0cae67ea6ec7587381536fca84f1779623"
+
dependencies = [
+
"base64 0.22.1",
+
"bollard-buildkit-proto",
+
"bytes",
+
"chrono",
+
"prost",
+
"serde",
+
"serde_json",
+
"serde_repr",
+
"serde_with",
+
]
+
+
[[package]]
+
name = "bumpalo"
+
version = "3.19.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+
[[package]]
+
name = "byteorder"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+
[[package]]
+
name = "bytes"
+
version = "1.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+
+
[[package]]
+
name = "cc"
+
version = "1.2.49"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
+
dependencies = [
+
"find-msvc-tools",
+
"shlex",
+
]
+
+
[[package]]
+
name = "cfg-if"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+
[[package]]
+
name = "chrono"
+
version = "0.4.42"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+
dependencies = [
+
"iana-time-zone",
+
"js-sys",
+
"num-traits",
+
"serde",
+
"wasm-bindgen",
+
"windows-link",
+
]
+
+
[[package]]
+
name = "compression-codecs"
+
version = "0.4.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2"
+
dependencies = [
+
"compression-core",
+
"flate2",
+
"memchr",
+
]
+
+
[[package]]
+
name = "compression-core"
+
version = "0.4.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
+
+
[[package]]
+
name = "concurrent-queue"
+
version = "2.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+
dependencies = [
+
"crossbeam-utils",
+
]
+
+
[[package]]
+
name = "const-oid"
+
version = "0.9.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+
[[package]]
+
name = "core-foundation"
+
version = "0.9.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+
dependencies = [
+
"core-foundation-sys",
+
"libc",
+
]
+
+
[[package]]
+
name = "core-foundation"
+
version = "0.10.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+
dependencies = [
+
"core-foundation-sys",
+
"libc",
+
]
+
+
[[package]]
+
name = "core-foundation-sys"
+
version = "0.8.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+
[[package]]
+
name = "cpufeatures"
+
version = "0.2.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+
dependencies = [
+
"libc",
+
]
+
+
[[package]]
+
name = "crc"
+
version = "3.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+
dependencies = [
+
"crc-catalog",
+
]
+
+
[[package]]
+
name = "crc-catalog"
+
version = "2.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+
[[package]]
+
name = "crc32fast"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+
dependencies = [
+
"cfg-if",
+
]
+
+
[[package]]
+
name = "crossbeam-queue"
+
version = "0.3.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+
dependencies = [
+
"crossbeam-utils",
+
]
+
+
[[package]]
+
name = "crossbeam-utils"
+
version = "0.8.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+
[[package]]
+
name = "crypto-common"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+
dependencies = [
+
"generic-array",
+
"typenum",
+
]
+
+
[[package]]
+
name = "ctor"
+
version = "0.6.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
+
dependencies = [
+
"ctor-proc-macro",
+
"dtor",
+
]
+
+
[[package]]
+
name = "ctor-proc-macro"
+
version = "0.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+
+
[[package]]
+
name = "darling"
+
version = "0.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
+
dependencies = [
+
"darling_core",
+
"darling_macro",
+
]
+
+
[[package]]
+
name = "darling_core"
+
version = "0.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
+
dependencies = [
+
"fnv",
+
"ident_case",
+
"proc-macro2",
+
"quote",
+
"strsim",
+
"syn",
+
]
+
+
[[package]]
+
name = "darling_macro"
+
version = "0.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
+
dependencies = [
+
"darling_core",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "data-encoding"
+
version = "2.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+
[[package]]
+
name = "der"
+
version = "0.7.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+
dependencies = [
+
"const-oid",
+
"pem-rfc7468",
+
"zeroize",
+
]
+
+
[[package]]
+
name = "deranged"
+
version = "0.5.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
+
dependencies = [
+
"powerfmt",
+
"serde_core",
+
]
+
+
[[package]]
+
name = "digest"
+
version = "0.10.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+
dependencies = [
+
"block-buffer",
+
"const-oid",
+
"crypto-common",
+
"subtle",
+
]
+
+
[[package]]
+
name = "displaydoc"
+
version = "0.2.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "docker_credential"
+
version = "1.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8"
+
dependencies = [
+
"base64 0.21.7",
+
"serde",
+
"serde_json",
+
]
+
+
[[package]]
+
name = "dotenvy"
+
version = "0.15.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+
[[package]]
+
name = "dtor"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301"
+
dependencies = [
+
"dtor-proc-macro",
+
]
+
+
[[package]]
+
name = "dtor-proc-macro"
+
version = "0.0.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
+
+
[[package]]
+
name = "dyn-clone"
+
version = "1.0.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+
[[package]]
+
name = "either"
+
version = "1.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
dependencies = [
+
"serde",
+
]
+
+
[[package]]
+
name = "encoding_rs"
+
version = "0.8.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+
dependencies = [
+
"cfg-if",
+
]
+
+
[[package]]
+
name = "equivalent"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+
[[package]]
+
name = "errno"
+
version = "0.3.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+
dependencies = [
+
"libc",
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "etcetera"
+
version = "0.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+
dependencies = [
+
"cfg-if",
+
"home",
+
"windows-sys 0.48.0",
+
]
+
+
[[package]]
+
name = "etcetera"
+
version = "0.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96"
+
dependencies = [
+
"cfg-if",
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "event-listener"
+
version = "5.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+
dependencies = [
+
"concurrent-queue",
+
"parking",
+
"pin-project-lite",
+
]
+
+
[[package]]
+
name = "fastrand"
+
version = "2.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+
[[package]]
+
name = "ferroid"
+
version = "0.8.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e0e9414a6ae93ef993ce40a1e02944f13d4508e2bf6f1ced1580ce6910f08253"
+
dependencies = [
+
"portable-atomic",
+
"rand 0.9.2",
+
"web-time",
+
]
+
+
[[package]]
+
name = "filetime"
+
version = "0.2.26"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"libredox",
+
"windows-sys 0.60.2",
+
]
+
+
[[package]]
+
name = "find-msvc-tools"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
+
+
[[package]]
+
name = "flate2"
+
version = "1.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
+
dependencies = [
+
"crc32fast",
+
"miniz_oxide",
+
]
+
+
[[package]]
+
name = "flume"
+
version = "0.11.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+
dependencies = [
+
"futures-core",
+
"futures-sink",
+
"spin",
+
]
+
+
[[package]]
+
name = "fnv"
+
version = "1.0.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+
[[package]]
+
name = "foldhash"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+
[[package]]
+
name = "foreign-types"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+
dependencies = [
+
"foreign-types-shared",
+
]
+
+
[[package]]
+
name = "foreign-types-shared"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+
[[package]]
+
name = "form_urlencoded"
+
version = "1.2.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+
dependencies = [
+
"percent-encoding",
+
]
+
+
[[package]]
+
name = "futures"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+
dependencies = [
+
"futures-channel",
+
"futures-core",
+
"futures-executor",
+
"futures-io",
+
"futures-sink",
+
"futures-task",
+
"futures-util",
+
]
+
+
[[package]]
+
name = "futures-channel"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+
dependencies = [
+
"futures-core",
+
"futures-sink",
+
]
+
+
[[package]]
+
name = "futures-core"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+
[[package]]
+
name = "futures-executor"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+
dependencies = [
+
"futures-core",
+
"futures-task",
+
"futures-util",
+
]
+
+
[[package]]
+
name = "futures-intrusive"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+
dependencies = [
+
"futures-core",
+
"lock_api",
+
"parking_lot",
+
]
+
+
[[package]]
+
name = "futures-io"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+
[[package]]
+
name = "futures-macro"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "futures-sink"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+
[[package]]
+
name = "futures-task"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+
[[package]]
+
name = "futures-util"
+
version = "0.3.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+
dependencies = [
+
"futures-channel",
+
"futures-core",
+
"futures-io",
+
"futures-macro",
+
"futures-sink",
+
"futures-task",
+
"memchr",
+
"pin-project-lite",
+
"pin-utils",
+
"slab",
+
]
+
+
[[package]]
+
name = "generic-array"
+
version = "0.14.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
+
dependencies = [
+
"typenum",
+
"version_check",
+
]
+
+
[[package]]
+
name = "getrandom"
+
version = "0.2.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"wasi",
+
]
+
+
[[package]]
+
name = "getrandom"
+
version = "0.3.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"r-efi",
+
"wasip2",
+
]
+
+
[[package]]
+
name = "h2"
+
version = "0.4.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+
dependencies = [
+
"atomic-waker",
+
"bytes",
+
"fnv",
+
"futures-core",
+
"futures-sink",
+
"http",
+
"indexmap 2.12.1",
+
"slab",
+
"tokio",
+
"tokio-util",
+
"tracing",
+
]
+
+
[[package]]
+
name = "hashbrown"
+
version = "0.12.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+
[[package]]
+
name = "hashbrown"
+
version = "0.15.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+
dependencies = [
+
"allocator-api2",
+
"equivalent",
+
"foldhash",
+
]
+
+
[[package]]
+
name = "hashbrown"
+
version = "0.16.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+
[[package]]
+
name = "hashlink"
+
version = "0.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+
dependencies = [
+
"hashbrown 0.15.5",
+
]
+
+
[[package]]
+
name = "heck"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+
[[package]]
+
name = "hex"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+
[[package]]
+
name = "hkdf"
+
version = "0.12.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+
dependencies = [
+
"hmac",
+
]
+
+
[[package]]
+
name = "hmac"
+
version = "0.12.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+
dependencies = [
+
"digest",
+
]
+
+
[[package]]
+
name = "home"
+
version = "0.5.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+
dependencies = [
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "http"
+
version = "1.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+
dependencies = [
+
"bytes",
+
"itoa",
+
]
+
+
[[package]]
+
name = "http-body"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+
dependencies = [
+
"bytes",
+
"http",
+
]
+
+
[[package]]
+
name = "http-body-util"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+
dependencies = [
+
"bytes",
+
"futures-core",
+
"http",
+
"http-body",
+
"pin-project-lite",
+
]
+
+
[[package]]
+
name = "httparse"
+
version = "1.10.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+
[[package]]
+
name = "httpdate"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+
[[package]]
+
name = "hyper"
+
version = "1.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+
dependencies = [
+
"atomic-waker",
+
"bytes",
+
"futures-channel",
+
"futures-core",
+
"h2",
+
"http",
+
"http-body",
+
"httparse",
+
"httpdate",
+
"itoa",
+
"pin-project-lite",
+
"pin-utils",
+
"smallvec",
+
"tokio",
+
"want",
+
]
+
+
[[package]]
+
name = "hyper-named-pipe"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278"
+
dependencies = [
+
"hex",
+
"hyper",
+
"hyper-util",
+
"pin-project-lite",
+
"tokio",
+
"tower-service",
+
"winapi",
+
]
+
+
[[package]]
+
name = "hyper-rustls"
+
version = "0.27.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+
dependencies = [
+
"http",
+
"hyper",
+
"hyper-util",
+
"rustls",
+
"rustls-pki-types",
+
"tokio",
+
"tokio-rustls",
+
"tower-service",
+
]
+
+
[[package]]
+
name = "hyper-timeout"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
+
dependencies = [
+
"hyper",
+
"hyper-util",
+
"pin-project-lite",
+
"tokio",
+
"tower-service",
+
]
+
+
[[package]]
+
name = "hyper-tls"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+
dependencies = [
+
"bytes",
+
"http-body-util",
+
"hyper",
+
"hyper-util",
+
"native-tls",
+
"tokio",
+
"tokio-native-tls",
+
"tower-service",
+
]
+
+
[[package]]
+
name = "hyper-util"
+
version = "0.1.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
+
dependencies = [
+
"base64 0.22.1",
+
"bytes",
+
"futures-channel",
+
"futures-core",
+
"futures-util",
+
"http",
+
"http-body",
+
"hyper",
+
"ipnet",
+
"libc",
+
"percent-encoding",
+
"pin-project-lite",
+
"socket2",
+
"system-configuration",
+
"tokio",
+
"tower-service",
+
"tracing",
+
"windows-registry",
+
]
+
+
[[package]]
+
name = "hyperlocal"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7"
+
dependencies = [
+
"hex",
+
"http-body-util",
+
"hyper",
+
"hyper-util",
+
"pin-project-lite",
+
"tokio",
+
"tower-service",
+
]
+
+
[[package]]
+
name = "iana-time-zone"
+
version = "0.1.64"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
+
dependencies = [
+
"android_system_properties",
+
"core-foundation-sys",
+
"iana-time-zone-haiku",
+
"js-sys",
+
"log",
+
"wasm-bindgen",
+
"windows-core",
+
]
+
+
[[package]]
+
name = "iana-time-zone-haiku"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+
dependencies = [
+
"cc",
+
]
+
+
[[package]]
+
name = "icu_collections"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+
dependencies = [
+
"displaydoc",
+
"potential_utf",
+
"yoke",
+
"zerofrom",
+
"zerovec",
+
]
+
+
[[package]]
+
name = "icu_locale_core"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+
dependencies = [
+
"displaydoc",
+
"litemap",
+
"tinystr",
+
"writeable",
+
"zerovec",
+
]
+
+
[[package]]
+
name = "icu_normalizer"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+
dependencies = [
+
"icu_collections",
+
"icu_normalizer_data",
+
"icu_properties",
+
"icu_provider",
+
"smallvec",
+
"zerovec",
+
]
+
+
[[package]]
+
name = "icu_normalizer_data"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+
[[package]]
+
name = "icu_properties"
+
version = "2.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+
dependencies = [
+
"icu_collections",
+
"icu_locale_core",
+
"icu_properties_data",
+
"icu_provider",
+
"zerotrie",
+
"zerovec",
+
]
+
+
[[package]]
+
name = "icu_properties_data"
+
version = "2.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+
[[package]]
+
name = "icu_provider"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+
dependencies = [
+
"displaydoc",
+
"icu_locale_core",
+
"writeable",
+
"yoke",
+
"zerofrom",
+
"zerotrie",
+
"zerovec",
+
]
+
+
[[package]]
+
name = "ident_case"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+
[[package]]
+
name = "idna"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+
dependencies = [
+
"idna_adapter",
+
"smallvec",
+
"utf8_iter",
+
]
+
+
[[package]]
+
name = "idna_adapter"
+
version = "1.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+
dependencies = [
+
"icu_normalizer",
+
"icu_properties",
+
]
+
+
[[package]]
+
name = "indexmap"
+
version = "1.9.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+
dependencies = [
+
"autocfg",
+
"hashbrown 0.12.3",
+
"serde",
+
]
+
+
[[package]]
+
name = "indexmap"
+
version = "2.12.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
+
dependencies = [
+
"equivalent",
+
"hashbrown 0.16.1",
+
"serde",
+
"serde_core",
+
]
+
+
[[package]]
+
name = "ipnet"
+
version = "2.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+
[[package]]
+
name = "iri-string"
+
version = "0.7.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
+
dependencies = [
+
"memchr",
+
"serde",
+
]
+
+
[[package]]
+
name = "itertools"
+
version = "0.14.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+
dependencies = [
+
"either",
+
]
+
+
[[package]]
+
name = "itoa"
+
version = "1.0.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+
[[package]]
+
name = "js-sys"
+
version = "0.3.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
+
dependencies = [
+
"once_cell",
+
"wasm-bindgen",
+
]
+
+
[[package]]
+
name = "lazy_static"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
dependencies = [
+
"spin",
+
]
+
+
[[package]]
+
name = "libc"
+
version = "0.2.178"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
+
+
[[package]]
+
name = "libm"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+
[[package]]
+
name = "libredox"
+
version = "0.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
+
dependencies = [
+
"bitflags",
+
"libc",
+
"redox_syscall",
+
]
+
+
[[package]]
+
name = "libsqlite3-sys"
+
version = "0.30.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+
dependencies = [
+
"pkg-config",
+
"vcpkg",
+
]
+
+
[[package]]
+
name = "linux-raw-sys"
+
version = "0.11.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+
[[package]]
+
name = "litemap"
+
version = "0.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+
[[package]]
+
name = "lock_api"
+
version = "0.4.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+
dependencies = [
+
"scopeguard",
+
]
+
+
[[package]]
+
name = "log"
+
version = "0.4.29"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+
[[package]]
+
name = "matchers"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+
dependencies = [
+
"regex-automata",
+
]
+
+
[[package]]
+
name = "matchit"
+
version = "0.8.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+
[[package]]
+
name = "md-5"
+
version = "0.10.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+
dependencies = [
+
"cfg-if",
+
"digest",
+
]
+
+
[[package]]
+
name = "memchr"
+
version = "2.7.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+
+
[[package]]
+
name = "mime"
+
version = "0.3.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+
[[package]]
+
name = "miniz_oxide"
+
version = "0.8.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+
dependencies = [
+
"adler2",
+
"simd-adler32",
+
]
+
+
[[package]]
+
name = "mio"
+
version = "1.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+
dependencies = [
+
"libc",
+
"wasi",
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "native-tls"
+
version = "0.2.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+
dependencies = [
+
"libc",
+
"log",
+
"openssl",
+
"openssl-probe",
+
"openssl-sys",
+
"schannel",
+
"security-framework 2.11.1",
+
"security-framework-sys",
+
"tempfile",
+
]
+
+
[[package]]
+
name = "nu-ansi-term"
+
version = "0.50.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+
dependencies = [
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "num"
+
version = "0.4.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+
dependencies = [
+
"num-bigint",
+
"num-complex",
+
"num-integer",
+
"num-iter",
+
"num-rational",
+
"num-traits",
+
]
+
+
[[package]]
+
name = "num-bigint"
+
version = "0.4.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+
dependencies = [
+
"num-integer",
+
"num-traits",
+
]
+
+
[[package]]
+
name = "num-bigint-dig"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
+
dependencies = [
+
"lazy_static",
+
"libm",
+
"num-integer",
+
"num-iter",
+
"num-traits",
+
"rand 0.8.5",
+
"smallvec",
+
"zeroize",
+
]
+
+
[[package]]
+
name = "num-complex"
+
version = "0.4.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+
dependencies = [
+
"num-traits",
+
]
+
+
[[package]]
+
name = "num-conv"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+
[[package]]
+
name = "num-integer"
+
version = "0.1.46"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+
dependencies = [
+
"num-traits",
+
]
+
+
[[package]]
+
name = "num-iter"
+
version = "0.1.45"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+
dependencies = [
+
"autocfg",
+
"num-integer",
+
"num-traits",
+
]
+
+
[[package]]
+
name = "num-rational"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+
dependencies = [
+
"num-bigint",
+
"num-integer",
+
"num-traits",
+
]
+
+
[[package]]
+
name = "num-traits"
+
version = "0.2.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+
dependencies = [
+
"autocfg",
+
"libm",
+
]
+
+
[[package]]
+
name = "once_cell"
+
version = "1.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+
[[package]]
+
name = "openssl"
+
version = "0.10.75"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
+
dependencies = [
+
"bitflags",
+
"cfg-if",
+
"foreign-types",
+
"libc",
+
"once_cell",
+
"openssl-macros",
+
"openssl-sys",
+
]
+
+
[[package]]
+
name = "openssl-macros"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "openssl-probe"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+
[[package]]
+
name = "openssl-sys"
+
version = "0.9.111"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
+
dependencies = [
+
"cc",
+
"libc",
+
"pkg-config",
+
"vcpkg",
+
]
+
+
[[package]]
+
name = "parking"
+
version = "2.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+
[[package]]
+
name = "parking_lot"
+
version = "0.12.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+
dependencies = [
+
"lock_api",
+
"parking_lot_core",
+
]
+
+
[[package]]
+
name = "parking_lot_core"
+
version = "0.9.12"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"redox_syscall",
+
"smallvec",
+
"windows-link",
+
]
+
+
[[package]]
+
name = "parse-display"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
+
dependencies = [
+
"parse-display-derive",
+
"regex",
+
"regex-syntax",
+
]
+
+
[[package]]
+
name = "parse-display-derive"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"regex",
+
"regex-syntax",
+
"structmeta",
+
"syn",
+
]
+
+
[[package]]
+
name = "pem-rfc7468"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+
dependencies = [
+
"base64ct",
+
]
+
+
[[package]]
+
name = "percent-encoding"
+
version = "2.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+
[[package]]
+
name = "pin-project"
+
version = "1.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
+
dependencies = [
+
"pin-project-internal",
+
]
+
+
[[package]]
+
name = "pin-project-internal"
+
version = "1.1.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "pin-project-lite"
+
version = "0.2.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+
[[package]]
+
name = "pin-utils"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+
[[package]]
+
name = "pkcs1"
+
version = "0.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+
dependencies = [
+
"der",
+
"pkcs8",
+
"spki",
+
]
+
+
[[package]]
+
name = "pkcs8"
+
version = "0.10.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+
dependencies = [
+
"der",
+
"spki",
+
]
+
+
[[package]]
+
name = "pkg-config"
+
version = "0.3.32"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+
[[package]]
+
name = "portable-atomic"
+
version = "1.11.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+
[[package]]
+
name = "potential_utf"
+
version = "0.1.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+
dependencies = [
+
"zerovec",
+
]
+
+
[[package]]
+
name = "powerfmt"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+
[[package]]
+
name = "ppv-lite86"
+
version = "0.2.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+
dependencies = [
+
"zerocopy",
+
]
+
+
[[package]]
+
name = "prism"
+
version = "0.1.0"
+
dependencies = [
+
"anyhow",
+
"axum",
+
"chrono",
+
"ctor",
+
"futures",
+
"reqwest",
+
"serde",
+
"serde_json",
+
"sqlx",
+
"testcontainers",
+
"testcontainers-modules",
+
"thiserror",
+
"tokio",
+
"tokio-tungstenite",
+
"tower-http",
+
"tracing",
+
"tracing-subscriber",
+
]
+
+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.103"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "prost"
+
version = "0.14.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
+
dependencies = [
+
"bytes",
+
"prost-derive",
+
]
+
+
[[package]]
+
name = "prost-derive"
+
version = "0.14.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
+
dependencies = [
+
"anyhow",
+
"itertools",
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "prost-types"
+
version = "0.14.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
+
dependencies = [
+
"prost",
+
]
+
+
[[package]]
+
name = "quote"
+
version = "1.0.42"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+
dependencies = [
+
"proc-macro2",
+
]
+
+
[[package]]
+
name = "r-efi"
+
version = "5.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+
[[package]]
+
name = "rand"
+
version = "0.8.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+
dependencies = [
+
"libc",
+
"rand_chacha 0.3.1",
+
"rand_core 0.6.4",
+
]
+
+
[[package]]
+
name = "rand"
+
version = "0.9.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+
dependencies = [
+
"rand_chacha 0.9.0",
+
"rand_core 0.9.3",
+
]
+
+
[[package]]
+
name = "rand_chacha"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+
dependencies = [
+
"ppv-lite86",
+
"rand_core 0.6.4",
+
]
+
+
[[package]]
+
name = "rand_chacha"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+
dependencies = [
+
"ppv-lite86",
+
"rand_core 0.9.3",
+
]
+
+
[[package]]
+
name = "rand_core"
+
version = "0.6.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
dependencies = [
+
"getrandom 0.2.16",
+
]
+
+
[[package]]
+
name = "rand_core"
+
version = "0.9.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+
dependencies = [
+
"getrandom 0.3.4",
+
]
+
+
[[package]]
+
name = "redox_syscall"
+
version = "0.5.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+
dependencies = [
+
"bitflags",
+
]
+
+
[[package]]
+
name = "ref-cast"
+
version = "1.0.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+
dependencies = [
+
"ref-cast-impl",
+
]
+
+
[[package]]
+
name = "ref-cast-impl"
+
version = "1.0.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "regex"
+
version = "1.12.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+
dependencies = [
+
"aho-corasick",
+
"memchr",
+
"regex-automata",
+
"regex-syntax",
+
]
+
+
[[package]]
+
name = "regex-automata"
+
version = "0.4.13"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+
dependencies = [
+
"aho-corasick",
+
"memchr",
+
"regex-syntax",
+
]
+
+
[[package]]
+
name = "regex-syntax"
+
version = "0.8.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+
+
[[package]]
+
name = "reqwest"
+
version = "0.12.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a"
+
dependencies = [
+
"base64 0.22.1",
+
"bytes",
+
"encoding_rs",
+
"futures-core",
+
"h2",
+
"http",
+
"http-body",
+
"http-body-util",
+
"hyper",
+
"hyper-rustls",
+
"hyper-tls",
+
"hyper-util",
+
"js-sys",
+
"log",
+
"mime",
+
"native-tls",
+
"percent-encoding",
+
"pin-project-lite",
+
"rustls-pki-types",
+
"serde",
+
"serde_json",
+
"serde_urlencoded",
+
"sync_wrapper",
+
"tokio",
+
"tokio-native-tls",
+
"tower",
+
"tower-http",
+
"tower-service",
+
"url",
+
"wasm-bindgen",
+
"wasm-bindgen-futures",
+
"web-sys",
+
]
+
+
[[package]]
+
name = "ring"
+
version = "0.17.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+
dependencies = [
+
"cc",
+
"cfg-if",
+
"getrandom 0.2.16",
+
"libc",
+
"untrusted",
+
"windows-sys 0.52.0",
+
]
+
+
[[package]]
+
name = "rsa"
+
version = "0.9.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
+
dependencies = [
+
"const-oid",
+
"digest",
+
"num-bigint-dig",
+
"num-integer",
+
"num-traits",
+
"pkcs1",
+
"pkcs8",
+
"rand_core 0.6.4",
+
"signature",
+
"spki",
+
"subtle",
+
"zeroize",
+
]
+
+
[[package]]
+
name = "rustc-hash"
+
version = "2.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+
[[package]]
+
name = "rustix"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+
dependencies = [
+
"bitflags",
+
"errno",
+
"libc",
+
"linux-raw-sys",
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "rustls"
+
version = "0.23.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
+
dependencies = [
+
"log",
+
"once_cell",
+
"ring",
+
"rustls-pki-types",
+
"rustls-webpki",
+
"subtle",
+
"zeroize",
+
]
+
+
[[package]]
+
name = "rustls-native-certs"
+
version = "0.8.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923"
+
dependencies = [
+
"openssl-probe",
+
"rustls-pki-types",
+
"schannel",
+
"security-framework 3.5.1",
+
]
+
+
[[package]]
+
name = "rustls-pemfile"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+
dependencies = [
+
"rustls-pki-types",
+
]
+
+
[[package]]
+
name = "rustls-pki-types"
+
version = "1.13.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
+
dependencies = [
+
"zeroize",
+
]
+
+
[[package]]
+
name = "rustls-webpki"
+
version = "0.103.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
+
dependencies = [
+
"ring",
+
"rustls-pki-types",
+
"untrusted",
+
]
+
+
[[package]]
+
name = "rustversion"
+
version = "1.0.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+
[[package]]
+
name = "ryu"
+
version = "1.0.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+
[[package]]
+
name = "schannel"
+
version = "0.1.28"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+
dependencies = [
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "schemars"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f"
+
dependencies = [
+
"dyn-clone",
+
"ref-cast",
+
"serde",
+
"serde_json",
+
]
+
+
[[package]]
+
name = "schemars"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
+
dependencies = [
+
"dyn-clone",
+
"ref-cast",
+
"serde",
+
"serde_json",
+
]
+
+
[[package]]
+
name = "scopeguard"
+
version = "1.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+
[[package]]
+
name = "security-framework"
+
version = "2.11.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+
dependencies = [
+
"bitflags",
+
"core-foundation 0.9.4",
+
"core-foundation-sys",
+
"libc",
+
"security-framework-sys",
+
]
+
+
[[package]]
+
name = "security-framework"
+
version = "3.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
+
dependencies = [
+
"bitflags",
+
"core-foundation 0.10.1",
+
"core-foundation-sys",
+
"libc",
+
"security-framework-sys",
+
]
+
+
[[package]]
+
name = "security-framework-sys"
+
version = "2.15.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+
dependencies = [
+
"core-foundation-sys",
+
"libc",
+
]
+
+
[[package]]
+
name = "serde"
+
version = "1.0.228"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+
dependencies = [
+
"serde_core",
+
"serde_derive",
+
]
+
+
[[package]]
+
name = "serde_core"
+
version = "1.0.228"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+
dependencies = [
+
"serde_derive",
+
]
+
+
[[package]]
+
name = "serde_derive"
+
version = "1.0.228"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "serde_json"
+
version = "1.0.145"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
+
dependencies = [
+
"itoa",
+
"memchr",
+
"ryu",
+
"serde",
+
"serde_core",
+
]
+
+
[[package]]
+
name = "serde_path_to_error"
+
version = "0.1.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+
dependencies = [
+
"itoa",
+
"serde",
+
"serde_core",
+
]
+
+
[[package]]
+
name = "serde_repr"
+
version = "0.1.20"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "serde_urlencoded"
+
version = "0.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+
dependencies = [
+
"form_urlencoded",
+
"itoa",
+
"ryu",
+
"serde",
+
]
+
+
[[package]]
+
name = "serde_with"
+
version = "3.16.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
+
dependencies = [
+
"base64 0.22.1",
+
"chrono",
+
"hex",
+
"indexmap 1.9.3",
+
"indexmap 2.12.1",
+
"schemars 0.9.0",
+
"schemars 1.1.0",
+
"serde_core",
+
"serde_json",
+
"serde_with_macros",
+
"time",
+
]
+
+
[[package]]
+
name = "serde_with_macros"
+
version = "3.16.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
+
dependencies = [
+
"darling",
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "sha1"
+
version = "0.10.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+
dependencies = [
+
"cfg-if",
+
"cpufeatures",
+
"digest",
+
]
+
+
[[package]]
+
name = "sha2"
+
version = "0.10.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+
dependencies = [
+
"cfg-if",
+
"cpufeatures",
+
"digest",
+
]
+
+
[[package]]
+
name = "sharded-slab"
+
version = "0.1.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+
dependencies = [
+
"lazy_static",
+
]
+
+
[[package]]
+
name = "shlex"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+
[[package]]
+
name = "signal-hook-registry"
+
version = "1.4.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad"
+
dependencies = [
+
"libc",
+
]
+
+
[[package]]
+
name = "signature"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+
dependencies = [
+
"digest",
+
"rand_core 0.6.4",
+
]
+
+
[[package]]
+
name = "simd-adler32"
+
version = "0.3.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+
+
[[package]]
+
name = "slab"
+
version = "0.4.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
+
[[package]]
+
name = "smallvec"
+
version = "1.15.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
dependencies = [
+
"serde",
+
]
+
+
[[package]]
+
name = "socket2"
+
version = "0.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+
dependencies = [
+
"libc",
+
"windows-sys 0.60.2",
+
]
+
+
[[package]]
+
name = "spin"
+
version = "0.9.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
dependencies = [
+
"lock_api",
+
]
+
+
[[package]]
+
name = "spki"
+
version = "0.7.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+
dependencies = [
+
"base64ct",
+
"der",
+
]
+
+
[[package]]
+
name = "sqlx"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+
dependencies = [
+
"sqlx-core",
+
"sqlx-macros",
+
"sqlx-mysql",
+
"sqlx-postgres",
+
"sqlx-sqlite",
+
]
+
+
[[package]]
+
name = "sqlx-core"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+
dependencies = [
+
"base64 0.22.1",
+
"bytes",
+
"chrono",
+
"crc",
+
"crossbeam-queue",
+
"either",
+
"event-listener",
+
"futures-core",
+
"futures-intrusive",
+
"futures-io",
+
"futures-util",
+
"hashbrown 0.15.5",
+
"hashlink",
+
"indexmap 2.12.1",
+
"log",
+
"memchr",
+
"once_cell",
+
"percent-encoding",
+
"serde",
+
"serde_json",
+
"sha2",
+
"smallvec",
+
"thiserror",
+
"tokio",
+
"tokio-stream",
+
"tracing",
+
"url",
+
]
+
+
[[package]]
+
name = "sqlx-macros"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"sqlx-core",
+
"sqlx-macros-core",
+
"syn",
+
]
+
+
[[package]]
+
name = "sqlx-macros-core"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+
dependencies = [
+
"dotenvy",
+
"either",
+
"heck",
+
"hex",
+
"once_cell",
+
"proc-macro2",
+
"quote",
+
"serde",
+
"serde_json",
+
"sha2",
+
"sqlx-core",
+
"sqlx-mysql",
+
"sqlx-postgres",
+
"sqlx-sqlite",
+
"syn",
+
"tokio",
+
"url",
+
]
+
+
[[package]]
+
name = "sqlx-mysql"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+
dependencies = [
+
"atoi",
+
"base64 0.22.1",
+
"bitflags",
+
"byteorder",
+
"bytes",
+
"chrono",
+
"crc",
+
"digest",
+
"dotenvy",
+
"either",
+
"futures-channel",
+
"futures-core",
+
"futures-io",
+
"futures-util",
+
"generic-array",
+
"hex",
+
"hkdf",
+
"hmac",
+
"itoa",
+
"log",
+
"md-5",
+
"memchr",
+
"once_cell",
+
"percent-encoding",
+
"rand 0.8.5",
+
"rsa",
+
"serde",
+
"sha1",
+
"sha2",
+
"smallvec",
+
"sqlx-core",
+
"stringprep",
+
"thiserror",
+
"tracing",
+
"whoami",
+
]
+
+
[[package]]
+
name = "sqlx-postgres"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+
dependencies = [
+
"atoi",
+
"base64 0.22.1",
+
"bitflags",
+
"byteorder",
+
"chrono",
+
"crc",
+
"dotenvy",
+
"etcetera 0.8.0",
+
"futures-channel",
+
"futures-core",
+
"futures-util",
+
"hex",
+
"hkdf",
+
"hmac",
+
"home",
+
"itoa",
+
"log",
+
"md-5",
+
"memchr",
+
"once_cell",
+
"rand 0.8.5",
+
"serde",
+
"serde_json",
+
"sha2",
+
"smallvec",
+
"sqlx-core",
+
"stringprep",
+
"thiserror",
+
"tracing",
+
"whoami",
+
]
+
+
[[package]]
+
name = "sqlx-sqlite"
+
version = "0.8.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+
dependencies = [
+
"atoi",
+
"chrono",
+
"flume",
+
"futures-channel",
+
"futures-core",
+
"futures-executor",
+
"futures-intrusive",
+
"futures-util",
+
"libsqlite3-sys",
+
"log",
+
"percent-encoding",
+
"serde",
+
"serde_urlencoded",
+
"sqlx-core",
+
"thiserror",
+
"tracing",
+
"url",
+
]
+
+
[[package]]
+
name = "stable_deref_trait"
+
version = "1.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+
[[package]]
+
name = "stringprep"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+
dependencies = [
+
"unicode-bidi",
+
"unicode-normalization",
+
"unicode-properties",
+
]
+
+
[[package]]
+
name = "strsim"
+
version = "0.11.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+
[[package]]
+
name = "structmeta"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"structmeta-derive",
+
"syn",
+
]
+
+
[[package]]
+
name = "structmeta-derive"
+
version = "0.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "subtle"
+
version = "2.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+
[[package]]
+
name = "syn"
+
version = "2.0.111"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "sync_wrapper"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
dependencies = [
+
"futures-core",
+
]
+
+
[[package]]
+
name = "synstructure"
+
version = "0.13.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "system-configuration"
+
version = "0.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+
dependencies = [
+
"bitflags",
+
"core-foundation 0.9.4",
+
"system-configuration-sys",
+
]
+
+
[[package]]
+
name = "system-configuration-sys"
+
version = "0.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+
dependencies = [
+
"core-foundation-sys",
+
"libc",
+
]
+
+
[[package]]
+
name = "tempfile"
+
version = "3.23.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+
dependencies = [
+
"fastrand",
+
"getrandom 0.3.4",
+
"once_cell",
+
"rustix",
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "testcontainers"
+
version = "0.26.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a347cac4368ba4f1871743adb27dc14829024d26b1763572404726b0b9943eb8"
+
dependencies = [
+
"astral-tokio-tar",
+
"async-trait",
+
"bollard",
+
"bytes",
+
"docker_credential",
+
"either",
+
"etcetera 0.11.0",
+
"ferroid",
+
"futures",
+
"itertools",
+
"log",
+
"memchr",
+
"parse-display",
+
"pin-project-lite",
+
"serde",
+
"serde_json",
+
"serde_with",
+
"thiserror",
+
"tokio",
+
"tokio-stream",
+
"tokio-util",
+
"url",
+
]
+
+
[[package]]
+
name = "testcontainers-modules"
+
version = "0.14.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5e75e78ff453128a2c7da9a5d5a3325ea34ea214d4bf51eab3417de23a4e5147"
+
dependencies = [
+
"testcontainers",
+
]
+
+
[[package]]
+
name = "thiserror"
+
version = "2.0.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+
dependencies = [
+
"thiserror-impl",
+
]
+
+
[[package]]
+
name = "thiserror-impl"
+
version = "2.0.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "thread_local"
+
version = "1.1.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+
dependencies = [
+
"cfg-if",
+
]
+
+
[[package]]
+
name = "time"
+
version = "0.3.44"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+
dependencies = [
+
"deranged",
+
"itoa",
+
"num-conv",
+
"powerfmt",
+
"serde",
+
"time-core",
+
"time-macros",
+
]
+
+
[[package]]
+
name = "time-core"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+
+
[[package]]
+
name = "time-macros"
+
version = "0.2.24"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+
dependencies = [
+
"num-conv",
+
"time-core",
+
]
+
+
[[package]]
+
name = "tinystr"
+
version = "0.8.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+
dependencies = [
+
"displaydoc",
+
"zerovec",
+
]
+
+
[[package]]
+
name = "tinyvec"
+
version = "1.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
+
dependencies = [
+
"tinyvec_macros",
+
]
+
+
[[package]]
+
name = "tinyvec_macros"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+
[[package]]
+
name = "tokio"
+
version = "1.48.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+
dependencies = [
+
"bytes",
+
"libc",
+
"mio",
+
"parking_lot",
+
"pin-project-lite",
+
"signal-hook-registry",
+
"socket2",
+
"tokio-macros",
+
"windows-sys 0.61.2",
+
]
+
+
[[package]]
+
name = "tokio-macros"
+
version = "2.6.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "tokio-native-tls"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+
dependencies = [
+
"native-tls",
+
"tokio",
+
]
+
+
[[package]]
+
name = "tokio-rustls"
+
version = "0.26.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+
dependencies = [
+
"rustls",
+
"tokio",
+
]
+
+
[[package]]
+
name = "tokio-stream"
+
version = "0.1.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+
dependencies = [
+
"futures-core",
+
"pin-project-lite",
+
"tokio",
+
]
+
+
[[package]]
+
name = "tokio-tungstenite"
+
version = "0.28.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857"
+
dependencies = [
+
"futures-util",
+
"log",
+
"tokio",
+
"tungstenite",
+
]
+
+
[[package]]
+
name = "tokio-util"
+
version = "0.7.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+
dependencies = [
+
"bytes",
+
"futures-core",
+
"futures-sink",
+
"pin-project-lite",
+
"tokio",
+
]
+
+
[[package]]
+
name = "tonic"
+
version = "0.14.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
+
dependencies = [
+
"async-trait",
+
"axum",
+
"base64 0.22.1",
+
"bytes",
+
"h2",
+
"http",
+
"http-body",
+
"http-body-util",
+
"hyper",
+
"hyper-timeout",
+
"hyper-util",
+
"percent-encoding",
+
"pin-project",
+
"socket2",
+
"sync_wrapper",
+
"tokio",
+
"tokio-stream",
+
"tower",
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "tonic-prost"
+
version = "0.14.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
+
dependencies = [
+
"bytes",
+
"prost",
+
"tonic",
+
]
+
+
[[package]]
+
name = "tower"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+
dependencies = [
+
"futures-core",
+
"futures-util",
+
"indexmap 2.12.1",
+
"pin-project-lite",
+
"slab",
+
"sync_wrapper",
+
"tokio",
+
"tokio-util",
+
"tower-layer",
+
"tower-service",
+
"tracing",
+
]
+
+
[[package]]
+
name = "tower-http"
+
version = "0.6.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+
dependencies = [
+
"async-compression",
+
"bitflags",
+
"bytes",
+
"futures-core",
+
"futures-util",
+
"http",
+
"http-body",
+
"iri-string",
+
"pin-project-lite",
+
"tokio",
+
"tokio-util",
+
"tower",
+
"tower-layer",
+
"tower-service",
+
]
+
+
[[package]]
+
name = "tower-layer"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+
[[package]]
+
name = "tower-service"
+
version = "0.3.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+
[[package]]
+
name = "tracing"
+
version = "0.1.43"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
+
dependencies = [
+
"log",
+
"pin-project-lite",
+
"tracing-attributes",
+
"tracing-core",
+
]
+
+
[[package]]
+
name = "tracing-attributes"
+
version = "0.1.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "tracing-core"
+
version = "0.1.35"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
+
dependencies = [
+
"once_cell",
+
"valuable",
+
]
+
+
[[package]]
+
name = "tracing-log"
+
version = "0.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+
dependencies = [
+
"log",
+
"once_cell",
+
"tracing-core",
+
]
+
+
[[package]]
+
name = "tracing-subscriber"
+
version = "0.3.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
+
dependencies = [
+
"matchers",
+
"nu-ansi-term",
+
"once_cell",
+
"regex-automata",
+
"sharded-slab",
+
"smallvec",
+
"thread_local",
+
"tracing",
+
"tracing-core",
+
"tracing-log",
+
]
+
+
[[package]]
+
name = "try-lock"
+
version = "0.2.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+
[[package]]
+
name = "tungstenite"
+
version = "0.28.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
+
dependencies = [
+
"bytes",
+
"data-encoding",
+
"http",
+
"httparse",
+
"log",
+
"rand 0.9.2",
+
"sha1",
+
"thiserror",
+
"utf-8",
+
]
+
+
[[package]]
+
name = "typenum"
+
version = "1.19.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+
[[package]]
+
name = "unicode-bidi"
+
version = "0.3.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+
[[package]]
+
name = "unicode-normalization"
+
version = "0.1.25"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+
dependencies = [
+
"tinyvec",
+
]
+
+
[[package]]
+
name = "unicode-properties"
+
version = "0.1.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+
[[package]]
+
name = "untrusted"
+
version = "0.9.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+
[[package]]
+
name = "ureq"
+
version = "3.1.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
+
dependencies = [
+
"base64 0.22.1",
+
"log",
+
"percent-encoding",
+
"rustls",
+
"rustls-pki-types",
+
"ureq-proto",
+
"utf-8",
+
"webpki-roots",
+
]
+
+
[[package]]
+
name = "ureq-proto"
+
version = "0.5.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
+
dependencies = [
+
"base64 0.22.1",
+
"http",
+
"httparse",
+
"log",
+
]
+
+
[[package]]
+
name = "url"
+
version = "2.5.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+
dependencies = [
+
"form_urlencoded",
+
"idna",
+
"percent-encoding",
+
"serde",
+
]
+
+
[[package]]
+
name = "utf-8"
+
version = "0.7.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+
[[package]]
+
name = "utf8_iter"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+
[[package]]
+
name = "valuable"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+
[[package]]
+
name = "vcpkg"
+
version = "0.2.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+
[[package]]
+
name = "version_check"
+
version = "0.9.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+
[[package]]
+
name = "want"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+
dependencies = [
+
"try-lock",
+
]
+
+
[[package]]
+
name = "wasi"
+
version = "0.11.1+wasi-snapshot-preview1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+
[[package]]
+
name = "wasip2"
+
version = "1.0.1+wasi-0.2.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+
dependencies = [
+
"wit-bindgen",
+
]
+
+
[[package]]
+
name = "wasite"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+
[[package]]
+
name = "wasm-bindgen"
+
version = "0.2.106"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
+
dependencies = [
+
"cfg-if",
+
"once_cell",
+
"rustversion",
+
"wasm-bindgen-macro",
+
"wasm-bindgen-shared",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-futures"
+
version = "0.4.56"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
+
dependencies = [
+
"cfg-if",
+
"js-sys",
+
"once_cell",
+
"wasm-bindgen",
+
"web-sys",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-macro"
+
version = "0.2.106"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
+
dependencies = [
+
"quote",
+
"wasm-bindgen-macro-support",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-macro-support"
+
version = "0.2.106"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
+
dependencies = [
+
"bumpalo",
+
"proc-macro2",
+
"quote",
+
"syn",
+
"wasm-bindgen-shared",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-shared"
+
version = "0.2.106"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "web-sys"
+
version = "0.3.83"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
+
dependencies = [
+
"js-sys",
+
"wasm-bindgen",
+
]
+
+
[[package]]
+
name = "web-time"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+
dependencies = [
+
"js-sys",
+
"wasm-bindgen",
+
]
+
+
[[package]]
+
name = "webpki-roots"
+
version = "1.0.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
+
dependencies = [
+
"rustls-pki-types",
+
]
+
+
[[package]]
+
name = "whoami"
+
version = "1.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+
dependencies = [
+
"libredox",
+
"wasite",
+
]
+
+
[[package]]
+
name = "winapi"
+
version = "0.3.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+
dependencies = [
+
"winapi-i686-pc-windows-gnu",
+
"winapi-x86_64-pc-windows-gnu",
+
]
+
+
[[package]]
+
name = "winapi-i686-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+
[[package]]
+
name = "winapi-x86_64-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+
[[package]]
+
name = "windows-core"
+
version = "0.62.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+
dependencies = [
+
"windows-implement",
+
"windows-interface",
+
"windows-link",
+
"windows-result",
+
"windows-strings",
+
]
+
+
[[package]]
+
name = "windows-implement"
+
version = "0.60.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "windows-interface"
+
version = "0.59.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "windows-link"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+
[[package]]
+
name = "windows-registry"
+
version = "0.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+
dependencies = [
+
"windows-link",
+
"windows-result",
+
"windows-strings",
+
]
+
+
[[package]]
+
name = "windows-result"
+
version = "0.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+
dependencies = [
+
"windows-link",
+
]
+
+
[[package]]
+
name = "windows-strings"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+
dependencies = [
+
"windows-link",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.48.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+
dependencies = [
+
"windows-targets 0.48.5",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.52.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+
dependencies = [
+
"windows-targets 0.52.6",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.60.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+
dependencies = [
+
"windows-targets 0.53.5",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.61.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+
dependencies = [
+
"windows-link",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+
dependencies = [
+
"windows_aarch64_gnullvm 0.48.5",
+
"windows_aarch64_msvc 0.48.5",
+
"windows_i686_gnu 0.48.5",
+
"windows_i686_msvc 0.48.5",
+
"windows_x86_64_gnu 0.48.5",
+
"windows_x86_64_gnullvm 0.48.5",
+
"windows_x86_64_msvc 0.48.5",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+
dependencies = [
+
"windows_aarch64_gnullvm 0.52.6",
+
"windows_aarch64_msvc 0.52.6",
+
"windows_i686_gnu 0.52.6",
+
"windows_i686_gnullvm 0.52.6",
+
"windows_i686_msvc 0.52.6",
+
"windows_x86_64_gnu 0.52.6",
+
"windows_x86_64_gnullvm 0.52.6",
+
"windows_x86_64_msvc 0.52.6",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.53.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+
dependencies = [
+
"windows-link",
+
"windows_aarch64_gnullvm 0.53.1",
+
"windows_aarch64_msvc 0.53.1",
+
"windows_i686_gnu 0.53.1",
+
"windows_i686_gnullvm 0.53.1",
+
"windows_i686_msvc 0.53.1",
+
"windows_x86_64_gnu 0.53.1",
+
"windows_x86_64_gnullvm 0.53.1",
+
"windows_x86_64_msvc 0.53.1",
+
]
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.48.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.53.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+
[[package]]
+
name = "wit-bindgen"
+
version = "0.46.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+
[[package]]
+
name = "writeable"
+
version = "0.6.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+
[[package]]
+
name = "xattr"
+
version = "1.6.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+
dependencies = [
+
"libc",
+
"rustix",
+
]
+
+
[[package]]
+
name = "yoke"
+
version = "0.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+
dependencies = [
+
"stable_deref_trait",
+
"yoke-derive",
+
"zerofrom",
+
]
+
+
[[package]]
+
name = "yoke-derive"
+
version = "0.8.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
"synstructure",
+
]
+
+
[[package]]
+
name = "zerocopy"
+
version = "0.8.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
+
dependencies = [
+
"zerocopy-derive",
+
]
+
+
[[package]]
+
name = "zerocopy-derive"
+
version = "0.8.31"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "zerofrom"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+
dependencies = [
+
"zerofrom-derive",
+
]
+
+
[[package]]
+
name = "zerofrom-derive"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
"synstructure",
+
]
+
+
[[package]]
+
name = "zeroize"
+
version = "1.8.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+
[[package]]
+
name = "zerotrie"
+
version = "0.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+
dependencies = [
+
"displaydoc",
+
"yoke",
+
"zerofrom",
+
]
+
+
[[package]]
+
name = "zerovec"
+
version = "0.11.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+
dependencies = [
+
"yoke",
+
"zerofrom",
+
"zerovec-derive",
+
]
+
+
[[package]]
+
name = "zerovec-derive"
+
version = "0.11.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+25
Cargo.toml
···
+
[package]
+
name = "prism"
+
version = "0.1.0"
+
edition = "2024"
+
+
[dependencies]
+
axum = { version = "0.8", features = ["ws"] }
+
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "json", "chrono"] }
+
tokio = { version = "1", features = ["full"] }
+
tokio-tungstenite = "0.28"
+
serde = { version = "1", features = ["derive"] }
+
serde_json = "1"
+
tracing = "0.1"
+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+
chrono = { version = "0.4", features = ["serde"] }
+
thiserror = "2"
+
anyhow = "1"
+
futures = "0.3"
+
tower-http = { version = "0.6", features = ["cors", "compression-gzip"] }
+
+
[dev-dependencies]
+
ctor = "0.6"
+
testcontainers = "0.26"
+
testcontainers-modules = { version = "0.14", features = ["postgres"] }
+
reqwest = { version = "0.12", features = ["json"] }
+26 -9
Dockerfile
···
-
FROM oven/bun:latest
-
WORKDIR /usr/src/app
+
# Build stage
+
FROM rust:1.92-alpine AS builder
+
+
WORKDIR /app
+
+
RUN apk add --no-cache musl-dev openssl-dev openssl-libs-static pkgconfig
+
+
COPY Cargo.toml Cargo.lock* ./
+
+
RUN mkdir src && echo "fn main() {}" > src/main.rs
+
RUN cargo build --release || true
+
RUN rm -rf src
+
+
COPY src ./src
+
COPY migrations ./migrations
+
COPY .sqlx ./.sqlx
+
+
RUN touch src/main.rs && cargo build --release
+
+
# Runtime stage
+
FROM alpine:3.21
-
COPY package.json bun.lock tsconfig.json ./
+
WORKDIR /app
-
RUN bun install --frozen-lockfile
+
RUN apk add --no-cache ca-certificates
-
COPY ./lexicons ./lexicons
-
COPY ./src ./src
+
COPY --from=builder /app/target/release/prism /app/prism
+
COPY --from=builder /app/migrations /app/migrations
-
# Expose the port your server listens on
EXPOSE 3000
-
# Set the default command to run your TypeScript spawner directly
-
CMD ["bun", "run", "src/index.ts"]
+
CMD ["/app/prism"]
+66 -18
README.md
···
# Prism
-
Prism is a simple [ATProto](https://atproto.com/) AppView service for gmstn. Point it to a relay and it'll filter relay events for you and store/cache them for future retrieval so that queries aren't so heavy on other PDSes directly.
+
Prism是一个用于gmstn的[ATProto](https://atproto.com/) AppView服务。它从[TAP](https://docs.bsky.app/blog/introducing-tap)消费事件,并将其存储/缓存以供将来检索,这样查询就不会对其他PDS造成太大负担。
<a href="https://docs.bsky.app/docs/advanced-guides/federation-architecture#app-views">
<img src="./assets/prism-appview-concept.png" width="400" alt="appview-prism-pinkfloyd"/>
</a>
-
Other services may subscribe to a Prism instance as though it is a relay itself and receive the filtered gmstn events as they are played from the parent relay.
+
Prism实现了用于访问缓存feed的XRPC端点。Lexicon定义可以在`lexicons/`目录中找到。
+
+
## 架构
-
Prism implements endpoints for accessing the cache feed, which are `xrpc` compatible and the lexicons can be found in `lexicons/`.
+
```
+
ATProto Relay → TAP (backfill + firehose) → Prism (Rust/Axum) → PostgreSQL
+
+
XRPC API + WebSocket
+
```
-
Backfill batteries included. :3
+
## 本地运行
-
## 如何在本地运行
+
### 前置条件
-
请按照以下步骤在您的本地计算机上设置和运行此应用程序。
+
Rust, docker/podman
-
启动数据库: 首先,使用 Podman 或 Docker Compose 启动 PostgreSQL 数据库服务。
+
### 快速开始
+
+
1. 启动数据库:
```bash
-
docker-compose up
+
docker compose up postgres
+
# or
+
podman-compose up postgres
```
-
+
2. 运行数据库迁移:
```bash
-
podman-compose up
+
export DATABASE_URL=postgres://postgres:postgres@localhost:5432/prism
+
cargo sqlx migrate run --source migrations
```
-
安装依赖: 使用 pnpm 安装项目所需的所有依赖项。
+
3. 运行开发服务器:
```bash
-
bun install
+
cargo run
```
-
运行数据库迁移: 应用所有待处理的数据库迁移,以确保您的数据库结构是最新的。
+
服务将在 http://localhost:3000 上可用
+
+
### 完整堆栈 (包含TAP)
```bash
-
bun run db:migrate
+
docker compose up
```
-
启动开发服务器: 最后,启动本地开发服务器。
+
这将启动:
+
- PostgreSQL 端口 5432
+
- TAP 端口 8080 (HTTP) 和 2480 (WebSocket)
+
- Prism 端口 3000
+
+
## API端点
+
+
| 端点 | 描述 |
+
|----------|-------------|
+
| `GET /xrpc/systems.gmstn.development.channel.listChannels?author=<did>` | 按作者列出频道 |
+
| `GET /xrpc/systems.gmstn.development.channel.listInvites?recipient=<did>` | 按接收者列出邀请 |
+
| `GET /xrpc/systems.gmstn.development.channel.listMemberships?recipient=<did>` | 按接收者列出成员资格 |
+
| `GET /xrpc/systems.gmstn.development.lattice.listLattices?author=<did>` | 按作者列出晶格 |
+
| `GET /xrpc/systems.gmstn.development.shard.listShards?author=<did>` | 按作者列出碎片 |
+
| `WS /ws` | 用于实时记录更新的WebSocket |
+
+
所有列表端点都支持`limit`(1-100,默认50)和`cursor`(ISO8601时间戳)参数。
+
+
## 配置
+
+
环境变量:
+
+
| 变量 | 默认值 | 描述 |
+
|----------|---------|-------------|
+
| `DATABASE_URL` | `postgres://postgres:postgres@localhost:5432/prism` | PostgreSQL连接字符串 |
+
| `TAP_WS_URL` | `ws://localhost:2480/channel` | TAP WebSocket URL |
+
| `HOST` | `0.0.0.0` | 服务器绑定地址 |
+
| `PORT` | `3000` | 服务器端口 |
+
| `RUST_LOG` | `prism=info` | 日志级别 |
+
+
## 开发
```bash
-
bun run dev
-
```
+
# 检查代码
+
cargo check
-
服务启动后,您应该可以在 http://localhost:3000 (或您配置的端口) 上访问应用程序。
+
# 运行测试
+
cargo test
+
+
# 准备sqlx离线缓存(需要运行中的数据库)
+
cargo sqlx prepare
+
```
-33
biome.json
···
-
{
-
"formatter": {
-
"indentStyle": "space",
-
"lineWidth": 100
-
},
-
"linter": {
-
"rules": {
-
"a11y": {
-
"useAriaPropsForRole": "off",
-
"useButtonType": "off",
-
"useSemanticElements": "off",
-
"noSvgWithoutTitle": "off"
-
},
-
"complexity": {
-
"noStaticOnlyClass": "off",
-
"noForEach": "off"
-
},
-
"suspicious": {
-
"noArrayIndexKey": "off",
-
"noPrototypeBuiltins": "off"
-
},
-
"style": {
-
"noNonNullAssertion": "off"
-
}
-
}
-
},
-
"javascript": {
-
"formatter": {
-
"quoteStyle": "single"
-
}
-
}
-
}
-
-580
bun.lock
···
-
{
-
"lockfileVersion": 1,
-
"configVersion": 1,
-
"workspaces": {
-
"": {
-
"name": "prism",
-
"dependencies": {
-
"@atcute/repo": "^0.1.0",
-
"@atcute/xrpc-server": "^0.1.3",
-
"@atcute/xrpc-server-bun": "^0.1.1",
-
"@skyware/firehose": "^0.5.2",
-
"compression": "^1.8.1",
-
"dotenv": "^17.2.3",
-
"express": "^5.1.0",
-
"helmet": "^8.1.0",
-
"kysely": "^0.28.8",
-
"pg": "^8.16.3",
-
"pino": "^10.1.0",
-
"ws": "^8.18.3",
-
},
-
"devDependencies": {
-
"@types/bun": "^1.3.3",
-
"@types/compression": "^1.8.1",
-
"@types/express": "^5.0.5",
-
"@types/pg": "^8.15.6",
-
"@types/ws": "^8.18.1",
-
"biome": "^0.3.3",
-
"pino-pretty": "^13.1.2",
-
"typescript": "^5.9.3",
-
},
-
},
-
},
-
"packages": {
-
"@atcute/car": ["@atcute/car@5.0.0", "", { "dependencies": { "@atcute/cbor": "^2.2.7", "@atcute/cid": "^2.2.6", "@atcute/uint8array": "^1.0.5", "@atcute/varint": "^1.0.3" } }, "sha512-OIY2xTXv8lSpZsDSn/UYQtJSMvDw5Hi4Q+uyvmiqSM+fht08QRAEq/nxa5YFciPZ3nfDFnZ3//EgJw7QhkSXLQ=="],
-
-
"@atcute/cbor": ["@atcute/cbor@2.2.7", "", { "dependencies": { "@atcute/cid": "^2.2.5", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5" } }, "sha512-/mwAF0gnokOphceZqFq3uzMGdd8sbw5y6bxF8CRutRkCCUcpjjpJc5fkLwhxyGgOveF3mZuHE6p7t/+IAqb7Aw=="],
-
-
"@atcute/cid": ["@atcute/cid@2.2.6", "", { "dependencies": { "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5" } }, "sha512-bTAHHbJ24p+E//V4KCS4xdmd39o211jJswvqQOevj7vk+5IYcgDLx1ryZWZ1sEPOo9x875li/kj5gpKL14RDwQ=="],
-
-
"@atcute/crypto": ["@atcute/crypto@2.2.6", "", { "dependencies": { "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "@noble/secp256k1": "^3.0.0" } }, "sha512-vkuexF+kmrKE1/Uqzub99Qi4QpnxA2jbu60E6PTgL4XypELQ6rb59MB/J1VbY2gs0kd3ET7+L3+NWpKD5nXyfA=="],
-
-
"@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="],
-
-
"@atcute/identity-resolver": ["@atcute/identity-resolver@1.1.4", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@atcute/util-fetch": "^1.0.3", "@badrap/valita": "^0.4.6" }, "peerDependencies": { "@atcute/identity": "^1.0.0" } }, "sha512-/SVh8vf2cXFJenmBnGeYF2aY3WGQm3cJeew5NWTlkqoy3LvJ5wkvKq9PWu4Tv653VF40rPOp6LOdVr9Fa+q5rA=="],
-
-
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
-
-
"@atcute/mst": ["@atcute/mst@0.1.0", "", { "dependencies": { "@atcute/cbor": "^2.2.7", "@atcute/cid": "^2.2.6", "@atcute/uint8array": "^1.0.5" } }, "sha512-h+iDToKEnBpigk2DOHjSqY63vJtjYKUIztqu1CZ0P+I54wV2SrgoqAXAT1xrW6A1Iup8cjTv+U2H5WVG4KxPLw=="],
-
-
"@atcute/multibase": ["@atcute/multibase@1.1.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.5" } }, "sha512-HBxuCgYLKPPxETV0Rot4VP9e24vKl8JdzGCZOVsDaOXJgbRZoRIF67Lp0H/OgnJeH/Xpva8Z5ReoTNJE5dn3kg=="],
-
-
"@atcute/repo": ["@atcute/repo@0.1.0", "", { "dependencies": { "@atcute/car": "^5.0.0", "@atcute/cbor": "^2.2.7", "@atcute/cid": "^2.2.6", "@atcute/crypto": "^2.2.5", "@atcute/lexicons": "^1.2.2", "@atcute/mst": "^0.1.0", "@atcute/uint8array": "^1.0.5" } }, "sha512-INiYAuma8dydBu7cqd2WVpcXh3mzhIepYBUqFWAK5MqMulPRLTRCc/9GW3G9pxYrOdlvLCVamG2Jf8XK0nuFEw=="],
-
-
"@atcute/uint8array": ["@atcute/uint8array@1.0.5", "", {}, "sha512-XLWWxoR2HNl2qU+FCr0rp1APwJXci7HnzbOQLxK55OaMNBXZ19+xNC5ii4QCsThsDxa4JS/JTzuiQLziITWf2Q=="],
-
-
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.4", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-sIU9Qk0dE8PLEXSfhy+gIJV+HpiiknMytCI2SqLlqd0vgZUtEKI/EQfP+23LHWvP+CLCzVDOa6cpH045OlmNBg=="],
-
-
"@atcute/varint": ["@atcute/varint@1.0.3", "", {}, "sha512-fdvMPyBB+McDT+Ai5e9RwEbwYV4yjZ60S2Dn5PTjGqUyxvoCH1z42viuheDZRUDkmfQehXJTZ5az7dSozVNtog=="],
-
-
"@atcute/xrpc-server": ["@atcute/xrpc-server@0.1.3", "", { "dependencies": { "@atcute/cbor": "^2.2.7", "@atcute/crypto": "^2.2.5", "@atcute/identity": "^1.1.1", "@atcute/identity-resolver": "^1.1.4", "@atcute/lexicons": "^1.2.2", "@atcute/multibase": "^1.1.6", "@atcute/uint8array": "^1.0.5", "@badrap/valita": "^0.4.6", "nanoid": "^5.1.5" } }, "sha512-AMig6MuAL5VfXRZVsQqQXKCXnZgpjTc6UM6RggvyE1qVT8y9tZPFXdP5tt/p6Jf+h4cAw+XMu2uyrGpUmnTSyQ=="],
-
-
"@atcute/xrpc-server-bun": ["@atcute/xrpc-server-bun@0.1.1", "", { "peerDependencies": { "@atcute/xrpc-server": "^0.1.3" } }, "sha512-fAn93prgn+yldeSeaGEaU37irPCvIFWSc1Dm6D7ip5KiaINWhkdEkln+IuRCklo3slC4hcjHIgZnIigHy/p8HA=="],
-
-
"@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="],
-
-
"@noble/secp256k1": ["@noble/secp256k1@3.0.0", "", {}, "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg=="],
-
-
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
-
-
"@skyware/firehose": ["@skyware/firehose@0.5.2", "", { "dependencies": { "@atcute/car": "^3.0.3", "@atcute/cbor": "^2.2.2", "nanoevents": "^9.1.0" } }, "sha512-Ayg/cF0BkakBNQVA51ClDka0+nC96WiARNrGElMQxfqbwao0PBaCXkunfr8qS4DWS3TqLnR6hA9mvm1vAYlxJQ=="],
-
-
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
-
-
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
-
-
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
-
-
"@types/compression": ["@types/compression@1.8.1", "", { "dependencies": { "@types/express": "*", "@types/node": "*" } }, "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q=="],
-
-
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
-
-
"@types/express": ["@types/express@5.0.5", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^1" } }, "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ=="],
-
-
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="],
-
-
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
-
-
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
-
-
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
-
-
"@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
-
-
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
-
-
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
-
-
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
-
-
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
-
-
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
-
-
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
-
-
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
-
-
"ansi-escapes": ["ansi-escapes@1.4.0", "", {}, "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw=="],
-
-
"ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="],
-
-
"ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="],
-
-
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
-
-
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
-
-
"assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="],
-
-
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
-
-
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
-
-
"aws-sign2": ["aws-sign2@0.7.0", "", {}, "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="],
-
-
"aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="],
-
-
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
-
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
-
-
"biome": ["biome@0.3.3", "", { "dependencies": { "bluebird": "^3.4.1", "chalk": "^1.1.3", "commander": "^2.9.0", "editor": "^1.0.0", "fs-promise": "^0.5.0", "inquirer-promise": "0.0.3", "request-promise": "^3.0.0", "untildify": "^3.0.2", "user-home": "^2.0.0" }, "bin": { "biome": "./dist/index.js" } }, "sha512-4LXjrQYbn9iTXu9Y4SKT7ABzTV0WnLDHCVSd2fPUOKsy1gQ+E4xPFmlY1zcWexoi0j7fGHItlL6OWA2CZ/yYAQ=="],
-
-
"bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
-
-
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
-
-
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
-
-
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
-
-
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
-
-
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
-
-
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
-
-
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
-
-
"chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="],
-
-
"cli-cursor": ["cli-cursor@1.0.2", "", { "dependencies": { "restore-cursor": "^1.0.1" } }, "sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A=="],
-
-
"cli-width": ["cli-width@1.1.1", "", {}, "sha512-eMU2akIeEIkCxGXUNmDnJq1KzOIiPnJ+rKqRe6hcxE3vIOPvpMrBYOn/Bl7zNlYJj/zQxXquAnozHUCf9Whnsg=="],
-
-
"code-point-at": ["code-point-at@1.1.0", "", {}, "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA=="],
-
-
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
-
-
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
-
-
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
-
-
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
-
-
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
-
-
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
-
-
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
-
-
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
-
-
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
-
-
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
-
-
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
-
-
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
-
-
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
-
-
"dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="],
-
-
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
-
-
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
-
-
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
-
-
"dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
-
-
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
-
-
"earlgrey-runtime": ["earlgrey-runtime@0.1.2", "", { "dependencies": { "core-js": "^2.4.0", "kaiser": ">=0.0.4", "lodash": "^4.17.2", "regenerator-runtime": "^0.9.5" } }, "sha512-T4qoScXi5TwALDv8nlGTvOuCT8jXcKcxtO8qVdqv46IA2GHJfQzwoBPbkOmORnyhu3A98cVVuhWLsM2CzPljJg=="],
-
-
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
-
-
"editor": ["editor@1.0.0", "", {}, "sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw=="],
-
-
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
-
-
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
-
-
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
-
-
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
-
-
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
-
-
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
-
-
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
-
-
"escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
-
-
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
-
-
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
-
-
"exit-hook": ["exit-hook@1.1.1", "", {}, "sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg=="],
-
-
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
-
-
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
-
-
"extsprintf": ["extsprintf@1.3.0", "", {}, "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="],
-
-
"fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="],
-
-
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
-
-
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
-
-
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
-
-
"figures": ["figures@1.7.0", "", { "dependencies": { "escape-string-regexp": "^1.0.5", "object-assign": "^4.1.0" } }, "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ=="],
-
-
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
-
-
"forever-agent": ["forever-agent@0.6.1", "", {}, "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="],
-
-
"form-data": ["form-data@2.3.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", "mime-types": "^2.1.12" } }, "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ=="],
-
-
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
-
-
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
-
-
"fs-extra": ["fs-extra@0.26.7", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^2.1.0", "klaw": "^1.0.0", "path-is-absolute": "^1.0.0", "rimraf": "^2.2.8" } }, "sha512-waKu+1KumRhYv8D8gMRCKJGAMI9pRnPuEb1mvgYD0f7wBscg+h6bW4FDTmEZhB9VKxvoTtxW+Y7bnIlB7zja6Q=="],
-
-
"fs-promise": ["fs-promise@0.5.0", "", { "dependencies": { "any-promise": "^1.0.0", "fs-extra": "^0.26.5", "mz": "^2.3.1", "thenify-all": "^1.6.0" } }, "sha512-Y+4F4ujhEcayCJt6JmzcOun9MYGQwz+bVUiuBmTkJImhBHKpBvmVPZR9wtfiF7k3ffwAOAuurygQe+cPLSFQhw=="],
-
-
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
-
-
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
-
-
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
-
-
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
-
-
"getpass": ["getpass@0.1.7", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng=="],
-
-
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
-
-
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
-
-
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
-
-
"har-schema": ["har-schema@2.0.0", "", {}, "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="],
-
-
"har-validator": ["har-validator@5.1.5", "", { "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" } }, "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w=="],
-
-
"has-ansi": ["has-ansi@2.0.0", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg=="],
-
-
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
-
-
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
-
-
"helmet": ["helmet@8.1.0", "", {}, "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg=="],
-
-
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
-
-
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
-
-
"http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="],
-
-
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
-
-
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
-
-
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
-
-
"inquirer": ["inquirer@0.11.4", "", { "dependencies": { "ansi-escapes": "^1.1.0", "ansi-regex": "^2.0.0", "chalk": "^1.0.0", "cli-cursor": "^1.0.1", "cli-width": "^1.0.1", "figures": "^1.3.5", "lodash": "^3.3.1", "readline2": "^1.0.1", "run-async": "^0.1.0", "rx-lite": "^3.1.2", "string-width": "^1.0.1", "strip-ansi": "^3.0.0", "through": "^2.3.6" } }, "sha512-QR+2TW90jnKk9LUUtbcA3yQXKt2rDEKMh6+BAZQIeumtzHexnwVLdPakSslGijXYLJCzFv7GMXbFCn0pA00EUw=="],
-
-
"inquirer-promise": ["inquirer-promise@0.0.3", "", { "dependencies": { "earlgrey-runtime": ">=0.0.11", "inquirer": "^0.11.3" } }, "sha512-82CQX586JAV9GAgU9yXZsMDs+NorjA0nLhkfFx9+PReyOnuoHRbHrC1Z90sS95bFJI1Tm1gzMObuE0HabzkJpg=="],
-
-
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
-
-
"is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="],
-
-
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
-
-
"is-typedarray": ["is-typedarray@1.0.0", "", {}, "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="],
-
-
"isstream": ["isstream@0.1.2", "", {}, "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="],
-
-
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
-
-
"jsbn": ["jsbn@0.1.1", "", {}, "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="],
-
-
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
-
-
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
-
-
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
-
-
"jsonfile": ["jsonfile@2.4.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw=="],
-
-
"jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="],
-
-
"kaiser": ["kaiser@0.0.4", "", { "dependencies": { "earlgrey-runtime": ">=0.0.10" } }, "sha512-m8ju+rmBqvclZmyrOXgGGhOYSjKJK6RN1NhqEltemY87UqZOxEkizg9TOy1vQSyJ01Wx6SAPuuN0iO2Mgislvw=="],
-
-
"klaw": ["klaw@1.3.1", "", { "optionalDependencies": { "graceful-fs": "^4.1.9" } }, "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw=="],
-
-
"kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="],
-
-
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
-
-
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
-
-
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
-
-
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
-
-
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
-
-
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
-
-
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
-
-
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
-
-
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
-
-
"mute-stream": ["mute-stream@0.0.5", "", {}, "sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg=="],
-
-
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
-
-
"nanoevents": ["nanoevents@9.1.0", "", {}, "sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A=="],
-
-
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
-
-
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
-
-
"number-is-nan": ["number-is-nan@1.0.1", "", {}, "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ=="],
-
-
"oauth-sign": ["oauth-sign@0.9.0", "", {}, "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="],
-
-
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
-
-
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
-
-
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
-
-
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
-
-
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
-
-
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
-
-
"onetime": ["onetime@1.1.0", "", {}, "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A=="],
-
-
"os-homedir": ["os-homedir@1.0.2", "", {}, "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ=="],
-
-
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
-
-
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
-
-
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
-
-
"performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
-
-
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
-
-
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
-
-
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
-
-
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
-
-
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
-
-
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
-
-
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
-
-
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
-
-
"pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="],
-
-
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
-
-
"pino-pretty": ["pino-pretty@13.1.2", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ=="],
-
-
"pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="],
-
-
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
-
-
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
-
-
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
-
-
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
-
-
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
-
-
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
-
-
"psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="],
-
-
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
-
-
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
-
-
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
-
-
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
-
-
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
-
-
"raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="],
-
-
"readline2": ["readline2@1.0.1", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "mute-stream": "0.0.5" } }, "sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g=="],
-
-
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
-
-
"regenerator-runtime": ["regenerator-runtime@0.9.6", "", {}, "sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw=="],
-
-
"request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="],
-
-
"request-promise": ["request-promise@3.0.0", "", { "dependencies": { "bluebird": "^3.3", "lodash": "^4.6.1", "request": "^2.34" } }, "sha512-wVGUX+BoKxYsavTA72i6qHcyLbjzM4LR4y/AmDCqlbuMAursZdDWO7PmgbGAUvD2SeEJ5iB99VSq/U51i/DNbw=="],
-
-
"restore-cursor": ["restore-cursor@1.0.1", "", { "dependencies": { "exit-hook": "^1.0.0", "onetime": "^1.0.0" } }, "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw=="],
-
-
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
-
-
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
-
-
"run-async": ["run-async@0.1.0", "", { "dependencies": { "once": "^1.3.0" } }, "sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw=="],
-
-
"rx-lite": ["rx-lite@3.1.2", "", {}, "sha512-1I1+G2gteLB8Tkt8YI1sJvSIfa0lWuRtC8GjvtyPBcLSF5jBCCJJqKrpER5JU5r6Bhe+i9/pK3VMuUcXu0kdwQ=="],
-
-
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
-
-
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
-
-
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
-
-
"secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
-
-
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
-
-
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
-
-
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
-
-
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
-
-
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
-
-
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
-
-
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
-
-
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
-
-
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
-
-
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
-
-
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
-
-
"string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="],
-
-
"strip-ansi": ["strip-ansi@3.0.1", "", { "dependencies": { "ansi-regex": "^2.0.0" } }, "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg=="],
-
-
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
-
-
"supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="],
-
-
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
-
-
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
-
-
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
-
-
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
-
-
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
-
-
"tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
-
-
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
-
-
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
-
-
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
-
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
-
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
-
-
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
-
-
"untildify": ["untildify@3.0.3", "", {}, "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA=="],
-
-
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
-
-
"user-home": ["user-home@2.0.0", "", { "dependencies": { "os-homedir": "^1.0.0" } }, "sha512-KMWqdlOcjCYdtIJpicDSFBQ8nFwS2i9sslAd6f4+CBGcU4gist2REnr2fxj2YocvJFxSF3ZOHLYLVZnUxv4BZQ=="],
-
-
"uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
-
-
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
-
-
"verror": ["verror@1.10.0", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw=="],
-
-
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
-
-
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
-
-
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
-
-
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
-
-
"@atcute/identity/@atcute/lexicons": ["@atcute/lexicons@1.2.5", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-9yO9WdgxW8jZ7SbzUycH710z+JmsQ9W9n5S6i6eghYju32kkluFmgBeS47r8e8p2+Dv4DemS7o/3SUGsX9FR5Q=="],
-
-
"@skyware/firehose/@atcute/car": ["@atcute/car@3.1.3", "", { "dependencies": { "@atcute/cbor": "^2.2.7", "@atcute/cid": "^2.2.5", "@atcute/uint8array": "^1.0.5", "@atcute/varint": "^1.0.3", "yocto-queue": "^1.2.1" } }, "sha512-WJ13bAEt7TjDMVi09ubjLtvhdljbWInGm9Kfy7Y6NhrmiyC/aZYaA/zHX/bHI6xv1c/h3SQduWqxOr4ae49eqA=="],
-
-
"@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
-
-
"accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
-
-
"body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"express/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"finalhandler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
-
-
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
-
-
"inquirer/lodash": ["lodash@3.10.1", "", {}, "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ=="],
-
-
"raw-body/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
-
-
"request/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
-
-
"request/qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="],
-
-
"router/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"send/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
-
"body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
-
"express/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
-
"finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
-
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
-
-
"request/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
-
-
"router/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
}
-
}
-53
docker-compose.yaml
···
-
services:
-
prism-db:
-
image: postgres:latest
-
environment:
-
- POSTGRES_USER=prism
-
- POSTGRES_PASSWORD=prism
-
- POSTGRES_DB=prism
-
ports:
-
- "5432:5432"
-
healthcheck:
-
test: ["CMD-SHELL", "pg_isready -U prism"]
-
interval: 5s
-
timeout: 5s
-
retries: 5
-
-
prism-server:
-
build: .
-
command: bun run start:server
-
ports:
-
- "3000:3000"
-
environment:
-
- DATABASE_URL=postgresql://prism:prism@prism-db:5432/prism
-
- NODE_ENV=production
-
- LOG_LEVEL=info
-
depends_on:
-
prism-db:
-
condition: service_healthy
-
restart: always
-
-
prism-firehose:
-
build: .
-
command: bun run start:firehose
-
environment:
-
- DATABASE_URL=postgresql://prism:prism@prism-db:5432/prism
-
- NODE_ENV=production
-
- LOG_LEVEL=info
-
depends_on:
-
prism-db:
-
condition: service_healthy
-
restart: always
-
-
prism-backfill:
-
build: .
-
command: bun run start:backfill
-
environment:
-
- DATABASE_URL=postgresql://prism:prism@prism-db:5432/prism
-
- NODE_ENV=production
-
- LOG_LEVEL=info
-
depends_on:
-
prism-db:
-
condition: service_healthy
-
# This service is a task, so we don't restart it automatically if it exits successfully
-
restart: on-failure
+49
docker-compose.yml
···
+
services:
+
postgres:
+
image: postgres:18-alpine
+
environment:
+
POSTGRES_USER: postgres
+
POSTGRES_PASSWORD: postgres
+
POSTGRES_DB: prism
+
volumes:
+
- postgres_data:/var/lib/postgresql/data
+
ports:
+
- "5432:5432"
+
healthcheck:
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
+
interval: 5s
+
timeout: 5s
+
retries: 5
+
+
tap:
+
image: ghcr.io/bluesky-social/indigo:tap-latest
+
environment:
+
TAP_UPSTREAM_RELAY: ${TAP_UPSTREAM_RELAY:-wss://bsky.network}
+
TAP_SIGNAL_COLLECTION: ${TAP_SIGNAL_COLLECTION:-systems.gmstn.development.lattice}
+
TAP_COLLECTION_FILTERS: ${TAP_COLLECTION_FILTERS:-systems.gmstn.development.*}
+
TAP_HTTP_ADDR: 0.0.0.0:8080
+
TAP_WS_ADDR: 0.0.0.0:2480
+
ports:
+
- "8080:8080"
+
- "2480:2480"
+
restart: unless-stopped
+
+
prism:
+
build: .
+
environment:
+
DATABASE_URL: postgres://postgres:postgres@postgres:5432/prism
+
TAP_WS_URL: ws://tap:2480/channel
+
HOST: 0.0.0.0
+
PORT: 3000
+
RUST_LOG: prism=debug,tower_http=debug
+
ports:
+
- "3000:3000"
+
depends_on:
+
postgres:
+
condition: service_healthy
+
tap:
+
condition: service_started
+
restart: unless-stopped
+
+
volumes:
+
postgres_data:
+58
justfile
···
+
set dotenv-load
+
+
default:
+
@just --list
+
+
build:
+
cargo build --release
+
+
check:
+
SQLX_OFFLINE=true cargo check
+
+
run:
+
cargo run
+
+
watch:
+
cargo watch -x run
+
+
test:
+
cargo test
+
+
test-verbose:
+
cargo test -- --nocapture
+
+
test-api:
+
cargo test --test api -- --nocapture
+
+
test-pagination:
+
cargo test --test pagination -- --nocapture
+
+
test-tap:
+
cargo test --test tap_consumer -- --nocapture
+
+
fmt:
+
cargo fmt
+
+
lint:
+
cargo clippy -- -D warnings
+
+
db-up:
+
docker compose up -d postgres
+
+
db-down:
+
docker compose down
+
+
migrate:
+
cargo sqlx migrate run --source migrations
+
+
sqlx-prepare:
+
cargo sqlx prepare
+
+
clean:
+
cargo clean
+
+
full-stack:
+
docker compose up
+
+
full-stack-build:
+
docker compose up --build
+70
migrations/001_init.sql
···
+
CREATE TABLE IF NOT EXISTS account (
+
did TEXT PRIMARY KEY,
+
handle TEXT NOT NULL,
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+
);
+
+
CREATE TABLE IF NOT EXISTS lattice (
+
uri TEXT PRIMARY KEY,
+
cid TEXT UNIQUE NOT NULL,
+
creator_did TEXT NOT NULL REFERENCES account(did),
+
description TEXT,
+
created_at TIMESTAMPTZ NOT NULL,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
data JSONB NOT NULL
+
);
+
+
CREATE INDEX IF NOT EXISTS idx_lattice_creator_indexed ON lattice(creator_did, indexed_at DESC);
+
+
CREATE TABLE IF NOT EXISTS shard (
+
uri TEXT PRIMARY KEY,
+
cid TEXT UNIQUE NOT NULL,
+
creator_did TEXT NOT NULL REFERENCES account(did),
+
description TEXT,
+
created_at TIMESTAMPTZ NOT NULL,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
data JSONB NOT NULL
+
);
+
+
CREATE INDEX IF NOT EXISTS idx_shard_creator_indexed ON shard(creator_did, indexed_at DESC);
+
+
CREATE TABLE IF NOT EXISTS channel (
+
cid TEXT PRIMARY KEY,
+
uri TEXT UNIQUE,
+
creator_did TEXT REFERENCES account(did),
+
name TEXT,
+
topic TEXT,
+
created_at TIMESTAMPTZ,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
data JSONB
+
);
+
+
CREATE INDEX IF NOT EXISTS idx_channel_creator_indexed ON channel(creator_did, indexed_at DESC);
+
+
CREATE TABLE IF NOT EXISTS channel_invite (
+
cid TEXT PRIMARY KEY,
+
uri TEXT UNIQUE,
+
creator_did TEXT REFERENCES account(did),
+
channel TEXT NOT NULL REFERENCES channel(cid),
+
recipient_did TEXT REFERENCES account(did),
+
created_at TIMESTAMPTZ,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
data JSONB
+
);
+
+
CREATE INDEX IF NOT EXISTS idx_invite_recipient_indexed ON channel_invite(recipient_did, indexed_at DESC);
+
+
CREATE TABLE IF NOT EXISTS channel_membership (
+
cid TEXT PRIMARY KEY,
+
uri TEXT UNIQUE NOT NULL,
+
channel TEXT NOT NULL REFERENCES channel(cid),
+
invite TEXT NOT NULL REFERENCES channel_invite(cid),
+
recipient_did TEXT REFERENCES account(did),
+
state TEXT NOT NULL,
+
created_at TIMESTAMPTZ NOT NULL,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
updated_at TIMESTAMPTZ,
+
data JSONB NOT NULL
+
);
+
+
CREATE INDEX IF NOT EXISTS idx_membership_recipient_indexed ON channel_membership(recipient_did, indexed_at DESC);
+42
migrations/002_unified_records.sql
···
+
CREATE TABLE record (
+
uri TEXT PRIMARY KEY,
+
cid TEXT UNIQUE NOT NULL,
+
collection TEXT NOT NULL,
+
creator_did TEXT NOT NULL,
+
created_at TIMESTAMPTZ NOT NULL,
+
indexed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
data JSONB NOT NULL,
+
target_did TEXT,
+
ref_cids TEXT[] NOT NULL DEFAULT '{}'
+
);
+
+
INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)
+
SELECT uri, cid, 'systems.gmstn.development.lattice', creator_did, created_at, indexed_at, data, NULL, '{}'
+
FROM lattice;
+
+
INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)
+
SELECT uri, cid, 'systems.gmstn.development.shard', creator_did, created_at, indexed_at, data, NULL, '{}'
+
FROM shard;
+
+
INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)
+
SELECT uri, cid, 'systems.gmstn.development.channel', creator_did, created_at, indexed_at, COALESCE(data, '{}'), NULL, '{}'
+
FROM channel WHERE uri IS NOT NULL;
+
+
INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)
+
SELECT uri, cid, 'systems.gmstn.development.channel.invite', creator_did, created_at, indexed_at, COALESCE(data, '{}'), recipient_did, ARRAY[channel]
+
FROM channel_invite WHERE uri IS NOT NULL;
+
+
INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)
+
SELECT uri, cid, 'systems.gmstn.development.channel.membership', recipient_did, created_at, indexed_at, data, recipient_did, ARRAY[channel, invite]
+
FROM channel_membership;
+
+
CREATE INDEX idx_record_collection_creator ON record(collection, creator_did, indexed_at DESC);
+
CREATE INDEX idx_record_collection_target ON record(collection, target_did, indexed_at DESC) WHERE target_did IS NOT NULL;
+
CREATE INDEX idx_record_cid ON record(cid);
+
CREATE INDEX idx_record_ref_cids ON record USING GIN(ref_cids);
+
+
DROP TABLE channel_membership;
+
DROP TABLE channel_invite;
+
DROP TABLE channel;
+
DROP TABLE shard;
+
DROP TABLE lattice;
-205
migrations/20251127130000_init.ts
···
-
import { Kysely, sql } from 'kysely'
-
-
export async function up(db: Kysely<any>): Promise<void> {
-
await db.schema
-
.createTable('pds')
-
.addColumn('hostname', 'text', (col) => col.primaryKey())
-
.addColumn('added_at', 'timestamp', (col) =>
-
col.defaultTo(sql`now()`).notNull()
-
)
-
.execute()
-
-
await db.schema
-
.createTable('account')
-
.addColumn('did', 'text', (col) => col.primaryKey().notNull())
-
.addColumn('handle', 'text', (col) => col.notNull().unique())
-
.addColumn('pds_hostname', 'text', (col) =>
-
col.references('pds.hostname')
-
)
-
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.execute()
-
-
await db.schema
-
.createTable('lattice')
-
.addColumn('uri', 'text', (col) => col.primaryKey().notNull())
-
.addColumn('cid', 'text', (col) => col.notNull())
-
.addColumn('creator_did', 'text', (col) =>
-
col.references('account.did').notNull()
-
)
-
.addColumn('description', 'text')
-
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('indexed_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('data', 'jsonb', (col) => col.notNull())
-
.execute()
-
-
await db.schema
-
.createTable('shard')
-
.addColumn('uri', 'text', (col) => col.primaryKey().notNull())
-
.addColumn('cid', 'text', (col) => col.notNull())
-
.addColumn('creator_did', 'text', (col) =>
-
col.references('account.did').notNull()
-
)
-
.addColumn('description', 'text')
-
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('indexed_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('data', 'jsonb', (col) => col.notNull())
-
.execute()
-
-
await db.schema
-
.createTable('channel')
-
.addColumn('cid', 'text', (col) => col.primaryKey().notNull())
-
.addColumn('uri', 'text')
-
.addColumn('creator_did', 'text', (col) =>
-
col.references('account.did')
-
)
-
.addColumn('name', 'text')
-
.addColumn('topic', 'text')
-
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('indexed_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('data', 'jsonb')
-
.execute()
-
-
await db.schema
-
.createTable('channel_invite')
-
.addColumn('cid', 'text', (col) => col.primaryKey())
-
.addColumn('uri', 'text')
-
.addColumn('channel', 'text', (col) =>
-
col.references('channel.cid')
-
)
-
.addColumn('creator_did', 'text', (col) =>
-
col.references('account.did')
-
)
-
.addColumn('recipient_did', 'text', (col) =>
-
col.references('account.did')
-
)
-
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('indexed_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('data', 'jsonb')
-
.execute()
-
-
await db.schema
-
.createTable('channel_membership')
-
.addColumn('uri', 'text', (col) => col.primaryKey().notNull())
-
.addColumn('cid', 'text', (col) => col.notNull())
-
.addColumn('channel', 'text', (col) =>
-
col.references('channel.cid').notNull()
-
)
-
.addColumn('invite', 'text', (col) =>
-
col.references('channel_invite.cid').notNull()
-
)
-
.addColumn('recipient_did', 'text', (col) =>
-
col.references('account.did').notNull()
-
)
-
.addColumn('state', 'text', (col) => col.notNull())
-
.addColumn('created_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('indexed_at', 'timestamptz', (col) => col.notNull().defaultTo(sql`now()`))
-
.addColumn('updated_at', 'timestamptz')
-
.addColumn('data', 'jsonb', (col) => col.notNull())
-
.execute()
-
-
await db.schema
-
.createTable('raw_record_queue')
-
.addColumn('id', 'serial', (col) => col.primaryKey())
-
.addColumn('record_data', 'jsonb', (col) => col.notNull())
-
.addColumn('status', 'varchar(20)', (col) =>
-
col.notNull().defaultTo('pending')
-
)
-
.addColumn('created_at', 'timestamp', (col) =>
-
col.defaultTo(sql`now()`).notNull()
-
)
-
.addColumn('updated_at', 'timestamp', (col) =>
-
col.defaultTo(sql`now()`).notNull()
-
)
-
.addCheckConstraint(
-
'raw_record_queue_status_check',
-
sql`status IN ('pending', 'processing', 'complete', 'failed')`
-
)
-
.execute()
-
-
await db.schema
-
.createIndex('raw_record_queue_status_created_at_idx')
-
.on('raw_record_queue')
-
.columns(['status', 'created_at'])
-
.execute()
-
-
await db.schema
-
.createTable('firehose_cursor')
-
.addColumn('id', 'integer', (col) => col.primaryKey())
-
.addColumn('cursor', 'varchar', (col) => col.notNull())
-
.addColumn('updated_at', 'timestamp', (col) =>
-
col.defaultTo(sql`now()`).notNull()
-
)
-
.execute()
-
-
await db.insertInto('firehose_cursor')
-
.values({ id: 1, cursor: '0' })
-
.execute()
-
-
await sql`
-
CREATE OR REPLACE FUNCTION notify_new_raw_record()
-
RETURNS trigger AS $$
-
BEGIN
-
PERFORM pg_notify('new_raw_record', NEW.id::text);
-
RETURN NEW;
-
END;
-
$$ LANGUAGE plpgsql;
-
`.execute(db)
-
-
await sql`
-
CREATE TRIGGER trigger_notify_new_raw_record
-
AFTER INSERT ON raw_record_queue
-
FOR EACH ROW
-
EXECUTE FUNCTION notify_new_raw_record();
-
`.execute(db)
-
-
await db.schema
-
.createIndex('idx_channel_invite_recipient_indexed')
-
.on('channel_invite')
-
.columns(['recipient_did', 'indexed_at'])
-
.execute()
-
-
await db.schema
-
.createIndex('idx_channel_creator_indexed')
-
.on('channel')
-
.columns(['creator_did', 'indexed_at'])
-
.execute()
-
-
await db.schema
-
.createIndex('idx_channel_membership_recipient_indexed')
-
.on('channel_membership')
-
.columns(['recipient_did', 'indexed_at'])
-
.execute()
-
-
await db.schema
-
.createIndex('idx_lattice_creator_indexed')
-
.on('lattice')
-
.columns(['creator_did', 'indexed_at'])
-
.execute()
-
-
await db.schema
-
.createIndex('idx_shard_creator_indexed')
-
.on('shard')
-
.columns(['creator_did', 'indexed_at'])
-
.execute()
-
}
-
-
export async function down(db: Kysely<any>): Promise<void> {
-
await db.schema.dropIndex('idx_shard_creator_indexed').execute()
-
await db.schema.dropIndex('idx_lattice_creator_indexed').execute()
-
await db.schema.dropIndex('idx_channel_membership_recipient_indexed').execute()
-
await db.schema.dropIndex('idx_channel_creator_indexed').execute()
-
await db.schema.dropIndex('idx_channel_invite_recipient_indexed').execute()
-
-
await sql`DROP TRIGGER IF EXISTS trigger_notify_new_raw_record ON raw_record_queue`.execute(db)
-
await sql`DROP FUNCTION IF EXISTS notify_new_raw_record`.execute(db)
-
-
await db.schema.dropTable('firehose_cursor').execute()
-
await db.schema.dropTable('raw_record_queue').execute()
-
await db.schema.dropTable('channel_membership').execute()
-
await db.schema.dropTable('channel_invite').execute()
-
await db.schema.dropTable('channel').execute()
-
await db.schema.dropTable('shard').execute()
-
await db.schema.dropTable('lattice').execute()
-
await db.schema.dropTable('account').execute()
-
await db.schema.dropTable('pds').execute()
-
}
-46
package.json
···
-
{
-
"name": "prism",
-
"version": "1.0.0",
-
"description": "",
-
"main": "index.js",
-
"scripts": {
-
"test": "bun test",
-
"dev": "bun run src/index.ts",
-
"start": "bun run src/index.ts",
-
"start:server": "bun run src/services/api.ts",
-
"start:firehose": "bun run src/services/firehose-listen.ts",
-
"start:backfill": "bun run src/scripts/start-backfill.ts",
-
"lint": "bun run biome lint ./src && bun run biome format ./src",
-
"lint:fix": "bun run biome lint --fix ./src && bun run biome format --fix ./src",
-
"db:migrate": "bun run src/scripts/migrate.ts latest",
-
"db:revert": "bun run src/scripts/migrate.ts down"
-
},
-
"keywords": [],
-
"author": "",
-
"license": "ISC",
-
"packageManager": "pnpm@10.20.0",
-
"dependencies": {
-
"@atcute/repo": "^0.1.0",
-
"@atcute/xrpc-server": "^0.1.3",
-
"@atcute/xrpc-server-bun": "^0.1.1",
-
"@skyware/firehose": "^0.5.2",
-
"compression": "^1.8.1",
-
"dotenv": "^17.2.3",
-
"express": "^5.1.0",
-
"helmet": "^8.1.0",
-
"kysely": "^0.28.8",
-
"pg": "^8.16.3",
-
"pino": "^10.1.0",
-
"ws": "^8.18.3"
-
},
-
"devDependencies": {
-
"@types/bun": "^1.3.3",
-
"@types/compression": "^1.8.1",
-
"@types/express": "^5.0.5",
-
"@types/pg": "^8.15.6",
-
"@types/ws": "^8.18.1",
-
"biome": "^0.3.3",
-
"pino-pretty": "^13.1.2",
-
"typescript": "^5.9.3"
-
}
-
}
-206
src/api/channel.ts
···
-
import { InvalidRequestError, json } from '@atcute/xrpc-server';
-
import { db } from '../db';
-
import { parseLimit } from '../util/params';
-
import { logger } from '../util/logger';
-
-
export async function listInvitesHandler({ params }: { params: any }) {
-
try {
-
const { limit: rawLimit, cursor, recipient } = params;
-
const limit = parseLimit(rawLimit);
-
-
if (!recipient) {
-
throw new InvalidRequestError({ description: 'Missing required parameter: recipient' });
-
}
-
-
if (typeof recipient !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "recipient" must be a string' });
-
}
-
-
let query = db
-
.selectFrom('channel_invite as ci')
-
.innerJoin('channel as c', 'ci.channel', 'c.cid')
-
.select([
-
'ci.uri',
-
'ci.cid',
-
'ci.creator_did',
-
'ci.indexed_at',
-
'ci.created_at',
-
'ci.channel',
-
'c.uri as channel_uri'
-
])
-
.where('recipient_did', '=', recipient)
-
.orderBy('ci.indexed_at', 'desc')
-
.limit(limit);
-
-
if (cursor) {
-
if (typeof cursor !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "cursor" must be a string' });
-
}
-
query = query.where('ci.indexed_at', '<', new Date(cursor));
-
}
-
-
const results = await query.execute();
-
-
const invites = results.map(row => {
-
return {
-
uri: row.uri,
-
cid: row.cid,
-
author: row.creator_did,
-
channel: {
-
uri: row.channel_uri,
-
cid: row.channel,
-
},
-
createdAt: row.created_at.toISOString(),
-
};
-
});
-
-
const lastResult = results.at(-1);
-
const nextCursor = lastResult ? lastResult.indexed_at.toISOString() : undefined;
-
-
return json({
-
invites,
-
cursor: nextCursor,
-
});
-
-
} catch (error) {
-
logger.error({ err: error }, 'Internal server error in listInvitesHandler');
-
throw error;
-
}
-
}
-
-
export async function listChannelsHandler({ params }: { params: any }) {
-
try {
-
const { limit: rawLimit, cursor, author } = params;
-
const limit = parseLimit(rawLimit);
-
-
if (!author) {
-
throw new InvalidRequestError({ description: 'Missing required parameter: author' });
-
}
-
-
if (typeof author !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "author" must be a string' });
-
}
-
-
let query = db
-
.selectFrom('channel')
-
.select([
-
'uri',
-
'cid',
-
'creator_did',
-
'name',
-
'topic',
-
'created_at',
-
'indexed_at'
-
])
-
.where('creator_did', '=', author)
-
.orderBy('indexed_at', 'desc')
-
.limit(limit);
-
-
if (cursor) {
-
if (typeof cursor !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "cursor" must be a string' });
-
}
-
query = query.where('indexed_at', '<', new Date(cursor));
-
}
-
-
const results = await query.execute();
-
-
const channels = results.map(row => {
-
return {
-
uri: row.uri,
-
cid: row.cid,
-
author: row.creator_did,
-
displayName: row.name,
-
description: row.topic,
-
createdAt: row.created_at.toISOString(),
-
indexedAt: row.indexed_at.toISOString(),
-
};
-
}).filter(item => item !== null);
-
-
const lastResult = results.at(-1);
-
const nextCursor = lastResult ? lastResult.indexed_at.toISOString() : undefined;
-
-
return json({
-
channels,
-
cursor: nextCursor,
-
});
-
-
} catch (error) {
-
logger.error({ err: error }, 'Internal server error in listChannelsHandler');
-
throw error;
-
}
-
}
-
-
export async function listMembershipsHandler({ params }: { params: any }) {
-
try {
-
const { limit: rawLimit, cursor, recipient } = params;
-
const limit = parseLimit(rawLimit);
-
-
if (!recipient) {
-
throw new InvalidRequestError({ description: 'Missing required parameter: recipient' });
-
}
-
-
if (typeof recipient !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "recipient" must be a string' });
-
}
-
-
let query = db
-
.selectFrom('channel_membership as cm')
-
.innerJoin('channel as c', 'cm.channel', 'c.cid')
-
.innerJoin('channel_invite as i', 'cm.invite', 'i.cid')
-
.select([
-
'cm.uri',
-
'cm.cid',
-
'cm.state',
-
'cm.created_at',
-
'cm.updated_at',
-
'cm.indexed_at',
-
'cm.channel',
-
'c.uri as channel_uri',
-
'cm.invite',
-
'i.uri as invite_uri'
-
])
-
.where('cm.recipient_did', '=', recipient)
-
.orderBy('cm.indexed_at', 'desc')
-
.limit(limit);
-
-
if (cursor) {
-
if (typeof cursor !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "cursor" must be a string' });
-
}
-
query = query.where('cm.indexed_at', '<', new Date(cursor));
-
}
-
-
const results = await query.execute();
-
-
const memberships = results.map(row => {
-
return {
-
uri: row.uri,
-
cid: row.cid,
-
state: row.state,
-
createdAt: row.created_at.toISOString(),
-
updatedAt: (row.updated_at ?? row.created_at).toISOString(),
-
channel: {
-
uri: row.channel_uri,
-
cid: row.channel,
-
},
-
invite: {
-
uri: row.invite_uri,
-
cid: row.invite,
-
},
-
};
-
}).filter(item => item !== null);
-
-
const lastResult = results.at(-1);
-
const nextCursor = lastResult ? lastResult.indexed_at.toISOString() : undefined;
-
-
return json({
-
memberships,
-
cursor: nextCursor,
-
});
-
-
} catch (error) {
-
logger.error({ err: error }, 'Internal server error in listMembershipsHandler');
-
throw error;
-
}
-
}
+55
src/api/channels.rs
···
+
use std::sync::Arc;
+
+
use axum::{
+
extract::{Query, State},
+
Json,
+
};
+
use chrono::{DateTime, Utc};
+
use serde::{Deserialize, Serialize};
+
+
use crate::api::error::ApiError;
+
use crate::db;
+
use crate::records::{ChannelView, CHANNEL_COLLECTION};
+
use crate::AppState;
+
+
#[derive(Debug, Deserialize)]
+
pub struct ListChannelsParams {
+
pub author: Option<String>,
+
pub limit: Option<i64>,
+
pub cursor: Option<String>,
+
}
+
+
#[derive(Debug, Serialize)]
+
pub struct ListChannelsResponse {
+
pub channels: Vec<ChannelView>,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
pub cursor: Option<String>,
+
}
+
+
pub async fn list_channels(
+
State(state): State<Arc<AppState>>,
+
Query(params): Query<ListChannelsParams>,
+
) -> Result<Json<ListChannelsResponse>, ApiError> {
+
let author = params.author.ok_or_else(|| {
+
ApiError::InvalidRequest("Missing required parameter: author".to_string())
+
})?;
+
+
let limit = params.limit.unwrap_or(50).min(100).max(1);
+
let cursor: Option<DateTime<Utc>> = params
+
.cursor
+
.as_ref()
+
.and_then(|c| DateTime::parse_from_rfc3339(c).ok())
+
.map(|dt| dt.with_timezone(&Utc));
+
+
let records =
+
db::list_records_by_creator(state.db.pool(), CHANNEL_COLLECTION, &author, cursor, limit)
+
.await?;
+
+
let next_cursor = records.last().map(|r| r.indexed_at.to_rfc3339());
+
let views: Vec<ChannelView> = records.into_iter().map(ChannelView::from).collect();
+
+
Ok(Json(ListChannelsResponse {
+
channels: views,
+
cursor: next_cursor,
+
}))
+
}
+64
src/api/error.rs
···
+
use axum::{
+
http::StatusCode,
+
response::{IntoResponse, Response},
+
Json,
+
};
+
use serde::Serialize;
+
+
#[derive(Debug, Serialize)]
+
pub struct XrpcError {
+
pub error: &'static str,
+
pub message: String,
+
}
+
+
impl XrpcError {
+
pub fn method_not_found(path: &str) -> Self {
+
Self {
+
error: "MethodNotFound",
+
message: format!("Method not found: {}", path),
+
}
+
}
+
+
pub fn invalid_request(message: impl Into<String>) -> Self {
+
Self {
+
error: "InvalidRequest",
+
message: message.into(),
+
}
+
}
+
+
pub fn internal_error(message: impl Into<String>) -> Self {
+
Self {
+
error: "InternalServerError",
+
message: message.into(),
+
}
+
}
+
}
+
+
pub enum ApiError {
+
MethodNotFound(String),
+
InvalidRequest(String),
+
Internal(String),
+
}
+
+
impl IntoResponse for ApiError {
+
fn into_response(self) -> Response {
+
let (status, error) = match self {
+
ApiError::MethodNotFound(path) => {
+
(StatusCode::NOT_FOUND, XrpcError::method_not_found(&path))
+
}
+
ApiError::InvalidRequest(msg) => {
+
(StatusCode::BAD_REQUEST, XrpcError::invalid_request(msg))
+
}
+
ApiError::Internal(msg) => {
+
(StatusCode::INTERNAL_SERVER_ERROR, XrpcError::internal_error(msg))
+
}
+
};
+
(status, Json(error)).into_response()
+
}
+
}
+
+
impl From<sqlx::Error> for ApiError {
+
fn from(e: sqlx::Error) -> Self {
+
ApiError::Internal(e.to_string())
+
}
+
}
-25
src/api/index.ts
···
-
import { XRPCRouter, json } from '@atcute/xrpc-server';
-
import { createBunWebSocket } from '@atcute/xrpc-server-bun';
-
import { listInvitesHandler, listChannelsHandler, listMembershipsHandler } from './channel';
-
import { listLatticesHandler } from './lattice';
-
import { listShardsHandler } from './shard';
-
import {
-
listInvitesSchema,
-
listChannelsSchema,
-
listMembershipsSchema,
-
listLatticesSchema,
-
listShardsSchema
-
} from './schemas';
-
-
export function createServer() {
-
const { adapter, wrap } = createBunWebSocket();
-
const router = new XRPCRouter({ websocket: adapter });
-
-
router.add(listInvitesSchema, { handler: listInvitesHandler });
-
router.add(listChannelsSchema, { handler: listChannelsHandler });
-
router.add(listMembershipsSchema, { handler: listMembershipsHandler });
-
router.add(listLatticesSchema, { handler: listLatticesHandler });
-
router.add(listShardsSchema, { handler: listShardsHandler });
-
-
return wrap(router);
-
}
+55
src/api/invites.rs
···
+
use std::sync::Arc;
+
+
use axum::{
+
extract::{Query, State},
+
Json,
+
};
+
use chrono::{DateTime, Utc};
+
use serde::{Deserialize, Serialize};
+
+
use crate::api::error::ApiError;
+
use crate::db;
+
use crate::records::{InviteView, INVITE_COLLECTION};
+
use crate::AppState;
+
+
#[derive(Debug, Deserialize)]
+
pub struct ListInvitesParams {
+
pub recipient: Option<String>,
+
pub limit: Option<i64>,
+
pub cursor: Option<String>,
+
}
+
+
#[derive(Debug, Serialize)]
+
pub struct ListInvitesResponse {
+
pub invites: Vec<InviteView>,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
pub cursor: Option<String>,
+
}
+
+
pub async fn list_invites(
+
State(state): State<Arc<AppState>>,
+
Query(params): Query<ListInvitesParams>,
+
) -> Result<Json<ListInvitesResponse>, ApiError> {
+
let recipient = params.recipient.ok_or_else(|| {
+
ApiError::InvalidRequest("Missing required parameter: recipient".to_string())
+
})?;
+
+
let limit = params.limit.unwrap_or(50).min(100).max(1);
+
let cursor: Option<DateTime<Utc>> = params
+
.cursor
+
.as_ref()
+
.and_then(|c| DateTime::parse_from_rfc3339(c).ok())
+
.map(|dt| dt.with_timezone(&Utc));
+
+
let records =
+
db::list_records_by_target(state.db.pool(), INVITE_COLLECTION, &recipient, cursor, limit)
+
.await?;
+
+
let next_cursor = records.last().map(|r| r.indexed_at.to_rfc3339());
+
let views: Vec<InviteView> = records.into_iter().map(InviteView::from).collect();
+
+
Ok(Json(ListInvitesResponse {
+
invites: views,
+
cursor: next_cursor,
+
}))
+
}
-67
src/api/lattice.ts
···
-
import { InvalidRequestError, json } from '@atcute/xrpc-server';
-
import { db } from '../db';
-
import { parseLimit } from '../util/params';
-
import { logger } from '../util/logger';
-
-
export async function listLatticesHandler(ctx: {
-
params: any;
-
}) {
-
try {
-
const { limit: rawLimit, cursor, author } = ctx.params;
-
const limit = parseLimit(rawLimit);
-
-
if (!author) {
-
throw new InvalidRequestError({ description: 'Missing required parameter: author' });
-
}
-
-
if (typeof author !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "author" must be a string' });
-
}
-
-
let query = db
-
.selectFrom('lattice')
-
.select([
-
'uri',
-
'cid',
-
'creator_did',
-
'description',
-
'created_at',
-
'indexed_at'
-
])
-
.where('creator_did', '=', author)
-
.orderBy('indexed_at', 'desc')
-
.limit(limit);
-
-
if (cursor) {
-
if (typeof cursor !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "cursor" must be a string' });
-
}
-
query = query.where('indexed_at', '<', new Date(cursor));
-
}
-
-
const results = await query.execute();
-
-
const lattices = results.map(row => {
-
return {
-
uri: row.uri,
-
cid: row.cid,
-
author: row.creator_did,
-
description: row.description,
-
createdAt: row.created_at.toISOString(),
-
indexedAt: row.indexed_at.toISOString(),
-
};
-
}).filter(item => item !== null);
-
-
const lastResult = results.at(-1);
-
const nextCursor = lastResult ? lastResult.indexed_at.toISOString() : undefined;
-
-
return json({
-
lattices,
-
cursor: nextCursor,
-
});
-
-
} catch (error) {
-
logger.error({ err: error }, 'Internal server error in listLatticesHandler');
-
throw error;
-
}
-
}
+55
src/api/lattices.rs
···
+
use std::sync::Arc;
+
+
use axum::{
+
extract::{Query, State},
+
Json,
+
};
+
use chrono::{DateTime, Utc};
+
use serde::{Deserialize, Serialize};
+
+
use crate::api::error::ApiError;
+
use crate::db;
+
use crate::records::{LatticeView, LATTICE_COLLECTION};
+
use crate::AppState;
+
+
#[derive(Debug, Deserialize)]
+
pub struct ListLatticesParams {
+
pub author: Option<String>,
+
pub limit: Option<i64>,
+
pub cursor: Option<String>,
+
}
+
+
#[derive(Debug, Serialize)]
+
pub struct ListLatticesResponse {
+
pub lattices: Vec<LatticeView>,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
pub cursor: Option<String>,
+
}
+
+
pub async fn list_lattices(
+
State(state): State<Arc<AppState>>,
+
Query(params): Query<ListLatticesParams>,
+
) -> Result<Json<ListLatticesResponse>, ApiError> {
+
let author = params.author.ok_or_else(|| {
+
ApiError::InvalidRequest("Missing required parameter: author".to_string())
+
})?;
+
+
let limit = params.limit.unwrap_or(50).min(100).max(1);
+
let cursor: Option<DateTime<Utc>> = params
+
.cursor
+
.as_ref()
+
.and_then(|c| DateTime::parse_from_rfc3339(c).ok())
+
.map(|dt| dt.with_timezone(&Utc));
+
+
let records =
+
db::list_records_by_creator(state.db.pool(), LATTICE_COLLECTION, &author, cursor, limit)
+
.await?;
+
+
let next_cursor = records.last().map(|r| r.indexed_at.to_rfc3339());
+
let views: Vec<LatticeView> = records.into_iter().map(LatticeView::from).collect();
+
+
Ok(Json(ListLatticesResponse {
+
lattices: views,
+
cursor: next_cursor,
+
}))
+
}
+55
src/api/memberships.rs
···
+
use std::sync::Arc;
+
+
use axum::{
+
extract::{Query, State},
+
Json,
+
};
+
use chrono::{DateTime, Utc};
+
use serde::{Deserialize, Serialize};
+
+
use crate::api::error::ApiError;
+
use crate::db;
+
use crate::records::{MembershipView, MEMBERSHIP_COLLECTION};
+
use crate::AppState;
+
+
#[derive(Debug, Deserialize)]
+
pub struct ListMembershipsParams {
+
pub recipient: Option<String>,
+
pub limit: Option<i64>,
+
pub cursor: Option<String>,
+
}
+
+
#[derive(Debug, Serialize)]
+
pub struct ListMembershipsResponse {
+
pub memberships: Vec<MembershipView>,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
pub cursor: Option<String>,
+
}
+
+
pub async fn list_memberships(
+
State(state): State<Arc<AppState>>,
+
Query(params): Query<ListMembershipsParams>,
+
) -> Result<Json<ListMembershipsResponse>, ApiError> {
+
let recipient = params.recipient.ok_or_else(|| {
+
ApiError::InvalidRequest("Missing required parameter: recipient".to_string())
+
})?;
+
+
let limit = params.limit.unwrap_or(50).min(100).max(1);
+
let cursor: Option<DateTime<Utc>> = params
+
.cursor
+
.as_ref()
+
.and_then(|c| DateTime::parse_from_rfc3339(c).ok())
+
.map(|dt| dt.with_timezone(&Utc));
+
+
let records =
+
db::list_records_by_target(state.db.pool(), MEMBERSHIP_COLLECTION, &recipient, cursor, limit)
+
.await?;
+
+
let next_cursor = records.last().map(|r| r.indexed_at.to_rfc3339());
+
let views: Vec<MembershipView> = records.into_iter().map(MembershipView::from).collect();
+
+
Ok(Json(ListMembershipsResponse {
+
memberships: views,
+
cursor: next_cursor,
+
}))
+
}
+47
src/api/mod.rs
···
+
mod channels;
+
pub mod error;
+
mod invites;
+
mod lattices;
+
mod memberships;
+
mod shards;
+
+
use std::sync::Arc;
+
+
use axum::{
+
extract::Request,
+
routing::get,
+
Router,
+
};
+
+
use crate::AppState;
+
use error::ApiError;
+
+
pub fn router() -> Router<Arc<AppState>> {
+
Router::new()
+
.route(
+
"/xrpc/systems.gmstn.development.channel.listChannels",
+
get(channels::list_channels),
+
)
+
.route(
+
"/xrpc/systems.gmstn.development.channel.listInvites",
+
get(invites::list_invites),
+
)
+
.route(
+
"/xrpc/systems.gmstn.development.channel.listMemberships",
+
get(memberships::list_memberships),
+
)
+
.route(
+
"/xrpc/systems.gmstn.development.lattice.listLattices",
+
get(lattices::list_lattices),
+
)
+
.route(
+
"/xrpc/systems.gmstn.development.shard.listShards",
+
get(shards::list_shards),
+
)
+
.fallback(fallback_handler)
+
}
+
+
async fn fallback_handler(request: Request) -> ApiError {
+
let path = request.uri().path().to_string();
+
ApiError::MethodNotFound(path)
+
}
-126
src/api/schemas.ts
···
-
import { query, object, string, integer, optional, array, didString, resourceUriString, datetimeString, constrain, integerRange } from '@atcute/lexicons/validations';
-
-
const commonParams = {
-
limit: optional(constrain(integer(), [integerRange(1, 100)]), 50),
-
cursor: optional(string()),
-
};
-
-
export const listInvitesSchema = query('systems.gmstn.development.channel.listInvites', {
-
params: object({
-
recipient: didString(),
-
limit: optional(constrain(integer(), [integerRange(1, 100)]), 50),
-
cursor: optional(string()),
-
}),
-
output: {
-
type: 'lex',
-
schema: object({
-
cursor: optional(string()),
-
invites: array(object({
-
uri: string(),
-
cid: string(),
-
author: didString(),
-
channel: object({
-
uri: resourceUriString(),
-
cid: string(),
-
}),
-
createdAt: datetimeString(),
-
})),
-
}),
-
},
-
});
-
-
export const listChannelsSchema = query('systems.gmstn.development.channel.listChannels', {
-
params: object({
-
author: didString(),
-
limit: optional(constrain(integer(), [integerRange(1, 100)]), 50),
-
cursor: optional(string()),
-
}),
-
output: {
-
type: 'lex',
-
schema: object({
-
cursor: optional(string()),
-
channels: array(object({
-
uri: resourceUriString(),
-
cid: string(),
-
author: didString(),
-
displayName: string(),
-
description: optional(string()),
-
createdAt: datetimeString(),
-
indexedAt: datetimeString(),
-
})),
-
}),
-
},
-
});
-
-
export const listMembershipsSchema = query('systems.gmstn.development.channel.listMemberships', {
-
params: object({
-
recipient: didString(),
-
limit: optional(constrain(integer(), [integerRange(1, 100)]), 50),
-
cursor: optional(string()),
-
}),
-
output: {
-
type: 'lex',
-
schema: object({
-
cursor: optional(string()),
-
memberships: array(object({
-
uri: string(),
-
cid: string(),
-
state: string(),
-
createdAt: datetimeString(),
-
updatedAt: datetimeString(),
-
channel: object({
-
uri: resourceUriString(),
-
cid: string(),
-
}),
-
invite: object({
-
uri: resourceUriString(),
-
cid: string(),
-
}),
-
})),
-
}),
-
},
-
});
-
-
export const listLatticesSchema = query('systems.gmstn.development.lattice.listLattices', {
-
params: object({
-
author: didString(),
-
limit: optional(constrain(integer(), [integerRange(1, 100)]), 50),
-
cursor: optional(string()),
-
}),
-
output: {
-
type: 'lex',
-
schema: object({
-
cursor: optional(string()),
-
lattices: array(object({
-
uri: resourceUriString(),
-
cid: string(),
-
author: didString(),
-
description: optional(string()),
-
createdAt: datetimeString(),
-
indexedAt: datetimeString(),
-
})),
-
}),
-
},
-
});
-
-
export const listShardsSchema = query('systems.gmstn.development.shard.listShards', {
-
params: object({
-
author: didString(),
-
limit: optional(constrain(integer(), [integerRange(1, 100)]), 50),
-
cursor: optional(string()),
-
}),
-
output: {
-
type: 'lex',
-
schema: object({
-
cursor: optional(string()),
-
shards: array(object({
-
uri: resourceUriString(),
-
cid: string(),
-
author: didString(),
-
description: optional(string()),
-
createdAt: datetimeString(),
-
indexedAt: datetimeString(),
-
})),
-
}),
-
},
-
});
-67
src/api/shard.ts
···
-
import { InvalidRequestError, json } from '@atcute/xrpc-server';
-
import { db } from '../db';
-
import { parseLimit } from '../util/params';
-
import { logger } from '../util/logger';
-
-
export async function listShardsHandler(ctx: {
-
params: any;
-
}) {
-
try {
-
const { limit: rawLimit, cursor, author } = ctx.params;
-
const limit = parseLimit(rawLimit);
-
-
if (!author) {
-
throw new InvalidRequestError({ description: 'Missing required parameter: author' });
-
}
-
-
if (typeof author !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "author" must be a string' });
-
}
-
-
let query = db
-
.selectFrom('shard')
-
.select([
-
'uri',
-
'cid',
-
'creator_did',
-
'description',
-
'created_at',
-
'indexed_at'
-
])
-
.where('creator_did', '=', author)
-
.orderBy('indexed_at', 'desc')
-
.limit(limit);
-
-
if (cursor) {
-
if (typeof cursor !== 'string') {
-
throw new InvalidRequestError({ description: 'Parameter "cursor" must be a string' });
-
}
-
query = query.where('indexed_at', '<', new Date(cursor));
-
}
-
-
const results = await query.execute();
-
-
const shards = results.map(row => {
-
return {
-
uri: row.uri,
-
cid: row.cid,
-
author: row.creator_did,
-
description: row.description,
-
createdAt: row.created_at.toISOString(),
-
indexedAt: row.indexed_at.toISOString(),
-
};
-
}).filter(item => item !== null);
-
-
const lastResult = results.at(-1);
-
const nextCursor = lastResult ? lastResult.indexed_at.toISOString() : undefined;
-
-
return json({
-
shards,
-
cursor: nextCursor,
-
});
-
-
} catch (error) {
-
logger.error({ err: error }, 'Internal server error in listShardsHandler');
-
throw error;
-
}
-
}
+55
src/api/shards.rs
···
+
use std::sync::Arc;
+
+
use axum::{
+
extract::{Query, State},
+
Json,
+
};
+
use chrono::{DateTime, Utc};
+
use serde::{Deserialize, Serialize};
+
+
use crate::api::error::ApiError;
+
use crate::db;
+
use crate::records::{ShardView, SHARD_COLLECTION};
+
use crate::AppState;
+
+
#[derive(Debug, Deserialize)]
+
pub struct ListShardsParams {
+
pub author: Option<String>,
+
pub limit: Option<i64>,
+
pub cursor: Option<String>,
+
}
+
+
#[derive(Debug, Serialize)]
+
pub struct ListShardsResponse {
+
pub shards: Vec<ShardView>,
+
#[serde(skip_serializing_if = "Option::is_none")]
+
pub cursor: Option<String>,
+
}
+
+
pub async fn list_shards(
+
State(state): State<Arc<AppState>>,
+
Query(params): Query<ListShardsParams>,
+
) -> Result<Json<ListShardsResponse>, ApiError> {
+
let author = params.author.ok_or_else(|| {
+
ApiError::InvalidRequest("Missing required parameter: author".to_string())
+
})?;
+
+
let limit = params.limit.unwrap_or(50).min(100).max(1);
+
let cursor: Option<DateTime<Utc>> = params
+
.cursor
+
.as_ref()
+
.and_then(|c| DateTime::parse_from_rfc3339(c).ok())
+
.map(|dt| dt.with_timezone(&Utc));
+
+
let records =
+
db::list_records_by_creator(state.db.pool(), SHARD_COLLECTION, &author, cursor, limit)
+
.await?;
+
+
let next_cursor = records.last().map(|r| r.indexed_at.to_rfc3339());
+
let views: Vec<ShardView> = records.into_iter().map(ShardView::from).collect();
+
+
Ok(Json(ListShardsResponse {
+
shards: views,
+
cursor: next_cursor,
+
}))
+
}
+33
src/config.rs
···
+
use std::env;
+
+
#[derive(Debug, Clone)]
+
pub struct Config {
+
pub database_url: String,
+
pub tap_ws_url: String,
+
pub host: String,
+
pub port: u16,
+
}
+
+
impl Config {
+
pub fn from_env() -> Self {
+
let port = match env::var("PORT") {
+
Ok(p) => match p.parse::<u16>() {
+
Ok(port) => port,
+
Err(_) => {
+
tracing::warn!(value = %p, "Invalid PORT value, using default 3000");
+
3000
+
}
+
},
+
Err(_) => 3000,
+
};
+
+
Self {
+
database_url: env::var("DATABASE_URL")
+
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/prism".to_string()),
+
tap_ws_url: env::var("TAP_WS_URL")
+
.unwrap_or_else(|_| "ws://localhost:2480/channel".to_string()),
+
host: env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
+
port,
+
}
+
}
+
}
-126
src/db.ts
···
-
import { Kysely, PostgresDialect, Generated } from 'kysely'
-
import { Pool } from 'pg'
-
import dotenv from 'dotenv'
-
-
dotenv.config();
-
-
type JsonValue = string | number | boolean | null | { [key: string]: JsonValue } | JsonValue[];
-
-
interface FirehoseEventTable {
-
timestamp: Date
-
event_type: 'commit' | 'identity' | 'account'
-
event_data: JsonValue
-
};
-
-
interface PdsTable {
-
hostname: string;
-
added_at: Generated<Date>;
-
}
-
-
interface AccountTable {
-
did: string;
-
handle: string;
-
pds_hostname: string | null;
-
created_at: Generated<Date>;
-
}
-
-
interface LatticeTable {
-
uri: string;
-
cid: string;
-
creator_did: string;
-
description: string | null;
-
created_at: Generated<Date>;
-
indexed_at: Generated<Date>;
-
data: JsonValue;
-
}
-
-
interface ShardTable {
-
uri: string;
-
cid: string;
-
creator_did: string;
-
description: string | null;
-
created_at: Generated<Date>;
-
indexed_at: Generated<Date>;
-
data: JsonValue;
-
}
-
-
interface ChannelTable {
-
cid: string;
-
uri: string | null;
-
creator_did: string | null;
-
name: string | null;
-
topic: string | null;
-
created_at: Generated<Date>;
-
indexed_at: Generated<Date>;
-
data: JsonValue | null;
-
}
-
-
interface ChannelInviteTable {
-
cid: string;
-
uri: string | null;
-
channel: string | null;
-
creator_did: string | null;
-
recipient_did: string | null;
-
created_at: Generated<Date>;
-
indexed_at: Generated<Date>;
-
data: JsonValue | null;
-
}
-
-
interface ChannelMembershipTable {
-
uri: string;
-
cid: string;
-
channel: string;
-
invite: string;
-
recipient_did: string;
-
state: string;
-
created_at: Generated<Date>;
-
indexed_at: Generated<Date>;
-
updated_at: Date | null;
-
data: JsonValue;
-
}
-
-
type RawRecordQueueStatus = 'pending' | 'processing' | 'complete' | 'failed';
-
-
interface RawRecordQueueTable {
-
id: Generated<number>;
-
record_data: JsonValue;
-
status: Generated<RawRecordQueueStatus>;
-
created_at: Generated<Date>;
-
updated_at: Generated<Date>;
-
}
-
-
interface FirehoseCursorTable {
-
id: number;
-
cursor: string;
-
updated_at: Generated<Date>;
-
}
-
-
-
export interface Database {
-
firehose_event: FirehoseEventTable;
-
firehose_cursor: FirehoseCursorTable;
-
-
pds: PdsTable;
-
account: AccountTable;
-
lattice: LatticeTable;
-
shard: ShardTable;
-
channel: ChannelTable;
-
channel_invite: ChannelInviteTable;
-
channel_membership: ChannelMembershipTable;
-
raw_record_queue: RawRecordQueueTable;
-
};
-
-
const pool = new Pool({
-
connectionString: process.env.DATABASE_URL,
-
max: process.env.DB_POOL_SIZE ? parseInt(process.env.DB_POOL_SIZE, 10) : 10,
-
});
-
-
const dialect = new PostgresDialect({
-
pool,
-
});
-
-
export const db = new Kysely<Database>({
-
dialect,
-
});
-
-
export { pool };
+20
src/db/mod.rs
···
+
mod queries;
+
+
pub use queries::*;
+
+
use sqlx::PgPool;
+
+
#[derive(Clone)]
+
pub struct Database {
+
pool: PgPool,
+
}
+
+
impl Database {
+
pub fn new(pool: PgPool) -> Self {
+
Self { pool }
+
}
+
+
pub fn pool(&self) -> &PgPool {
+
&self.pool
+
}
+
}
+111
src/db/queries.rs
···
+
use chrono::{DateTime, Utc};
+
use sqlx::PgPool;
+
+
use crate::records::{NewAccount, NewRecord, Record};
+
+
pub async fn upsert_account(pool: &PgPool, account: &NewAccount) -> sqlx::Result<()> {
+
sqlx::query!(
+
r#"
+
INSERT INTO account (did, handle, created_at)
+
VALUES ($1, $2, $3)
+
ON CONFLICT (did) DO NOTHING
+
"#,
+
account.did,
+
account.handle,
+
account.created_at
+
)
+
.execute(pool)
+
.await?;
+
Ok(())
+
}
+
+
pub async fn insert_record(pool: &PgPool, record: &NewRecord) -> sqlx::Result<()> {
+
sqlx::query!(
+
r#"
+
INSERT INTO record (uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids)
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+
ON CONFLICT (uri) DO UPDATE SET
+
cid = EXCLUDED.cid,
+
data = EXCLUDED.data,
+
indexed_at = EXCLUDED.indexed_at
+
"#,
+
record.uri,
+
record.cid,
+
record.collection,
+
record.creator_did,
+
record.created_at,
+
record.indexed_at,
+
record.data,
+
record.target_did,
+
&record.ref_cids
+
)
+
.execute(pool)
+
.await?;
+
Ok(())
+
}
+
+
pub async fn list_records_by_creator(
+
pool: &PgPool,
+
collection: &str,
+
creator_did: &str,
+
cursor: Option<DateTime<Utc>>,
+
limit: i64,
+
) -> sqlx::Result<Vec<Record>> {
+
let cursor = cursor.unwrap_or_else(Utc::now);
+
sqlx::query_as!(
+
Record,
+
r#"
+
SELECT uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids
+
FROM record
+
WHERE collection = $1 AND creator_did = $2 AND indexed_at < $3
+
ORDER BY indexed_at DESC
+
LIMIT $4
+
"#,
+
collection,
+
creator_did,
+
cursor,
+
limit
+
)
+
.fetch_all(pool)
+
.await
+
}
+
+
pub async fn list_records_by_target(
+
pool: &PgPool,
+
collection: &str,
+
target_did: &str,
+
cursor: Option<DateTime<Utc>>,
+
limit: i64,
+
) -> sqlx::Result<Vec<Record>> {
+
let cursor = cursor.unwrap_or_else(Utc::now);
+
sqlx::query_as!(
+
Record,
+
r#"
+
SELECT uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids
+
FROM record
+
WHERE collection = $1 AND target_did = $2 AND indexed_at < $3
+
ORDER BY indexed_at DESC
+
LIMIT $4
+
"#,
+
collection,
+
target_did,
+
cursor,
+
limit
+
)
+
.fetch_all(pool)
+
.await
+
}
+
+
pub async fn get_record_by_cid(pool: &PgPool, cid: &str) -> sqlx::Result<Option<Record>> {
+
sqlx::query_as!(
+
Record,
+
r#"
+
SELECT uri, cid, collection, creator_did, created_at, indexed_at, data, target_did, ref_cids
+
FROM record
+
WHERE cid = $1
+
"#,
+
cid
+
)
+
.fetch_optional(pool)
+
.await
+
}
-72
src/index.ts
···
-
import { logger } from './util/logger';
-
-
logger.info('Main app starting...');
-
-
const spawnOptions: any = {
-
stdout: 'inherit',
-
stderr: 'inherit',
-
};
-
-
const dir = __dirname;
-
-
type Subprocess = ReturnType<typeof Bun.spawn>;
-
-
let pdsListSyncProcess: Subprocess | undefined;
-
let pdsBackfillProcess: Subprocess | undefined;
-
let firehoseProcess: Subprocess | undefined;
-
let serverProcess: Subprocess | undefined;
-
-
async function main() {
-
// Run PDS List Sync first and wait for it to complete
-
const pdsListSyncPath = `${dir}/pds/discovery.ts`;
-
pdsListSyncProcess = Bun.spawn(['bun', pdsListSyncPath], spawnOptions);
-
logger.info({ pid: pdsListSyncProcess.pid }, 'pdsListSync process started');
-
-
await pdsListSyncProcess.exited;
-
logger.info('pdsListSync process completed');
-
pdsListSyncProcess = undefined;
-
-
const pdsBackfillPath = `${dir}/pds/backfill.ts`;
-
pdsBackfillProcess = Bun.spawn(['bun', pdsBackfillPath], spawnOptions);
-
logger.info({ pid: pdsBackfillProcess.pid }, 'pdsBackfill process started');
-
-
const firehosePath = `${dir}/services/firehose-listen.ts`;
-
firehoseProcess = Bun.spawn(['bun', firehosePath], spawnOptions);
-
logger.info({ pid: firehoseProcess.pid }, 'Firehose process started');
-
-
firehoseProcess.exited.then((code) => {
-
logger.error({ code }, 'Firehose process exited');
-
});
-
-
const serverPath = `${dir}/services/api.ts`;
-
serverProcess = Bun.spawn(['bun', serverPath], spawnOptions);
-
logger.info({ pid: serverProcess.pid }, 'XRPC Server process started');
-
-
serverProcess.exited.then((code) => {
-
logger.error({ code }, 'XRPC Server process exited');
-
});
-
}
-
-
const cleanup = () => {
-
logger.info('Stopping all subprocesses...');
-
-
if (pdsListSyncProcess && !pdsListSyncProcess.killed) {
-
pdsListSyncProcess.kill();
-
}
-
if (pdsBackfillProcess && !pdsBackfillProcess.killed) {
-
pdsBackfillProcess.kill();
-
}
-
if (firehoseProcess && !firehoseProcess.killed) {
-
firehoseProcess.kill();
-
}
-
if (serverProcess && !serverProcess.killed) {
-
serverProcess.kill();
-
}
-
-
process.exit(0);
-
};
-
-
process.on('SIGINT', cleanup);
-
process.on('SIGTERM', cleanup);
-
-
main();
-30
src/lexicons.ts
···
-
import fs from 'fs';
-
import path from 'path';
-
-
export const listInvitesLexicon = JSON.parse(
-
fs.readFileSync(path.join(__dirname, '../lexicons/systems/gmstn/development/channel/listInvites.json'), 'utf8')
-
);
-
-
export const listChannelsLexicon = JSON.parse(
-
fs.readFileSync(path.join(__dirname, '../lexicons/systems/gmstn/development/channel/listChannels.json'), 'utf8')
-
);
-
-
export const listMembershipsLexicon = JSON.parse(
-
fs.readFileSync(path.join(__dirname, '../lexicons/systems/gmstn/development/channel/listMemberships.json'), 'utf8')
-
);
-
-
export const listLatticesLexicon = JSON.parse(
-
fs.readFileSync(path.join(__dirname, '../lexicons/systems/gmstn/development/lattice/listLattices.json'), 'utf8')
-
);
-
-
export const listShardsLexicon = JSON.parse(
-
fs.readFileSync(path.join(__dirname, '../lexicons/systems/gmstn/development/shard/listShards.json'), 'utf8')
-
);
-
-
export const allLexicons = [
-
listInvitesLexicon,
-
listChannelsLexicon,
-
listMembershipsLexicon,
-
listLatticesLexicon,
-
listShardsLexicon,
-
];
+14
src/lib.rs
···
+
pub mod api;
+
pub mod config;
+
pub mod db;
+
pub mod records;
+
pub mod tap;
+
pub mod ws;
+
+
use db::Database;
+
use ws::Broadcaster;
+
+
pub struct AppState {
+
pub db: Database,
+
pub broadcaster: Broadcaster,
+
}
+124
src/main.rs
···
+
use std::sync::Arc;
+
+
use axum::Router;
+
use futures::FutureExt;
+
use sqlx::postgres::PgPoolOptions;
+
use tokio::sync::broadcast;
+
use tower_http::compression::CompressionLayer;
+
use tower_http::cors::{Any, CorsLayer};
+
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
+
+
use prism::config::Config;
+
use prism::db::Database;
+
use prism::ws::Broadcaster;
+
use prism::AppState;
+
+
#[tokio::main]
+
async fn main() -> anyhow::Result<()> {
+
tracing_subscriber::registry()
+
.with(
+
tracing_subscriber::EnvFilter::try_from_default_env()
+
.unwrap_or_else(|_| "prism=debug,tower_http=debug".into()),
+
)
+
.with(tracing_subscriber::fmt::layer())
+
.init();
+
+
let config = Config::from_env();
+
tracing::info!("Starting Prism with config: {:?}", config);
+
+
let pool = PgPoolOptions::new()
+
.max_connections(10)
+
.connect(&config.database_url)
+
.await?;
+
+
tracing::info!("Connected to database");
+
+
sqlx::migrate!("./migrations").run(&pool).await?;
+
tracing::info!("Migrations complete");
+
+
let (tx, _rx) = broadcast::channel::<String>(1024);
+
let broadcaster = Broadcaster::new(tx.clone());
+
+
let state = Arc::new(AppState {
+
db: Database::new(pool.clone()),
+
broadcaster,
+
});
+
+
let tap_state = state.clone();
+
let tap_url = config.tap_ws_url.clone();
+
let tap_tx = tx.clone();
+
tokio::spawn(async move {
+
loop {
+
let result = std::panic::AssertUnwindSafe(prism::tap::consumer::run(
+
tap_url.clone(),
+
tap_state.clone(),
+
tap_tx.clone(),
+
));
+
+
if let Err(e) = result.catch_unwind().await {
+
let panic_msg = if let Some(s) = e.downcast_ref::<&str>() {
+
s.to_string()
+
} else if let Some(s) = e.downcast_ref::<String>() {
+
s.clone()
+
} else {
+
"Unknown panic".to_string()
+
};
+
tracing::error!(panic = %panic_msg, "TAP consumer panicked, restarting in 5 seconds...");
+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
+
} else {
+
// run() returned normally (shouldn't happen as it loops forever)
+
tracing::warn!("TAP consumer exited unexpectedly, restarting...");
+
}
+
}
+
});
+
+
let app = Router::new()
+
.merge(prism::api::router())
+
.merge(prism::ws::router())
+
.layer(CompressionLayer::new())
+
.layer(
+
CorsLayer::new()
+
.allow_origin(Any)
+
.allow_methods(Any)
+
.allow_headers(Any),
+
)
+
.with_state(state);
+
+
let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.host, config.port)).await?;
+
tracing::info!("Listening on {}:{}", config.host, config.port);
+
+
axum::serve(listener, app)
+
.with_graceful_shutdown(shutdown_signal())
+
.await?;
+
+
tracing::info!("Shutdown complete");
+
Ok(())
+
}
+
+
async fn shutdown_signal() {
+
let ctrl_c = async {
+
tokio::signal::ctrl_c()
+
.await
+
.expect("Failed to install Ctrl+C handler");
+
};
+
+
#[cfg(unix)]
+
let terminate = async {
+
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
+
.expect("Failed to install SIGTERM handler")
+
.recv()
+
.await;
+
};
+
+
#[cfg(not(unix))]
+
let terminate = std::future::pending::<()>();
+
+
tokio::select! {
+
_ = ctrl_c => {
+
tracing::info!("Received Ctrl+C, shutting down gracefully...");
+
},
+
_ = terminate => {
+
tracing::info!("Received SIGTERM, shutting down gracefully...");
+
},
+
}
+
}
-223
src/pds/backfill.ts
···
-
import { sql } from 'kysely';
-
import { fromUint8Array } from '@atcute/repo';
-
import { db } from 'db';
-
import { RecordProcessor, AtpRecord } from 'util/recordProcessor';
-
import { logger } from '../util/logger';
-
-
interface NewAccount {
-
did: string;
-
handle: string;
-
pds_hostname: string;
-
created_at: any;
-
}
-
-
interface RequestData {
-
cursor?: string;
-
repos: {
-
did: string;
-
}[];
-
}
-
-
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
-
async function processSingleRepo(pdsHostname: string, did: string): Promise<void> {
-
const pdsBaseUrl = `https://${pdsHostname}`;
-
const getRepoUrl = new URL(`/xrpc/com.atproto.sync.getRepo`, pdsBaseUrl);
-
getRepoUrl.searchParams.set('did', did);
-
-
let carBytes: Uint8Array;
-
try {
-
const response = await fetch(getRepoUrl.href);
-
if (!response.ok) {
-
throw new Error(`Failed to getRepo for ${did}: ${response.status} ${response.statusText}`);
-
}
-
carBytes = new Uint8Array(await response.arrayBuffer());
-
} catch (e: any) {
-
return;
-
}
-
-
const processor = new RecordProcessor(did);
-
-
try {
-
for (const entry of fromUint8Array(carBytes)) {
-
const { collection, rkey, record: rawRecord } = entry;
-
const record = rawRecord as AtpRecord;
-
-
if (
-
collection &&
-
collection.startsWith('systems.gmstn.development.')
-
) {
-
const uri = `at://${did}/${collection}/${rkey}`;
-
const cid = entry.cid.$link;
-
-
await processor.processRecord(record, uri, cid, record.createdAt, false);
-
}
-
}
-
} catch (e: any) {
-
logger.debug({ err: e, did }, "Error processing repo. Skipping rest of repo.");
-
return;
-
}
-
-
await processor.insertAllProcessedRecords(false);
-
}
-
-
async function backfillPds(pdsHostname: string) {
-
logger.info({ pdsHostname }, "Starting backfill");
-
const pdsBaseUrl = `https://` + pdsHostname;
-
let cursor: string | undefined;
-
let totalReposProcessed = 0;
-
-
try {
-
do {
-
const listReposUrl = new URL('/xrpc/com.atproto.sync.listRepos', pdsBaseUrl);
-
if (cursor) {
-
listReposUrl.searchParams.set('cursor', cursor);
-
}
-
-
const response = await fetch(listReposUrl.href);
-
if (!response.ok) {
-
throw new Error(`Failed to listRepos: ${response.status} ${response.statusText}`);
-
}
-
-
const data = await response.json() as RequestData;
-
cursor = data.cursor;
-
const dids: string[] = data.repos.map((r: { did: string }) => r.did);
-
-
if (dids.length === 0) {
-
break;
-
}
-
-
logger.debug({ count: dids.length, cursor }, "Fetched repos");
-
-
const newAccounts: NewAccount[] = dids.map(repo => ({
-
did: repo,
-
handle: repo,
-
pds_hostname: pdsHostname,
-
created_at: sql`now()`,
-
}));
-
-
if (newAccounts.length > 0) {
-
try {
-
await db.insertInto('account')
-
.values(newAccounts)
-
.onConflict((oc) => oc
-
.column('did')
-
.doUpdateSet({
-
pds_hostname: sql`excluded.pds_hostname`,
-
})
-
)
-
.execute();
-
} catch (e: any) {
-
logger.error({ err: e }, "Failed to bulk upsert accounts");
-
}
-
}
-
-
const BATCH_SIZE = 10;
-
for (let i = 0; i < dids.length; i += BATCH_SIZE) {
-
const batch = dids.slice(i, i + BATCH_SIZE);
-
const tasks = batch.map(did => processSingleRepo(pdsHostname, did));
-
await Promise.allSettled(tasks);
-
}
-
-
totalReposProcessed += dids.length;
-
-
} while (cursor);
-
-
logger.info({ pdsHostname, totalReposProcessed }, "Finished paginating repos. Backfill complete for PDS.");
-
-
} catch (error) {
-
logger.error({ err: error, pdsHostname }, "Fatal error during backfill");
-
throw error;
-
}
-
}
-
-
async function main() {
-
const backfilledThisSession = new Set<string>();
-
let shouldExit = false;
-
-
const shutdown = (signal: string) => {
-
logger.info(`Received ${signal}, stopping backfill after current batch...`);
-
shouldExit = true;
-
};
-
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
-
process.on('SIGINT', () => shutdown('SIGINT'));
-
-
const PDS_CONCURRENCY = 32;
-
-
logger.info("Starting Backfill Daemon...");
-
-
while (!shouldExit) {
-
let pdsesToBackfill: { hostname: string }[] = [];
-
-
try {
-
pdsesToBackfill = await db
-
.selectFrom('pds')
-
.select('hostname')
-
// .where('hostname', 'in', ['pds.witchcraft.systems', 'pds.tgirl.cloud', 'pds.gmstn.systems'])
-
.orderBy(
-
(eb) => eb
-
.case()
-
.when('hostname', 'like', '%brid.gy%')
-
.then(1)
-
.else(0)
-
.end(),
-
'asc'
-
)
-
.orderBy('added_at', 'asc')
-
.execute();
-
-
pdsesToBackfill = pdsesToBackfill.filter(p => !backfilledThisSession.has(p.hostname));
-
-
if (pdsesToBackfill.length === 0) {
-
logger.info('No new PDSs found to backfill. Sleeping for 10s...');
-
-
for (let i = 0; i < 100; i++) {
-
if (shouldExit) break;
-
await delay(100);
-
}
-
continue;
-
}
-
-
logger.info({ count: pdsesToBackfill.length }, "Found new PDS(s) to backfill. Starting batches...");
-
-
for (let i = 0; i < pdsesToBackfill.length; i += PDS_CONCURRENCY) {
-
if (shouldExit) break;
-
-
const batch = pdsesToBackfill.slice(i, i + PDS_CONCURRENCY);
-
-
logger.info({
-
batchIndex: Math.floor(i / PDS_CONCURRENCY) + 1,
-
totalBatches: Math.ceil(pdsesToBackfill.length / PDS_CONCURRENCY),
-
batchSize: batch.length
-
}, "Processing PDS batch");
-
-
const tasks = batch.map(async (pds) => {
-
if (backfilledThisSession.has(pds.hostname)) return;
-
-
try {
-
await backfillPds(pds.hostname);
-
backfilledThisSession.add(pds.hostname);
-
} catch (e) {
-
logger.error({ err: e, pdsHostname: pds.hostname }, "Job for PDS failed. Moving to next PDS.");
-
}
-
});
-
-
await Promise.allSettled(tasks);
-
}
-
} catch (e: any) {
-
logger.error({ err: e }, "Fatal error during backfill daemon loop");
-
await delay(5000);
-
}
-
}
-
-
logger.info('Backfill daemon exited gracefully.');
-
process.exit(0);
-
}
-
-
if (require.main === module) {
-
main().catch((err) => {
-
console.error(err);
-
process.exit(1);
-
});
-
}
-280
src/pds/discovery.ts
···
-
import { Readable } from 'stream';
-
import readline from 'readline';
-
import { db } from 'db';
-
import { logger } from '../util/logger';
-
import { sql } from 'kysely';
-
-
const GITHUB_PDS_LIST_URL = 'https://raw.githubusercontent.com/mary-ext/atproto-scraping/refs/heads/trunk/state.json';
-
const PLC_EXPORT_URL = 'https://plc.directory/export';
-
-
interface GithubStateJson {
-
pdses: {
-
[url: string]: {
-
inviteCodeRequired?: boolean;
-
version?: string;
-
}
-
}
-
}
-
-
interface PlcEntry {
-
did: string;
-
operation: {
-
type: string;
-
alsoKnownAs?: string[];
-
handle?: string;
-
services?: {
-
[key: string]: {
-
type: string;
-
endpoint: string;
-
};
-
};
-
};
-
createdAt: string;
-
}
-
-
async function syncFromGithub(): Promise<boolean> {
-
logger.info('PRIMARY: Starting PDS list sync from GitHub (Mary-ext)...');
-
-
try {
-
const response = await fetch(GITHUB_PDS_LIST_URL);
-
if (!response.ok) {
-
throw new Error(`Failed to fetch PDS list: ${response.statusText}`);
-
}
-
-
const data = (await response.json()) as GithubStateJson;
-
const pdsUrls = Object.keys(data?.pdses || {});
-
-
if (pdsUrls.length === 0) {
-
logger.warn('No PDS hosts found in the upstream GitHub list.');
-
return false;
-
}
-
-
const pdsToInsert = pdsUrls
-
.map(url => {
-
try {
-
return { hostname: new URL(url).hostname };
-
} catch (e) {
-
return null;
-
}
-
})
-
.filter(Boolean) as { hostname: string }[];
-
-
if (pdsToInsert.length > 0) {
-
const CHUNK_SIZE = 1000;
-
let insertedCount = 0;
-
-
for (let i = 0; i < pdsToInsert.length; i += CHUNK_SIZE) {
-
const batch = pdsToInsert.slice(i, i + CHUNK_SIZE);
-
await db
-
.insertInto('pds')
-
.values(batch)
-
.onConflict((oc) => oc.column('hostname').doNothing())
-
.execute();
-
insertedCount += batch.length;
-
}
-
logger.info({ count: insertedCount }, "GitHub PDS sync complete.");
-
return true;
-
} else {
-
logger.info('No valid PDS hostnames parsed from GitHub.');
-
return false;
-
}
-
-
} catch (error) {
-
logger.error({ err: error }, "Error during GitHub PDS list sync");
-
return false;
-
}
-
}
-
-
async function syncFromPlc() {
-
logger.info('BACKUP: Starting PDS/Account sync from PLC Directory...');
-
-
let after: string | undefined;
-
try {
-
const result = await db
-
.selectFrom('account')
-
.select(sql<string>`max(created_at)`.as('max_created_at'))
-
.executeTakeFirst();
-
-
if (result?.max_created_at) {
-
after = new Date(result.max_created_at).toISOString();
-
logger.info({ after }, 'Resuming PLC sync from latest account created_at');
-
}
-
} catch (err) {
-
logger.warn({ err }, 'Failed to fetch latest cursor from DB, starting PLC sync from beginning');
-
}
-
-
const limit = 100000;
-
-
try {
-
while (true) {
-
const url = `${PLC_EXPORT_URL}?count=${limit}${after ? `&after=${after}` : ''}`;
-
logger.info({ url }, 'Fetching PLC export batch');
-
-
const response = await fetch(url);
-
-
if (response.status === 429) {
-
const delay = 90_000;
-
logger.warn(`Rate limited, waiting ${delay}ms...`);
-
await new Promise(resolve => setTimeout(resolve, delay));
-
continue;
-
}
-
-
if (!response.ok) {
-
throw new Error(`Failed to fetch PLC export: ${response.statusText}`);
-
}
-
-
if (!response.body) {
-
break;
-
}
-
-
const nodeStream = Readable.fromWeb(response.body as import('stream/web').ReadableStream);
-
const rl = readline.createInterface({
-
input: nodeStream,
-
crlfDelay: Infinity,
-
});
-
-
let lineCount = 0;
-
let lastCreatedAt: string | undefined;
-
-
const pdsToInsert = new Set<string>();
-
const accountsToUpsert = new Map<string, { did: string; handle: string; pds_hostname: string | null; created_at: string }>();
-
-
for await (const line of rl) {
-
lineCount++;
-
if (!line.trim()) continue;
-
-
try {
-
const entry = JSON.parse(line) as PlcEntry;
-
lastCreatedAt = entry.createdAt;
-
-
if (entry.operation.type === 'plc_operation' || entry.operation.type === 'create') {
-
let pdsHostname: string | null = null;
-
-
const services = entry.operation.services;
-
if (services) {
-
const pdsService = services['atproto_pds'];
-
if (pdsService && pdsService.type === 'AtprotoPersonalDataServer' && pdsService.endpoint) {
-
try {
-
pdsHostname = new URL(pdsService.endpoint).hostname;
-
pdsToInsert.add(pdsHostname);
-
} catch {
-
}
-
}
-
}
-
-
let handle: string | undefined = entry.operation.handle;
-
if (!handle && entry.operation.alsoKnownAs) {
-
const atHandle = entry.operation.alsoKnownAs.find(aka => aka.startsWith('at://'));
-
if (atHandle) {
-
handle = atHandle.replace('at://', '');
-
}
-
}
-
-
if (handle) {
-
accountsToUpsert.set(entry.did, {
-
did: entry.did,
-
handle: handle,
-
pds_hostname: pdsHostname,
-
created_at: entry.createdAt
-
});
-
}
-
}
-
} catch {
-
continue;
-
}
-
}
-
-
if (pdsToInsert.size > 0) {
-
const pdsValues = Array.from(pdsToInsert).map(hostname => ({ hostname }));
-
await db
-
.insertInto('pds')
-
.values(pdsValues)
-
.onConflict((oc) => oc.column('hostname').doNothing())
-
.execute();
-
}
-
-
if (accountsToUpsert.size > 0) {
-
const accountValues = Array.from(accountsToUpsert.values()).map(acc => ({
-
...acc,
-
created_at: new Date(acc.created_at)
-
}));
-
-
const ACCOUNT_BATCH_SIZE = 500;
-
for (let i = 0; i < accountValues.length; i += ACCOUNT_BATCH_SIZE) {
-
const batch = accountValues.slice(i, i + ACCOUNT_BATCH_SIZE);
-
try {
-
await db
-
.insertInto('account')
-
.values(batch)
-
.onConflict((oc) => oc
-
.column('did')
-
.doUpdateSet((eb) => ({
-
handle: eb.ref('excluded.handle'),
-
pds_hostname: eb.ref('excluded.pds_hostname'),
-
created_at: eb.ref('excluded.created_at')
-
}))
-
)
-
.execute();
-
} catch (err) {
-
for (const acc of batch) {
-
try {
-
await db
-
.insertInto('account')
-
.values(acc)
-
.onConflict((oc) => oc
-
.column('did')
-
.doUpdateSet((eb) => ({
-
handle: eb.ref('excluded.handle'),
-
pds_hostname: eb.ref('excluded.pds_hostname'),
-
created_at: eb.ref('excluded.created_at')
-
}))
-
)
-
.execute();
-
} catch (innerErr) { }
-
}
-
}
-
}
-
}
-
-
if (lineCount === 0) {
-
logger.info('Reached end of stream (0 lines received)');
-
break;
-
}
-
-
if (lastCreatedAt) {
-
if (after === lastCreatedAt) {
-
logger.warn({ after }, 'Cursor did not advance despite receiving data. Stopping.');
-
break;
-
}
-
after = lastCreatedAt;
-
}
-
}
-
logger.info("PLC Directory sync complete");
-
-
} catch (error) {
-
logger.error({ err: error }, "Error during PLC sync");
-
throw error;
-
}
-
}
-
-
async function main() {
-
if (!(await syncFromGithub())) {
-
await syncFromPlc();
-
} else {
-
logger.info("Primary GitHub sync succeeded; skipping PLC directory backup.");
-
}
-
}
-
-
if (require.main === module) {
-
main()
-
.then(() => {
-
logger.info('Discovery finished successfully.');
-
db.destroy();
-
process.exit(0);
-
})
-
.catch((err) => {
-
logger.error({ err }, 'Unhandled error in discovery');
-
db.destroy();
-
process.exit(1);
-
});
-
}
+31
src/records/channel.rs
···
+
use serde::Serialize;
+
+
use super::Record;
+
+
#[derive(Debug, Serialize)]
+
pub struct ChannelView {
+
pub uri: String,
+
pub cid: String,
+
pub author: String,
+
#[serde(rename = "displayName")]
+
pub display_name: String,
+
pub description: Option<String>,
+
#[serde(rename = "createdAt")]
+
pub created_at: String,
+
#[serde(rename = "indexedAt")]
+
pub indexed_at: String,
+
}
+
+
impl From<Record> for ChannelView {
+
fn from(r: Record) -> Self {
+
Self {
+
uri: r.uri,
+
cid: r.cid,
+
author: r.creator_did,
+
display_name: r.data.get("name").and_then(|v| v.as_str()).unwrap_or_default().to_string(),
+
description: r.data.get("topic").and_then(|v| v.as_str()).map(String::from),
+
created_at: r.created_at.to_rfc3339(),
+
indexed_at: r.indexed_at.to_rfc3339(),
+
}
+
}
+
}
+31
src/records/invite.rs
···
+
use serde::Serialize;
+
+
use super::Record;
+
+
#[derive(Debug, Serialize)]
+
pub struct InviteView {
+
pub uri: String,
+
pub cid: String,
+
pub author: String,
+
pub channel: String,
+
pub recipient: String,
+
#[serde(rename = "createdAt")]
+
pub created_at: String,
+
#[serde(rename = "indexedAt")]
+
pub indexed_at: String,
+
}
+
+
impl From<Record> for InviteView {
+
fn from(r: Record) -> Self {
+
let channel_cid = r.ref_cids.first().cloned().unwrap_or_default();
+
Self {
+
uri: r.uri,
+
cid: r.cid,
+
author: r.creator_did,
+
channel: channel_cid,
+
recipient: r.target_did.unwrap_or_default(),
+
created_at: r.created_at.to_rfc3339(),
+
indexed_at: r.indexed_at.to_rfc3339(),
+
}
+
}
+
}
+28
src/records/lattice.rs
···
+
use serde::Serialize;
+
+
use super::Record;
+
+
#[derive(Debug, Serialize)]
+
pub struct LatticeView {
+
pub uri: String,
+
pub cid: String,
+
pub author: String,
+
pub description: Option<String>,
+
#[serde(rename = "createdAt")]
+
pub created_at: String,
+
#[serde(rename = "indexedAt")]
+
pub indexed_at: String,
+
}
+
+
impl From<Record> for LatticeView {
+
fn from(r: Record) -> Self {
+
Self {
+
uri: r.uri,
+
cid: r.cid,
+
author: r.creator_did,
+
description: r.data.get("description").and_then(|v| v.as_str()).map(String::from),
+
created_at: r.created_at.to_rfc3339(),
+
indexed_at: r.indexed_at.to_rfc3339(),
+
}
+
}
+
}
+40
src/records/membership.rs
···
+
use serde::Serialize;
+
+
use super::Record;
+
+
#[derive(Debug, Serialize)]
+
pub struct MembershipView {
+
pub uri: String,
+
pub cid: String,
+
pub channel: String,
+
pub invite: String,
+
pub recipient: String,
+
pub state: String,
+
#[serde(rename = "createdAt")]
+
pub created_at: String,
+
#[serde(rename = "indexedAt")]
+
pub indexed_at: String,
+
#[serde(rename = "updatedAt", skip_serializing_if = "Option::is_none")]
+
pub updated_at: Option<String>,
+
}
+
+
impl From<Record> for MembershipView {
+
fn from(r: Record) -> Self {
+
let channel_cid = r.ref_cids.first().cloned().unwrap_or_default();
+
let invite_cid = r.ref_cids.get(1).cloned().unwrap_or_default();
+
let state = r.data.get("state").and_then(|v| v.as_str()).unwrap_or("unknown").to_string();
+
let updated_at = r.data.get("updatedAt").and_then(|v| v.as_str()).map(String::from);
+
+
Self {
+
uri: r.uri,
+
cid: r.cid,
+
channel: channel_cid,
+
invite: invite_cid,
+
recipient: r.target_did.unwrap_or_default(),
+
state,
+
created_at: r.created_at.to_rfc3339(),
+
indexed_at: r.indexed_at.to_rfc3339(),
+
updated_at,
+
}
+
}
+
}
+68
src/records/mod.rs
···
+
mod channel;
+
mod invite;
+
mod lattice;
+
mod membership;
+
mod shard;
+
+
pub use channel::*;
+
pub use invite::*;
+
pub use lattice::*;
+
pub use membership::*;
+
pub use shard::*;
+
+
use chrono::{DateTime, Utc};
+
use serde::{Deserialize, Serialize};
+
+
pub const LATTICE_COLLECTION: &str = "systems.gmstn.development.lattice";
+
pub const SHARD_COLLECTION: &str = "systems.gmstn.development.shard";
+
pub const CHANNEL_COLLECTION: &str = "systems.gmstn.development.channel";
+
pub const INVITE_COLLECTION: &str = "systems.gmstn.development.channel.invite";
+
pub const MEMBERSHIP_COLLECTION: &str = "systems.gmstn.development.channel.membership";
+
+
#[derive(Debug, Clone, Serialize, Deserialize)]
+
pub struct AtpRecord {
+
#[serde(rename = "$type")]
+
pub record_type: String,
+
#[serde(rename = "createdAt")]
+
pub created_at: String,
+
#[serde(flatten)]
+
pub extra: serde_json::Value,
+
}
+
+
#[derive(Debug, Clone, Serialize, Deserialize)]
+
pub struct CidRef {
+
pub cid: String,
+
}
+
+
#[derive(Debug, Clone)]
+
pub struct NewAccount {
+
pub did: String,
+
pub handle: String,
+
pub created_at: DateTime<Utc>,
+
}
+
+
#[derive(Debug, Clone, sqlx::FromRow)]
+
pub struct Record {
+
pub uri: String,
+
pub cid: String,
+
pub collection: String,
+
pub creator_did: String,
+
pub created_at: DateTime<Utc>,
+
pub indexed_at: DateTime<Utc>,
+
pub data: serde_json::Value,
+
pub target_did: Option<String>,
+
pub ref_cids: Vec<String>,
+
}
+
+
#[derive(Debug, Clone)]
+
pub struct NewRecord {
+
pub uri: String,
+
pub cid: String,
+
pub collection: String,
+
pub creator_did: String,
+
pub created_at: DateTime<Utc>,
+
pub indexed_at: DateTime<Utc>,
+
pub data: serde_json::Value,
+
pub target_did: Option<String>,
+
pub ref_cids: Vec<String>,
+
}
+28
src/records/shard.rs
···
+
use serde::Serialize;
+
+
use super::Record;
+
+
#[derive(Debug, Serialize)]
+
pub struct ShardView {
+
pub uri: String,
+
pub cid: String,
+
pub author: String,
+
pub description: Option<String>,
+
#[serde(rename = "createdAt")]
+
pub created_at: String,
+
#[serde(rename = "indexedAt")]
+
pub indexed_at: String,
+
}
+
+
impl From<Record> for ShardView {
+
fn from(r: Record) -> Self {
+
Self {
+
uri: r.uri,
+
cid: r.cid,
+
author: r.creator_did,
+
description: r.data.get("description").and_then(|v| v.as_str()).map(String::from),
+
created_at: r.created_at.to_rfc3339(),
+
indexed_at: r.indexed_at.to_rfc3339(),
+
}
+
}
+
}
-60
src/scripts/migrate.ts
···
-
import * as path from 'path'
-
import { promises as fs } from 'fs'
-
import { Migrator, FileMigrationProvider } from 'kysely'
-
import { db } from '../db'
-
-
async function migrate(direction: 'up' | 'down' | 'latest') {
-
const migrator = new Migrator({
-
db,
-
provider: new FileMigrationProvider({
-
fs,
-
path,
-
migrationFolder: path.join(__dirname, '../../migrations'),
-
}),
-
})
-
-
console.log(`Running migration: ${direction}`);
-
-
if (direction === 'down') {
-
const { error, results } = await migrator.migrateDown()
-
results?.forEach((it) => {
-
if (it.status === 'Success') {
-
console.log(`migration "${it.migrationName}" was reverted successfully`)
-
} else if (it.status === 'Error') {
-
console.error(`failed to revert migration "${it.migrationName}"`)
-
}
-
})
-
if (error) {
-
console.error('failed to migrate')
-
console.error(error)
-
process.exit(1)
-
}
-
}
-
-
if (direction === 'up' || direction === 'latest') {
-
const { error, results } = await migrator.migrateToLatest()
-
results?.forEach((it) => {
-
if (it.status === 'Success') {
-
console.log(`migration "${it.migrationName}" was executed successfully`)
-
} else if (it.status === 'Error') {
-
console.error(`failed to execute migration "${it.migrationName}"`)
-
}
-
})
-
if (error) {
-
console.error('failed to migrate')
-
console.error(error)
-
process.exit(1)
-
}
-
}
-
-
await db.destroy()
-
}
-
-
const direction = process.argv[2] as 'up' | 'down' | 'latest' | undefined;
-
-
if (!direction || !['up', 'down', 'latest'].includes(direction)) {
-
console.error('Please provide a migration direction: up, down, or latest');
-
process.exit(1);
-
}
-
-
migrate(direction);
-56
src/scripts/start-backfill.ts
···
-
import { logger } from '../util/logger';
-
-
const spawnOptions: { stdout: 'inherit' | 'pipe' | 'ignore', stderr: 'inherit' | 'pipe' | 'ignore' } = {
-
stdout: 'inherit',
-
stderr: 'inherit',
-
};
-
-
const dir = import.meta.dir + '/..';
-
-
type Subprocess = ReturnType<typeof Bun.spawn>;
-
-
let discoveryProcess: Subprocess | undefined;
-
let backfillProcess: Subprocess | undefined;
-
-
async function main() {
-
logger.info('Starting parallel backfill operations...');
-
-
const backfillPath = `${dir}/pds/backfill.ts`;
-
backfillProcess = Bun.spawn(['bun', backfillPath], spawnOptions);
-
logger.info({ pid: backfillProcess.pid }, 'Backfill (Worker) process started');
-
-
const discoveryPath = `${dir}/pds/discovery.ts`;
-
discoveryProcess = Bun.spawn(['bun', discoveryPath], spawnOptions);
-
logger.info({ pid: discoveryProcess.pid }, 'Discovery (PLC Sync) process started');
-
-
if (discoveryProcess) {
-
await discoveryProcess.exited;
-
logger.info('Discovery process finished syncing.');
-
logger.info('Backfill worker is still running to process the queue. Press Ctrl+C to stop.');
-
}
-
-
if (backfillProcess) {
-
await backfillProcess.exited;
-
logger.warn('Backfill worker exited unexpectedly.');
-
}
-
}
-
-
const cleanup = () => {
-
logger.info('\nStopping backfill subprocesses...');
-
-
if (discoveryProcess && !discoveryProcess.killed) {
-
discoveryProcess.kill();
-
logger.info('Killed discovery process');
-
}
-
if (backfillProcess && !backfillProcess.killed) {
-
backfillProcess.kill('SIGTERM');
-
logger.info('Signaled backfill process to stop (waiting for graceful exit)...');
-
}
-
-
setTimeout(() => process.exit(0), 1000);
-
};
-
-
process.on('SIGINT', cleanup);
-
process.on('SIGTERM', cleanup);
-
-
main();
-81
src/services/api.ts
···
-
import { db } from 'db';
-
import { logger } from '../util/logger';
-
import { createServer } from '../api';
-
import { websocketHandler as customWsHandler, closeAllClients } from './websocket';
-
import { setupQueueListener, processQueue, setShuttingDown } from './queue';
-
-
type WebSocketData = { type: 'custom' } | { controller: any; handler: any };
-
-
const bunConfig = createServer();
-
-
const server = Bun.serve<WebSocketData>({
-
port: 3000,
-
fetch(req, server) {
-
const url = new URL(req.url);
-
if (!url.pathname.startsWith('/xrpc/')) {
-
if (server.upgrade(req, { data: { type: 'custom' } })) {
-
return undefined;
-
}
-
}
-
-
return bunConfig.fetch(req, server as any);
-
},
-
websocket: {
-
open(ws) {
-
if (ws.data && (ws.data as any).type === 'custom') {
-
customWsHandler.open(ws as any);
-
} else {
-
bunConfig.websocket.open?.(ws as any);
-
}
-
},
-
close(ws, code, reason) {
-
if (ws.data && (ws.data as any).type === 'custom') {
-
customWsHandler.close(ws as any);
-
} else {
-
bunConfig.websocket.close?.(ws as any, code, reason);
-
}
-
},
-
message(ws, message) {
-
if (ws.data && (ws.data as any).type === 'custom') {
-
customWsHandler.message(ws as any, message);
-
} else {
-
bunConfig.websocket.message?.(ws as any, message);
-
}
-
},
-
drain(ws) {
-
if (bunConfig.websocket.drain) bunConfig.websocket.drain(ws as any);
-
}
-
}
-
});
-
-
logger.info('XRPC server running on http://localhost:3000');
-
logger.info('WebSocket server initialized and listening.');
-
-
setupQueueListener();
-
processQueue();
-
-
const queuePoller = setInterval(processQueue, 30000);
-
-
const shutdown = async () => {
-
logger.info('Shutting down server...');
-
-
setShuttingDown(true);
-
clearInterval(queuePoller);
-
-
server.stop();
-
logger.info('HTTP server closed');
-
-
closeAllClients();
-
-
try {
-
await db.destroy();
-
logger.info('Kysely instance destroyed');
-
} catch (err) {
-
logger.error({ err }, 'Error during database shutdown');
-
}
-
-
process.exit(0);
-
};
-
-
process.on('SIGTERM', shutdown);
-
process.on('SIGINT', shutdown);
-85
src/services/firehose-listen.ts
···
-
import { Firehose, CommitEvent, AccountEvent, IdentityEvent } from "@skyware/firehose";
-
import WebSocket from "ws";
-
import { RecordProcessor, AtpRecord } from "util/recordProcessor";
-
import { db } from "../db";
-
import { logger } from "../util/logger";
-
-
const main = async () => {
-
logger.info("Starting Firehose listener...");
-
-
let lastCursor: string | undefined;
-
try {
-
const result = await db.selectFrom('firehose_cursor')
-
.select('cursor')
-
.where('id', '=', 1)
-
.executeTakeFirst();
-
lastCursor = result?.cursor;
-
} catch (err) {
-
logger.error({ err }, "Failed to retrieve cursor from DB");
-
}
-
-
const firehose = new Firehose({
-
ws: WebSocket,
-
cursor: lastCursor,
-
});
-
-
let eventCount = 0;
-
-
firehose.on("commit", async (commit: CommitEvent) => {
-
const { repo: did, time: indexedAt, seq } = commit;
-
-
const processor = new RecordProcessor(did);
-
const createOps = commit.ops.filter(op => op.action === 'create');
-
-
for (const op of createOps) {
-
const record = op.record as AtpRecord;
-
const uri = `at://${did}/${op.path}`;
-
const cid = op.cid.toString();
-
-
await processor.processRecord(record, uri, cid, indexedAt, true);
-
}
-
-
await processor.insertAllProcessedRecords(true);
-
-
if (seq) {
-
eventCount++;
-
if (eventCount >= 20) {
-
eventCount = 0;
-
await db.updateTable('firehose_cursor')
-
.set({ cursor: seq.toString(), updated_at: new Date() })
-
.where('id', '=', 1)
-
.execute();
-
}
-
}
-
});
-
-
firehose.on("open", () => {
-
logger.info("Connection opened");
-
});
-
-
firehose.on("close", async (cursor) => {
-
logger.info({ cursor }, "Connection closed. Restarting.");
-
if (cursor) {
-
await db.updateTable('firehose_cursor')
-
.set({ cursor: cursor.toString(), updated_at: new Date() })
-
.where('id', '=', 1)
-
.execute();
-
firehose.cursor = cursor.toString();
-
}
-
firehose.start();
-
});
-
-
firehose.on("error", ({ error, cursor }) => {
-
logger.error({ err: error, cursor }, "An error occurred");
-
});
-
-
if (lastCursor && lastCursor !== '0') {
-
logger.info({ cursor: lastCursor }, "Resuming from cursor");
-
}
-
-
firehose.start();
-
-
logger.info("Listeners attached. Waiting for events...");
-
};
-
-
main();
-97
src/services/queue.ts
···
-
import { db, pool } from '../db';
-
import { sql } from 'kysely';
-
import { logger } from '../util/logger';
-
import { broadcast, getClientCount } from './websocket';
-
-
import { PoolClient } from 'pg';
-
-
const BATCH_SIZE = 25;
-
let listenerClient: PoolClient | null = null;
-
let isShuttingDown = false;
-
-
export function setShuttingDown(value: boolean) {
-
isShuttingDown = value;
-
if (value && listenerClient) {
-
listenerClient.release();
-
listenerClient = null;
-
}
-
}
-
-
async function popQueue(): Promise<any[]> {
-
if (isShuttingDown) return [];
-
-
try {
-
const records = await db.transaction().execute(async (trx) => {
-
const foundRecords = await trx
-
.selectFrom('raw_record_queue')
-
.selectAll()
-
.where('status', '=', 'pending')
-
.orderBy('created_at', 'asc')
-
.limit(BATCH_SIZE)
-
.forUpdate()
-
.skipLocked()
-
.execute();
-
-
if (foundRecords.length === 0) {
-
return [];
-
}
-
-
const recordIds = foundRecords.map((r: any) => r.id);
-
-
await trx
-
.updateTable('raw_record_queue')
-
.set({ status: 'complete', updated_at: sql`now()` })
-
.where('id', 'in', recordIds)
-
.execute();
-
-
return foundRecords;
-
});
-
-
return records;
-
} catch (e: any) {
-
logger.error({ err: e }, 'Error popping queue');
-
return [];
-
}
-
}
-
-
export async function processQueue() {
-
const records = await popQueue();
-
-
if (records.length > 0) {
-
logger.debug({ count: records.length, clients: getClientCount() }, 'Popped records, broadcasting to clients');
-
-
const recordData = records.map((r: any) => r.record_data);
-
-
broadcast({
-
type: 'new_records',
-
records: recordData,
-
});
-
}
-
}
-
-
export async function setupQueueListener() {
-
if (isShuttingDown) return;
-
-
try {
-
listenerClient = await pool.connect();
-
-
listenerClient.on('notification', async (msg) => {
-
if (msg.channel === 'new_raw_record') {
-
await processQueue();
-
}
-
});
-
-
await listenerClient.query('LISTEN new_raw_record');
-
logger.info('Listening for new_raw_record notifications');
-
-
listenerClient.on('error', (err) => {
-
if (isShuttingDown) return;
-
logger.error({ err }, 'Database listener client error');
-
// TODO: Reconnect logic could go here, but for now we rely on the heartbeat
-
});
-
-
} catch (err) {
-
if (isShuttingDown) return;
-
logger.error({ err }, 'Failed to setup queue listener');
-
}
-
}
-40
src/services/websocket.ts
···
-
import { ServerWebSocket } from 'bun';
-
import { logger } from '../util/logger';
-
-
const clients = new Set<ServerWebSocket<any>>();
-
-
export const websocketHandler = {
-
open(ws: ServerWebSocket<any>) {
-
logger.info('WebSocket client connected');
-
clients.add(ws);
-
},
-
close(ws: ServerWebSocket<any>) {
-
logger.info('WebSocket client disconnected');
-
clients.delete(ws);
-
},
-
message(_ws: ServerWebSocket<any>, _message: string | Buffer) {
-
// We don't expect messages from clients for now
-
},
-
};
-
-
export function broadcast(data: any) {
-
const message = JSON.stringify(data);
-
clients.forEach((client) => {
-
try {
-
client.send(message);
-
} catch (e: any) {
-
logger.error({ err: e }, 'Failed to send message to client');
-
}
-
});
-
}
-
-
export function closeAllClients() {
-
for (const client of clients) {
-
client.close();
-
}
-
clients.clear();
-
}
-
-
export function getClientCount() {
-
return clients.size;
-
}
+231
src/tap/consumer.rs
···
+
use std::sync::Arc;
+
use std::time::Duration;
+
+
use chrono::{DateTime, Utc};
+
use futures::{SinkExt, StreamExt};
+
use serde::Deserialize;
+
use tokio::sync::broadcast;
+
use tokio_tungstenite::{connect_async, tungstenite::Message};
+
+
use crate::db;
+
use crate::records::{
+
CidRef, NewAccount, NewRecord, CHANNEL_COLLECTION, INVITE_COLLECTION, LATTICE_COLLECTION,
+
MEMBERSHIP_COLLECTION, SHARD_COLLECTION,
+
};
+
use crate::AppState;
+
+
#[derive(Debug, Deserialize)]
+
pub struct TapMessage {
+
pub id: u64,
+
#[serde(rename = "type")]
+
pub msg_type: String,
+
pub record: Option<TapRecordEvent>,
+
}
+
+
#[derive(Debug, serde::Serialize)]
+
struct TapAck {
+
#[serde(rename = "type")]
+
msg_type: &'static str,
+
id: u64,
+
}
+
+
impl TapAck {
+
fn new(id: u64) -> Self {
+
Self { msg_type: "ack", id }
+
}
+
}
+
+
#[derive(Debug, Deserialize)]
+
pub struct TapRecordEvent {
+
pub did: String,
+
pub collection: String,
+
pub rkey: String,
+
pub cid: String,
+
pub action: String,
+
pub record: Option<serde_json::Value>,
+
#[serde(default)]
+
pub live: bool,
+
}
+
+
fn parse_datetime(s: &str) -> Option<DateTime<Utc>> {
+
DateTime::parse_from_rfc3339(s)
+
.map(|dt| dt.with_timezone(&Utc))
+
.ok()
+
}
+
+
fn build_uri(did: &str, collection: &str, rkey: &str) -> String {
+
format!("at://{}/{}/{}", did, collection, rkey)
+
}
+
+
fn extract_cid_ref(record: &serde_json::Value, field: &str) -> Option<String> {
+
record
+
.get(field)
+
.and_then(|v| serde_json::from_value::<CidRef>(v.clone()).ok())
+
.map(|r| r.cid)
+
}
+
+
async fn process_record_event(
+
state: &Arc<AppState>,
+
event: TapRecordEvent,
+
tx: &broadcast::Sender<String>,
+
) -> anyhow::Result<()> {
+
if event.action == "delete" {
+
return Ok(());
+
}
+
+
let record = match &event.record {
+
Some(r) => r,
+
None => return Ok(()),
+
};
+
+
let pool = state.db.pool();
+
let now = Utc::now();
+
let uri = build_uri(&event.did, &event.collection, &event.rkey);
+
+
if !event.collection.starts_with("systems.gmstn.development.") {
+
return Ok(());
+
}
+
+
let created_at = record
+
.get("createdAt")
+
.and_then(|v| v.as_str())
+
.and_then(parse_datetime)
+
.unwrap_or(now);
+
+
db::upsert_account(
+
pool,
+
&NewAccount {
+
did: event.did.clone(),
+
handle: event.did.clone(),
+
created_at: now,
+
},
+
)
+
.await?;
+
+
let (creator_did, target_did, ref_cids) = match event.collection.as_str() {
+
LATTICE_COLLECTION | SHARD_COLLECTION | CHANNEL_COLLECTION => {
+
(event.did.clone(), None, vec![])
+
}
+
INVITE_COLLECTION => {
+
let channel_cid = extract_cid_ref(record, "channel");
+
let recipient = record
+
.get("recipient")
+
.and_then(|v| v.as_str())
+
.map(String::from);
+
+
if let Some(ref recipient_did) = recipient {
+
db::upsert_account(
+
pool,
+
&NewAccount {
+
did: recipient_did.clone(),
+
handle: recipient_did.clone(),
+
created_at: now,
+
},
+
)
+
.await?;
+
}
+
+
let ref_cids = channel_cid.into_iter().collect();
+
(event.did.clone(), recipient, ref_cids)
+
}
+
MEMBERSHIP_COLLECTION => {
+
let channel_cid = extract_cid_ref(record, "channel");
+
let invite_cid = extract_cid_ref(record, "invite");
+
+
let ref_cids: Vec<String> = [channel_cid, invite_cid].into_iter().flatten().collect();
+
+
(event.did.clone(), Some(event.did.clone()), ref_cids)
+
}
+
_ => {
+
tracing::debug!(collection = %event.collection, "Unknown collection type");
+
return Ok(());
+
}
+
};
+
+
db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: event.cid,
+
collection: event.collection,
+
creator_did,
+
created_at,
+
indexed_at: now,
+
data: record.clone(),
+
target_did,
+
ref_cids,
+
},
+
)
+
.await?;
+
+
if let Ok(json) = serde_json::to_string(record) {
+
let _ = tx.send(json);
+
}
+
+
Ok(())
+
}
+
+
pub async fn run(url: String, state: Arc<AppState>, tx: broadcast::Sender<String>) {
+
loop {
+
tracing::info!("Connecting to TAP at {}", url);
+
+
match connect_async(&url).await {
+
Ok((ws_stream, _)) => {
+
tracing::info!("Connected to TAP");
+
let (mut write, mut read) = ws_stream.split();
+
+
while let Some(msg) = read.next().await {
+
match msg {
+
Ok(Message::Text(text)) => {
+
match serde_json::from_str::<TapMessage>(&text) {
+
Ok(msg) => {
+
let event_id = msg.id;
+
+
if msg.msg_type == "record" {
+
if let Some(record_event) = msg.record {
+
if let Err(e) = process_record_event(&state, record_event, &tx).await {
+
tracing::error!(error = %e, "Failed to process TAP event");
+
}
+
}
+
}
+
+
let ack = TapAck::new(event_id);
+
if let Ok(ack_json) = serde_json::to_string(&ack) {
+
if let Err(e) = write.send(Message::Text(ack_json.into())).await {
+
tracing::error!(error = %e, "Failed to send ack");
+
break;
+
}
+
}
+
}
+
Err(e) => {
+
tracing::warn!(error = %e, "Failed to parse TAP message");
+
}
+
}
+
}
+
Ok(Message::Ping(data)) => {
+
if let Err(e) = write.send(Message::Pong(data)).await {
+
tracing::error!(error = %e, "Failed to send pong");
+
break;
+
}
+
}
+
Ok(Message::Close(_)) => {
+
tracing::info!("TAP connection closed");
+
break;
+
}
+
Err(e) => {
+
tracing::error!(error = %e, "TAP WebSocket error");
+
break;
+
}
+
_ => {}
+
}
+
}
+
}
+
Err(e) => {
+
tracing::error!(error = %e, "Failed to connect to TAP");
+
}
+
}
+
+
tracing::info!("Reconnecting to TAP in 5 seconds...");
+
tokio::time::sleep(Duration::from_secs(5)).await;
+
}
+
}
+1
src/tap/mod.rs
···
+
pub mod consumer;
-16
src/util/logger.ts
···
-
import pino from 'pino';
-
-
const isDev = process.env.NODE_ENV !== 'production';
-
-
export const logger = pino({
-
level: process.env.LOG_LEVEL || 'info',
-
transport: isDev
-
? {
-
target: 'pino-pretty',
-
options: {
-
colorize: true,
-
translateTime: 'SYS:standard',
-
},
-
}
-
: undefined,
-
});
-17
src/util/params.ts
···
-
export function parseLimit(rawLimit: unknown, defaultLimit = 50): number {
-
let limit: number;
-
-
if (typeof rawLimit === 'string') {
-
limit = parseInt(rawLimit, 10);
-
} else if (typeof rawLimit === 'number') {
-
limit = rawLimit;
-
} else {
-
limit = defaultLimit;
-
}
-
-
if (isNaN(limit) || limit <= 0) {
-
limit = defaultLimit;
-
}
-
-
return limit;
-
}
-305
src/util/recordProcessor.ts
···
-
import { db } from "db";
-
import { sql } from "kysely";
-
import { logger } from "./logger";
-
-
export interface AtpRecord {
-
$type: string;
-
createdAt: string;
-
[key: string]: any;
-
}
-
-
export interface NewAccount {
-
did: string;
-
handle: string;
-
pds_hostname: string | null;
-
created_at: any;
-
}
-
-
export interface NewLattice { uri: string; cid: string; creator_did: string; description?: string; created_at: any; indexed_at: any; data: AtpRecord }
-
export interface NewShard { uri: string; cid: string; creator_did: string; description?: string; created_at: any; indexed_at: any; data: AtpRecord }
-
export interface NewChannel { uri: string; cid: string; creator_did: string; name: string; topic: string; created_at: any; indexed_at: any; data: AtpRecord }
-
-
export interface NewChannelInvite {
-
uri: string;
-
cid: string;
-
creator_did: string;
-
created_at: any;
-
indexed_at: any;
-
data: AtpRecord;
-
channel?: string;
-
recipient_did?: string;
-
}
-
-
export interface NewChannelMembership {
-
uri: string;
-
cid: string;
-
created_at: any;
-
indexed_at: any;
-
updated_at?: string;
-
state: string;
-
data: AtpRecord;
-
channel: string;
-
invite: string;
-
recipient_did?: string;
-
}
-
-
export interface NewRawRecord {
-
record_data: AtpRecord;
-
}
-
-
const LATTICE_LEXICON = 'systems.gmstn.development.lattice';
-
const SHARD_LEXICON = 'systems.gmstn.development.shard';
-
const CHANNEL_LEXICON = 'systems.gmstn.development.channel';
-
const CHANNEL_INVITE_LEXICON = 'systems.gmstn.development.channel.invite';
-
const CHANNEL_MEMBERSHIP_LEXICON = 'systems.gmstn.development.channel.membership';
-
-
export class RecordProcessor {
-
private newLattices: NewLattice[] = [];
-
private newShards: NewShard[] = [];
-
private newRawRecords: NewRawRecord[] = [];
-
-
private realChannels = new Map<string, NewChannel>();
-
private placeholderChannels = new Map<string, any>();
-
-
private realInvites = new Map<string, NewChannelInvite>();
-
private placeholderInvites = new Map<string, any>();
-
-
private realMemberships = new Map<string, NewChannelMembership>();
-
private placeholderAccounts = new Map<string, NewAccount>();
-
-
constructor(private did: string) {}
-
-
private queuePlaceholderAccount(did: string) {
-
if (!did) return;
-
if (!this.placeholderAccounts.has(did)) {
-
this.placeholderAccounts.set(did, {
-
did: did,
-
handle: did,
-
pds_hostname: null,
-
created_at: sql`now()`,
-
});
-
}
-
}
-
-
private queuePlaceholderChannel(cid: string) {
-
if (!cid) return;
-
if (this.realChannels.has(cid)) return;
-
-
if (!this.placeholderChannels.has(cid)) {
-
this.placeholderChannels.set(cid, {
-
cid: cid,
-
uri: null,
-
creator_did: null,
-
name: null,
-
topic: null,
-
created_at: sql`now()`,
-
indexed_at: sql`now()`,
-
data: null,
-
});
-
}
-
}
-
-
private queuePlaceholderInvite(cid: string) {
-
if (!cid) return;
-
if (this.realInvites.has(cid)) return;
-
-
if (!this.placeholderInvites.has(cid)) {
-
this.placeholderInvites.set(cid, {
-
cid: cid,
-
uri: null,
-
creator_did: null,
-
created_at: sql`now()`,
-
indexed_at: sql`now()`,
-
data: null,
-
channel: null,
-
recipient_did: null,
-
});
-
}
-
}
-
-
public async processRecord(
-
record: AtpRecord,
-
uri: string,
-
cid: string,
-
indexedAt: string,
-
includeRaw: boolean = false
-
) {
-
const collection = record?.$type;
-
-
if (!collection || !collection.startsWith('systems.gmstn.development.')) {
-
return;
-
}
-
-
if (includeRaw) {
-
this.newRawRecords.push({ record_data: record });
-
}
-
-
if (!record.createdAt || typeof record.createdAt !== 'string') {
-
return;
-
}
-
-
const baseRecord = {
-
uri: uri,
-
cid: cid,
-
created_at: record.createdAt,
-
indexed_at: indexedAt,
-
data: record,
-
};
-
-
switch (collection) {
-
case LATTICE_LEXICON:
-
this.newLattices.push({
-
...baseRecord,
-
creator_did: this.did,
-
description: record.description
-
} as NewLattice);
-
break;
-
-
case SHARD_LEXICON:
-
this.newShards.push({
-
...baseRecord,
-
creator_did: this.did,
-
description: record.description
-
} as NewShard);
-
break;
-
-
case CHANNEL_LEXICON: {
-
if (typeof record.name !== 'string' || typeof record.topic !== 'string') break;
-
-
this.placeholderChannels.delete(cid);
-
this.realChannels.set(cid, {
-
...baseRecord,
-
creator_did: this.did,
-
name: record.name,
-
topic: record.topic
-
} as NewChannel);
-
break;
-
}
-
-
case CHANNEL_INVITE_LEXICON: {
-
const recipientDid = record.recipient;
-
const channelCid = record.channel?.cid;
-
-
if (!channelCid || !recipientDid) break;
-
-
this.queuePlaceholderChannel(channelCid);
-
this.queuePlaceholderAccount(recipientDid);
-
-
this.placeholderInvites.delete(cid);
-
this.realInvites.set(cid, {
-
...baseRecord,
-
creator_did: this.did,
-
channel: channelCid,
-
recipient_did: recipientDid,
-
} as NewChannelInvite);
-
break;
-
}
-
-
case CHANNEL_MEMBERSHIP_LEXICON: {
-
const channelCid = record.channel?.cid;
-
const inviteCid = record.invite?.cid;
-
-
if (!channelCid || !inviteCid || typeof record.state !== 'string') break;
-
-
this.queuePlaceholderChannel(channelCid);
-
this.queuePlaceholderInvite(inviteCid);
-
-
this.realMemberships.set(cid, {
-
...baseRecord,
-
channel: channelCid,
-
invite: inviteCid,
-
state: record.state,
-
recipient_did: this.did,
-
updated_at: record.updatedAt || null,
-
} as NewChannelMembership);
-
break;
-
}
-
}
-
}
-
-
private async insertSimple(table: string, records: any[]) {
-
if (records.length === 0) return;
-
try {
-
await db.insertInto(table as any).values(records).execute();
-
logger.debug({ did: this.did, table, count: records.length }, "Inserted records");
-
} catch (e: any) {
-
if (e.code === '23505') {
-
logger.debug({ did: this.did, table, err: e.message }, "Duplicate record ignored");
-
} else {
-
logger.error({ did: this.did, table, err: e }, "Failed to insert records");
-
}
-
}
-
}
-
-
public async insertAllProcessedRecords(includeRaw: boolean = false) {
-
const accounts = Array.from(this.placeholderAccounts.values());
-
if (accounts.length > 0) {
-
await db.insertInto('account')
-
.values(accounts)
-
.onConflict(oc => oc.column('did').doNothing())
-
.execute()
-
.catch(e => logger.error({ did: this.did, err: e }, "Account insert error"));
-
}
-
-
const placeholderChans = Array.from(this.placeholderChannels.values());
-
if (placeholderChans.length > 0) {
-
await db.insertInto('channel')
-
.values(placeholderChans)
-
.onConflict(oc => oc.column('cid').doNothing())
-
.execute()
-
.catch(e => logger.error({ did: this.did, err: e }, "Placeholder channel error"));
-
}
-
-
const realChans = Array.from(this.realChannels.values());
-
if (realChans.length > 0) {
-
await db.insertInto('channel')
-
.values(realChans)
-
.onConflict(oc => oc.column('cid').doUpdateSet({
-
uri: (eb) => eb.ref('excluded.uri'),
-
creator_did: (eb) => eb.ref('excluded.creator_did'),
-
name: (eb) => eb.ref('excluded.name'),
-
topic: (eb) => eb.ref('excluded.topic'),
-
created_at: (eb) => eb.ref('excluded.created_at'),
-
indexed_at: (eb) => eb.ref('excluded.indexed_at'),
-
data: (eb) => eb.ref('excluded.data'),
-
}))
-
.execute()
-
.catch(e => logger.error({ did: this.did, err: e }, "Real channel upsert error"));
-
}
-
-
const placeholderInvs = Array.from(this.placeholderInvites.values());
-
if (placeholderInvs.length > 0) {
-
await db.insertInto('channel_invite')
-
.values(placeholderInvs)
-
.onConflict(oc => oc.column('cid').doNothing())
-
.execute()
-
.catch(e => logger.error({ did: this.did, err: e }, "Placeholder invite error"));
-
}
-
-
const realInvs = Array.from(this.realInvites.values());
-
if (realInvs.length > 0) {
-
await db.insertInto('channel_invite')
-
.values(realInvs)
-
.onConflict(oc => oc.column('cid').doUpdateSet({
-
uri: (eb) => eb.ref('excluded.uri'),
-
creator_did: (eb) => eb.ref('excluded.creator_did'),
-
created_at: (eb) => eb.ref('excluded.created_at'),
-
indexed_at: (eb) => eb.ref('excluded.indexed_at'),
-
data: (eb) => eb.ref('excluded.data'),
-
channel: (eb) => eb.ref('excluded.channel'),
-
recipient_did: (eb) => eb.ref('excluded.recipient_did'),
-
}))
-
.execute()
-
.catch(e => logger.error({ did: this.did, err: e }, "Real invite upsert error"));
-
}
-
-
const memberships = Array.from(this.realMemberships.values());
-
if (memberships.length > 0) {
-
await this.insertSimple('channel_membership', memberships);
-
}
-
-
if (includeRaw) await this.insertSimple('raw_record_queue', this.newRawRecords);
-
await this.insertSimple('lattice', this.newLattices);
-
await this.insertSimple('shard', this.newShards);
-
}
-
}
+21
src/ws/broadcast.rs
···
+
use tokio::sync::broadcast;
+
+
#[derive(Clone)]
+
pub struct Broadcaster {
+
tx: broadcast::Sender<String>,
+
}
+
+
impl Broadcaster {
+
pub fn new(tx: broadcast::Sender<String>) -> Self {
+
Self { tx }
+
}
+
+
pub fn subscribe(&self) -> broadcast::Receiver<String> {
+
self.tx.subscribe()
+
}
+
+
#[allow(dead_code)]
+
pub fn send(&self, msg: String) -> Result<usize, broadcast::error::SendError<String>> {
+
self.tx.send(msg)
+
}
+
}
+89
src/ws/mod.rs
···
+
mod broadcast;
+
+
pub use broadcast::*;
+
+
use std::sync::Arc;
+
+
use axum::{
+
extract::{
+
ws::{Message, WebSocket, WebSocketUpgrade},
+
State,
+
},
+
response::IntoResponse,
+
routing::get,
+
Router,
+
};
+
use futures::{SinkExt, StreamExt};
+
+
use crate::AppState;
+
+
pub fn router() -> Router<Arc<AppState>> {
+
Router::new().route("/ws", get(ws_handler))
+
}
+
+
async fn ws_handler(
+
ws: WebSocketUpgrade,
+
State(state): State<Arc<AppState>>,
+
) -> impl IntoResponse {
+
ws.on_upgrade(move |socket| handle_socket(socket, state))
+
}
+
+
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
+
let (mut sender, mut receiver) = socket.split();
+
+
let mut rx = state.broadcaster.subscribe();
+
+
let send_task = tokio::spawn(async move {
+
loop {
+
match rx.recv().await {
+
Ok(msg) => {
+
if sender.send(Message::Text(msg.into())).await.is_err() {
+
break;
+
}
+
}
+
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
+
tracing::warn!(skipped = n, "WebSocket client lagged, skipped messages");
+
// Continue receiving - client will miss some messages but stay connected
+
}
+
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
+
tracing::debug!("Broadcast channel closed");
+
break;
+
}
+
}
+
}
+
});
+
let send_abort = send_task.abort_handle();
+
+
let recv_task = tokio::spawn(async move {
+
while let Some(msg) = receiver.next().await {
+
match msg {
+
Ok(Message::Ping(data)) => {
+
tracing::trace!("Received ping, sending pong");
+
// Note: axum's WebSocket auto-responds to pings, but we log it
+
let _ = data;
+
}
+
Ok(Message::Close(_)) => {
+
tracing::debug!("Client closed connection");
+
break;
+
}
+
Err(e) => {
+
tracing::warn!(error = %e, "WebSocket error");
+
break;
+
}
+
_ => {}
+
}
+
}
+
});
+
let recv_abort = recv_task.abort_handle();
+
+
tokio::select! {
+
_ = send_task => {
+
recv_abort.abort();
+
},
+
_ = recv_task => {
+
send_abort.abort();
+
},
+
}
+
+
tracing::debug!("WebSocket connection closed");
+
}
+396
tests/api.rs
···
+
mod common;
+
+
use chrono::Utc;
+
use reqwest::StatusCode;
+
use serde_json::{json, Value};
+
use prism::records::{
+
NewAccount, NewRecord, CHANNEL_COLLECTION, INVITE_COLLECTION, LATTICE_COLLECTION,
+
MEMBERSHIP_COLLECTION, SHARD_COLLECTION,
+
};
+
+
async fn insert_test_account(pool: &sqlx::PgPool, did: &str) {
+
prism::db::upsert_account(
+
pool,
+
&NewAccount {
+
did: did.to_string(),
+
handle: did.to_string(),
+
created_at: Utc::now(),
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
async fn insert_test_channel(pool: &sqlx::PgPool, cid: &str, creator_did: &str, name: &str) {
+
let uri = format!("at://{}/systems.gmstn.development.channel/{}", creator_did, cid);
+
let now = Utc::now();
+
prism::db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: cid.to_string(),
+
collection: CHANNEL_COLLECTION.to_string(),
+
creator_did: creator_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"name": name, "topic": "test topic"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
async fn insert_test_invite(
+
pool: &sqlx::PgPool,
+
cid: &str,
+
creator_did: &str,
+
channel_cid: &str,
+
recipient_did: &str,
+
) {
+
let uri = format!(
+
"at://{}/systems.gmstn.development.channel.invite/{}",
+
creator_did, cid
+
);
+
let now = Utc::now();
+
prism::db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: cid.to_string(),
+
collection: INVITE_COLLECTION.to_string(),
+
creator_did: creator_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"recipient": recipient_did}),
+
target_did: Some(recipient_did.to_string()),
+
ref_cids: vec![channel_cid.to_string()],
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
async fn insert_test_membership(
+
pool: &sqlx::PgPool,
+
cid: &str,
+
recipient_did: &str,
+
channel_cid: &str,
+
invite_cid: &str,
+
) {
+
let uri = format!(
+
"at://{}/systems.gmstn.development.channel.membership/{}",
+
recipient_did, cid
+
);
+
let now = Utc::now();
+
prism::db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: cid.to_string(),
+
collection: MEMBERSHIP_COLLECTION.to_string(),
+
creator_did: recipient_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"state": "accepted"}),
+
target_did: Some(recipient_did.to_string()),
+
ref_cids: vec![channel_cid.to_string(), invite_cid.to_string()],
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
async fn insert_test_lattice(pool: &sqlx::PgPool, cid: &str, creator_did: &str) {
+
let uri = format!("at://{}/systems.gmstn.development.lattice/{}", creator_did, cid);
+
let now = Utc::now();
+
prism::db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: cid.to_string(),
+
collection: LATTICE_COLLECTION.to_string(),
+
creator_did: creator_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"description": "test lattice"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
async fn insert_test_shard(pool: &sqlx::PgPool, cid: &str, creator_did: &str) {
+
let uri = format!("at://{}/systems.gmstn.development.shard/{}", creator_did, cid);
+
let now = Utc::now();
+
prism::db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: cid.to_string(),
+
collection: SHARD_COLLECTION.to_string(),
+
creator_did: creator_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"description": "test shard"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
#[tokio::test]
+
async fn test_list_channels_returns_correct_structure() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let test_did = "did:plc:test_channels_structure";
+
insert_test_account(&pool, test_did).await;
+
insert_test_channel(&pool, "bafychannel001", test_did, "Test Channel").await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels?author={}",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
assert!(body["channels"].is_array());
+
+
let channels = body["channels"].as_array().unwrap();
+
assert!(!channels.is_empty());
+
+
let channel = &channels[0];
+
assert!(channel["uri"].is_string());
+
assert!(channel["cid"].is_string());
+
assert!(channel["displayName"].is_string());
+
assert!(channel["createdAt"].is_string());
+
assert!(channel["indexedAt"].is_string());
+
}
+
+
#[tokio::test]
+
async fn test_list_channels_empty_author() {
+
let base_url = common::base_url().await;
+
let client = common::client();
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels?author=did:plc:nonexistent",
+
base_url
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
assert!(body["channels"].is_array());
+
assert!(body["channels"].as_array().unwrap().is_empty());
+
assert!(body["cursor"].is_null());
+
}
+
+
#[tokio::test]
+
async fn test_list_channels_limit_bounds() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let test_did = "did:plc:test_limit_bounds";
+
insert_test_account(&pool, test_did).await;
+
+
for i in 0..5 {
+
insert_test_channel(&pool, &format!("bafylimitchan{}", i), test_did, &format!("Channel {}", i))
+
.await;
+
}
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels?author={}&limit=2",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
let channels = body["channels"].as_array().unwrap();
+
assert_eq!(channels.len(), 2);
+
assert!(body["cursor"].is_string());
+
+
let res_over_limit = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels?author={}&limit=150",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res_over_limit.status(), StatusCode::OK);
+
+
let body_over: Value = res_over_limit.json().await.unwrap();
+
let channels_over = body_over["channels"].as_array().unwrap();
+
assert!(channels_over.len() <= 100);
+
}
+
+
#[tokio::test]
+
async fn test_list_invites_by_recipient() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let creator_did = "did:plc:invite_creator";
+
let recipient_did = "did:plc:invite_recipient";
+
insert_test_account(&pool, creator_did).await;
+
insert_test_account(&pool, recipient_did).await;
+
+
insert_test_invite(&pool, "bafyinvite001", creator_did, "bafyinvitechan001", recipient_did).await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listInvites?recipient={}",
+
base_url, recipient_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
assert!(body["invites"].is_array());
+
+
let invites = body["invites"].as_array().unwrap();
+
assert!(!invites.is_empty());
+
+
let invite = &invites[0];
+
assert!(invite["uri"].is_string());
+
assert!(invite["cid"].is_string());
+
assert!(invite["channel"].is_string());
+
assert!(invite["recipient"].is_string());
+
assert!(invite["createdAt"].is_string());
+
}
+
+
#[tokio::test]
+
async fn test_list_memberships_by_recipient() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let creator_did = "did:plc:membership_creator";
+
let recipient_did = "did:plc:membership_recipient";
+
insert_test_account(&pool, creator_did).await;
+
insert_test_account(&pool, recipient_did).await;
+
+
insert_test_membership(&pool, "bafymem001", recipient_did, "bafymemchan001", "bafymeminvite001").await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listMemberships?recipient={}",
+
base_url, recipient_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
assert!(body["memberships"].is_array());
+
+
let memberships = body["memberships"].as_array().unwrap();
+
assert!(!memberships.is_empty());
+
+
let membership = &memberships[0];
+
assert!(membership["uri"].is_string());
+
assert!(membership["cid"].is_string());
+
assert!(membership["channel"].is_string());
+
assert!(membership["invite"].is_string());
+
assert!(membership["recipient"].is_string());
+
assert!(membership["state"].is_string());
+
assert!(membership["createdAt"].is_string());
+
}
+
+
#[tokio::test]
+
async fn test_list_lattices_by_author() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let test_did = "did:plc:lattice_author";
+
insert_test_account(&pool, test_did).await;
+
insert_test_lattice(&pool, "bafylattice001", test_did).await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.lattice.listLattices?author={}",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
assert!(body["lattices"].is_array());
+
+
let lattices = body["lattices"].as_array().unwrap();
+
assert!(!lattices.is_empty());
+
+
let lattice = &lattices[0];
+
assert!(lattice["uri"].is_string());
+
assert!(lattice["cid"].is_string());
+
assert!(lattice["author"].is_string());
+
assert!(lattice["createdAt"].is_string());
+
assert!(lattice["indexedAt"].is_string());
+
}
+
+
#[tokio::test]
+
async fn test_list_shards_by_author() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let test_did = "did:plc:shard_author";
+
insert_test_account(&pool, test_did).await;
+
insert_test_shard(&pool, "bafyshard001", test_did).await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.shard.listShards?author={}",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
assert!(body["shards"].is_array());
+
+
let shards = body["shards"].as_array().unwrap();
+
assert!(!shards.is_empty());
+
+
let shard = &shards[0];
+
assert!(shard["uri"].is_string());
+
assert!(shard["cid"].is_string());
+
assert!(shard["author"].is_string());
+
assert!(shard["createdAt"].is_string());
+
assert!(shard["indexedAt"].is_string());
+
}
+167
tests/common/mod.rs
···
+
use prism::{AppState, db::Database};
+
use reqwest::Client;
+
use sqlx::postgres::PgPoolOptions;
+
use std::sync::{Arc, OnceLock};
+
use tokio::net::TcpListener;
+
use tokio::sync::broadcast;
+
+
static SERVER_URL: OnceLock<String> = OnceLock::new();
+
static APP_PORT: OnceLock<u16> = OnceLock::new();
+
+
use testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner};
+
use testcontainers_modules::postgres::Postgres;
+
+
static DB_CONTAINER: OnceLock<ContainerAsync<Postgres>> = OnceLock::new();
+
+
fn has_external_infra() -> bool {
+
std::env::var("PRISM_TEST_INFRA_READY").is_ok()
+
}
+
+
#[cfg(test)]
+
#[ctor::dtor]
+
fn cleanup() {
+
if has_external_infra() {
+
return;
+
}
+
+
if std::env::var("XDG_RUNTIME_DIR").is_ok() {
+
let _ = std::process::Command::new("podman")
+
.args(["rm", "-f", "--filter", "label=prism_test=true"])
+
.output();
+
}
+
+
let _ = std::process::Command::new("docker")
+
.args(["container", "prune", "-f", "--filter", "label=prism_test=true"])
+
.output();
+
}
+
+
#[allow(dead_code)]
+
pub fn client() -> Client {
+
Client::new()
+
}
+
+
#[allow(dead_code)]
+
pub fn app_port() -> u16 {
+
*APP_PORT.get().expect("APP_PORT not initialized")
+
}
+
+
pub async fn base_url() -> &'static str {
+
SERVER_URL.get_or_init(|| {
+
let (tx, rx) = std::sync::mpsc::channel();
+
+
std::thread::spawn(move || {
+
if std::env::var("DOCKER_HOST").is_err() {
+
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
+
let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
+
if podman_sock.exists() {
+
unsafe {
+
std::env::set_var(
+
"DOCKER_HOST",
+
format!("unix://{}", podman_sock.display()),
+
);
+
}
+
}
+
}
+
}
+
+
let rt = tokio::runtime::Runtime::new().unwrap();
+
rt.block_on(async move {
+
if has_external_infra() {
+
let url = setup_with_external_infra().await;
+
tx.send(url).unwrap();
+
} else {
+
let url = setup_with_testcontainers().await;
+
tx.send(url).unwrap();
+
}
+
std::future::pending::<()>().await;
+
});
+
});
+
+
rx.recv().expect("Failed to start test server")
+
})
+
}
+
+
async fn setup_with_external_infra() -> String {
+
let database_url =
+
std::env::var("DATABASE_URL").expect("DATABASE_URL must be set when using external infra");
+
+
spawn_app(database_url).await
+
}
+
+
async fn setup_with_testcontainers() -> String {
+
let container = Postgres::default()
+
.with_tag("18-alpine")
+
.with_label("prism_test", "true")
+
.start()
+
.await
+
.expect("Failed to start Postgres");
+
+
let connection_string = format!(
+
"postgres://postgres:postgres@127.0.0.1:{}/postgres",
+
container
+
.get_host_port_ipv4(5432)
+
.await
+
.expect("Failed to get port")
+
);
+
+
DB_CONTAINER.set(container).ok();
+
+
spawn_app(connection_string).await
+
}
+
+
async fn spawn_app(database_url: String) -> String {
+
let pool = PgPoolOptions::new()
+
.max_connections(10)
+
.connect(&database_url)
+
.await
+
.expect("Failed to connect to Postgres");
+
+
sqlx::migrate!("./migrations")
+
.run(&pool)
+
.await
+
.expect("Failed to run migrations");
+
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+
let addr = listener.local_addr().unwrap();
+
APP_PORT.set(addr.port()).ok();
+
+
let (tx, _rx) = broadcast::channel::<String>(1024);
+
let broadcaster = prism::ws::Broadcaster::new(tx);
+
+
let state = Arc::new(AppState {
+
db: Database::new(pool),
+
broadcaster,
+
});
+
+
let app = prism::api::router()
+
.merge(prism::ws::router())
+
.with_state(state);
+
+
tokio::spawn(async move {
+
axum::serve(listener, app).await.unwrap();
+
});
+
+
format!("http://{}", addr)
+
}
+
+
#[allow(dead_code)]
+
pub async fn get_db_pool() -> sqlx::PgPool {
+
base_url().await;
+
+
let connection_string = if has_external_infra() {
+
std::env::var("DATABASE_URL").expect("DATABASE_URL not set")
+
} else {
+
let container = DB_CONTAINER.get().expect("DB container not initialized");
+
let port = container
+
.get_host_port_ipv4(5432)
+
.await
+
.expect("Failed to get port");
+
format!("postgres://postgres:postgres@127.0.0.1:{}/postgres", port)
+
};
+
+
PgPoolOptions::new()
+
.max_connections(5)
+
.connect(&connection_string)
+
.await
+
.expect("Failed to connect to test database")
+
}
+230
tests/pagination.rs
···
+
mod common;
+
+
use chrono::{Duration, Utc};
+
use reqwest::StatusCode;
+
use serde_json::{json, Value};
+
use prism::records::{NewAccount, NewRecord, CHANNEL_COLLECTION};
+
use std::sync::atomic::{AtomicU64, Ordering};
+
+
static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
+
+
fn unique_suffix() -> String {
+
let ts = std::time::SystemTime::now()
+
.duration_since(std::time::UNIX_EPOCH)
+
.unwrap()
+
.as_nanos();
+
let count = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
+
format!("{}_{}", ts, count)
+
}
+
+
async fn insert_test_account(pool: &sqlx::PgPool, did: &str) {
+
prism::db::upsert_account(
+
pool,
+
&NewAccount {
+
did: did.to_string(),
+
handle: did.to_string(),
+
created_at: Utc::now(),
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
async fn insert_channel_at_time(
+
pool: &sqlx::PgPool,
+
cid: &str,
+
creator_did: &str,
+
name: &str,
+
indexed_at: chrono::DateTime<Utc>,
+
) {
+
let uri = format!("at://{}/systems.gmstn.development.channel/{}", creator_did, cid);
+
prism::db::insert_record(
+
pool,
+
&NewRecord {
+
uri,
+
cid: cid.to_string(),
+
collection: CHANNEL_COLLECTION.to_string(),
+
creator_did: creator_did.to_string(),
+
created_at: indexed_at,
+
indexed_at,
+
data: json!({"name": name, "topic": "test topic"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
#[tokio::test]
+
async fn test_pagination_first_page_returns_latest() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let suffix = unique_suffix();
+
let test_did = format!("did:plc:pagination_first_{}", suffix);
+
insert_test_account(&pool, &test_did).await;
+
+
let now = Utc::now();
+
insert_channel_at_time(&pool, &format!("bafypagfirst001_{}", suffix), &test_did, "Oldest", now - Duration::hours(3))
+
.await;
+
insert_channel_at_time(&pool, &format!("bafypagfirst002_{}", suffix), &test_did, "Middle", now - Duration::hours(2))
+
.await;
+
insert_channel_at_time(&pool, &format!("bafypagfirst003_{}", suffix), &test_did, "Newest", now - Duration::hours(1))
+
.await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels?author={}&limit=2",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
let channels = body["channels"].as_array().unwrap();
+
+
assert_eq!(channels.len(), 2);
+
assert_eq!(channels[0]["displayName"].as_str().unwrap(), "Newest");
+
assert_eq!(channels[1]["displayName"].as_str().unwrap(), "Middle");
+
assert!(body["cursor"].is_string());
+
}
+
+
#[tokio::test]
+
async fn test_pagination_with_cursor_excludes_newer() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let suffix = unique_suffix();
+
let test_did = format!("did:plc:pagination_cursor_{}", suffix);
+
insert_test_account(&pool, &test_did).await;
+
+
let now = Utc::now();
+
let oldest_time = now - Duration::hours(3);
+
let middle_time = now - Duration::hours(2);
+
let newest_time = now - Duration::hours(1);
+
+
insert_channel_at_time(&pool, &format!("bafypagcur001_{}", suffix), &test_did, "Oldest", oldest_time).await;
+
insert_channel_at_time(&pool, &format!("bafypagcur002_{}", suffix), &test_did, "Middle", middle_time).await;
+
insert_channel_at_time(&pool, &format!("bafypagcur003_{}", suffix), &test_did, "Newest", newest_time).await;
+
+
let cursor = middle_time.to_rfc3339();
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels",
+
base_url
+
))
+
.query(&[("author", test_did.as_str()), ("cursor", cursor.as_str()), ("limit", "10")])
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
let channels = body["channels"].as_array().unwrap();
+
+
assert_eq!(channels.len(), 1);
+
assert_eq!(channels[0]["displayName"].as_str().unwrap(), "Oldest");
+
}
+
+
#[tokio::test]
+
async fn test_pagination_returns_cursor_for_continuation() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let suffix = unique_suffix();
+
let test_did = format!("did:plc:pagination_single_{}", suffix);
+
insert_test_account(&pool, &test_did).await;
+
+
let now = Utc::now();
+
insert_channel_at_time(&pool, &format!("bafypagsingle001_{}", suffix), &test_did, "Only", now - Duration::hours(1))
+
.await;
+
+
let res = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels?author={}&limit=10",
+
base_url, test_did
+
))
+
.send()
+
.await
+
.unwrap();
+
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
let channels = body["channels"].as_array().unwrap();
+
+
assert_eq!(channels.len(), 1);
+
assert!(body["cursor"].is_string());
+
}
+
+
#[tokio::test]
+
async fn test_pagination_full_traversal() {
+
let base_url = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
let client = common::client();
+
+
let suffix = unique_suffix();
+
let test_did = format!("did:plc:pagination_full_{}", suffix);
+
insert_test_account(&pool, &test_did).await;
+
+
let now = Utc::now();
+
for i in 0..5 {
+
insert_channel_at_time(
+
&pool,
+
&format!("bafypagfull{:03}_{}", i, suffix),
+
&test_did,
+
&format!("Channel {}", i),
+
now - Duration::hours(5 - i as i64),
+
)
+
.await;
+
}
+
+
let mut all_names: Vec<String> = Vec::new();
+
let mut cursor: Option<String> = None;
+
+
loop {
+
let mut req = client
+
.get(format!(
+
"{}/xrpc/systems.gmstn.development.channel.listChannels",
+
base_url
+
))
+
.query(&[("author", test_did.as_str()), ("limit", "2")]);
+
+
if let Some(c) = &cursor {
+
req = req.query(&[("cursor", c.as_str())]);
+
}
+
+
let res = req.send().await.unwrap();
+
assert_eq!(res.status(), StatusCode::OK);
+
+
let body: Value = res.json().await.unwrap();
+
let channels = body["channels"].as_array().unwrap();
+
+
if channels.is_empty() {
+
break;
+
}
+
+
for channel in channels {
+
all_names.push(channel["displayName"].as_str().unwrap().to_string());
+
}
+
+
cursor = body["cursor"].as_str().map(String::from);
+
+
if cursor.is_none() {
+
break;
+
}
+
}
+
+
assert_eq!(all_names.len(), 5);
+
assert_eq!(all_names[0], "Channel 4");
+
assert_eq!(all_names[4], "Channel 0");
+
}
+308
tests/tap_consumer.rs
···
+
mod common;
+
+
use chrono::Utc;
+
use serde_json::json;
+
use prism::records::{
+
NewAccount, NewRecord, CHANNEL_COLLECTION, INVITE_COLLECTION, LATTICE_COLLECTION,
+
MEMBERSHIP_COLLECTION, SHARD_COLLECTION,
+
};
+
+
async fn insert_test_account(pool: &sqlx::PgPool, did: &str) {
+
prism::db::upsert_account(
+
pool,
+
&NewAccount {
+
did: did.to_string(),
+
handle: did.to_string(),
+
created_at: Utc::now(),
+
},
+
)
+
.await
+
.unwrap();
+
}
+
+
#[tokio::test]
+
async fn test_lattice_insert() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let test_did = "did:plc:tap_lattice_test";
+
insert_test_account(&pool, test_did).await;
+
+
let uri = format!("at://{}/systems.gmstn.development.lattice/abc123", test_did);
+
let now = Utc::now();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafylatticetest001".to_string(),
+
collection: LATTICE_COLLECTION.to_string(),
+
creator_did: test_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"description": "Test lattice"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
+
let records = prism::db::list_records_by_creator(&pool, LATTICE_COLLECTION, test_did, None, 10)
+
.await
+
.unwrap();
+
+
assert!(!records.is_empty());
+
assert_eq!(records[0].uri, uri);
+
assert_eq!(records[0].cid, "bafylatticetest001");
+
assert_eq!(records[0].creator_did, test_did);
+
}
+
+
#[tokio::test]
+
async fn test_shard_insert() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let test_did = "did:plc:tap_shard_test";
+
insert_test_account(&pool, test_did).await;
+
+
let uri = format!("at://{}/systems.gmstn.development.shard/xyz789", test_did);
+
let now = Utc::now();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafyshardtest001".to_string(),
+
collection: SHARD_COLLECTION.to_string(),
+
creator_did: test_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"description": "Test shard"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
+
let records = prism::db::list_records_by_creator(&pool, SHARD_COLLECTION, test_did, None, 10)
+
.await
+
.unwrap();
+
+
assert!(!records.is_empty());
+
assert_eq!(records[0].uri, uri);
+
assert_eq!(records[0].cid, "bafyshardtest001");
+
assert_eq!(records[0].creator_did, test_did);
+
}
+
+
#[tokio::test]
+
async fn test_channel_insert() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let test_did = "did:plc:tap_channel_insert";
+
insert_test_account(&pool, test_did).await;
+
+
let uri = format!("at://{}/systems.gmstn.development.channel/chan001", test_did);
+
let now = Utc::now();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafychanneltest001".to_string(),
+
collection: CHANNEL_COLLECTION.to_string(),
+
creator_did: test_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"name": "Test Channel", "topic": "Test Topic"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
+
let records = prism::db::list_records_by_creator(&pool, CHANNEL_COLLECTION, test_did, None, 10)
+
.await
+
.unwrap();
+
+
assert_eq!(records.len(), 1);
+
assert_eq!(records[0].cid, "bafychanneltest001");
+
assert_eq!(records[0].uri, uri);
+
}
+
+
#[tokio::test]
+
async fn test_invite_insert() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let creator_did = "did:plc:tap_invite_creator";
+
let recipient_did = "did:plc:tap_invite_recipient";
+
insert_test_account(&pool, creator_did).await;
+
insert_test_account(&pool, recipient_did).await;
+
+
let uri = format!("at://{}/systems.gmstn.development.channel.invite/inv001", creator_did);
+
let now = Utc::now();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafyinvitetest001".to_string(),
+
collection: INVITE_COLLECTION.to_string(),
+
creator_did: creator_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"recipient": recipient_did}),
+
target_did: Some(recipient_did.to_string()),
+
ref_cids: vec!["bafychannel001".to_string()],
+
},
+
)
+
.await
+
.unwrap();
+
+
let records = prism::db::list_records_by_target(&pool, INVITE_COLLECTION, recipient_did, None, 10)
+
.await
+
.unwrap();
+
+
assert_eq!(records.len(), 1);
+
assert_eq!(records[0].cid, "bafyinvitetest001");
+
assert_eq!(records[0].ref_cids, vec!["bafychannel001".to_string()]);
+
}
+
+
#[tokio::test]
+
async fn test_membership_insert() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let recipient_did = "did:plc:tap_membership_recipient";
+
insert_test_account(&pool, recipient_did).await;
+
+
let uri = format!(
+
"at://{}/systems.gmstn.development.channel.membership/mem001",
+
recipient_did
+
);
+
let now = Utc::now();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafymembertest001".to_string(),
+
collection: MEMBERSHIP_COLLECTION.to_string(),
+
creator_did: recipient_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"state": "accepted"}),
+
target_did: Some(recipient_did.to_string()),
+
ref_cids: vec!["bafychannel001".to_string(), "bafyinvite001".to_string()],
+
},
+
)
+
.await
+
.unwrap();
+
+
let records = prism::db::list_records_by_target(&pool, MEMBERSHIP_COLLECTION, recipient_did, None, 10)
+
.await
+
.unwrap();
+
+
assert_eq!(records.len(), 1);
+
assert_eq!(records[0].cid, "bafymembertest001");
+
assert_eq!(records[0].data["state"], "accepted");
+
}
+
+
#[tokio::test]
+
async fn test_account_upsert_is_idempotent() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let test_did = "did:plc:tap_account_idempotent";
+
let now = Utc::now();
+
+
prism::db::upsert_account(
+
&pool,
+
&prism::records::NewAccount {
+
did: test_did.to_string(),
+
handle: "first_handle".to_string(),
+
created_at: now,
+
},
+
)
+
.await
+
.unwrap();
+
+
prism::db::upsert_account(
+
&pool,
+
&prism::records::NewAccount {
+
did: test_did.to_string(),
+
handle: "second_handle".to_string(),
+
created_at: now,
+
},
+
)
+
.await
+
.unwrap();
+
+
let account: Option<(String,)> =
+
sqlx::query_as("SELECT handle FROM account WHERE did = $1")
+
.bind(test_did)
+
.fetch_optional(&pool)
+
.await
+
.unwrap();
+
+
assert!(account.is_some());
+
assert_eq!(account.unwrap().0, "first_handle");
+
}
+
+
#[tokio::test]
+
async fn test_record_upsert_updates_data() {
+
let _ = common::base_url().await;
+
let pool = common::get_db_pool().await;
+
+
let test_did = "did:plc:tap_record_upsert";
+
insert_test_account(&pool, test_did).await;
+
+
let uri = format!("at://{}/systems.gmstn.development.channel/upsert001", test_did);
+
let now = Utc::now();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafyupsert001".to_string(),
+
collection: CHANNEL_COLLECTION.to_string(),
+
creator_did: test_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"name": "Original Name"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
+
prism::db::insert_record(
+
&pool,
+
&NewRecord {
+
uri: uri.clone(),
+
cid: "bafyupsert002".to_string(),
+
collection: CHANNEL_COLLECTION.to_string(),
+
creator_did: test_did.to_string(),
+
created_at: now,
+
indexed_at: now,
+
data: json!({"name": "Updated Name"}),
+
target_did: None,
+
ref_cids: vec![],
+
},
+
)
+
.await
+
.unwrap();
+
+
let records = prism::db::list_records_by_creator(&pool, CHANNEL_COLLECTION, test_did, None, 10)
+
.await
+
.unwrap();
+
+
assert_eq!(records.len(), 1);
+
assert_eq!(records[0].cid, "bafyupsert002");
+
assert_eq!(records[0].data["name"], "Updated Name");
+
}
-41
tsconfig.json
···
-
{
-
"compilerOptions": {
-
/* Basic Options */
-
"target": "ESNext" /* Modern Node.js supports this */,
-
"module": "NodeNext" /* This is the correct module system for modern Node.js */,
-
"lib": ["ESNext"] /* Use ESNext libs, NOT 'dom' for a backend server */,
-
"outDir": "./dist" /* Good */,
-
"rootDir": "./src" /* Good */,
-
"incremental": true,
-
-
/* Module Resolution */
-
"moduleResolution": "NodeNext" /* Matches 'module: NodeNext' */,
-
"baseUrl": "./src" /* This is KEY: allows imports like 'db' from 'src/db.ts' */,
-
// "paths": { ... } /* We're not using '@/' paths for this fix, so this is removed */
-
"resolveJsonModule": true,
-
-
/* Strictness & Quality */
-
"strict": true,
-
"forceConsistentCasingInFileNames": true,
-
"noImplicitAny": true,
-
"noImplicitThis": true,
-
"skipLibCheck": true,
-
-
/* Interop */
-
"esModuleInterop": true /* Allows 'import x from y' with CommonJS modules */,
-
"allowJs": true,
-
"checkJs": true,
-
-
/* Emit */
-
"declaration": true,
-
"sourceMap": true,
-
"isolatedModules": true /* Good practice */
-
},
-
"include": [
-
"src/**/*" /* Compile everything in src */
-
],
-
"exclude": [
-
"node_modules",
-
"dist"
-
]
-
}