[READ-ONLY] Mirror of https://github.com/flo-bit/contrail. atproto backend in a bottle flo-bit.dev/contrail/
0

Configure Feed

Select the types of activity you want to include in your feed.

commiat

Florian (Apr 24, 2026, 5:24 PM +0200) ad3a61d6 3ee5ed4a

+235 -212
-1
README.md
··· 96 96 - [Spaces](./docs/05-spaces.md) — permissioned records stored by the appview 97 97 - [Communities](./docs/06-communities.md) — group-controlled atproto DIDs 98 98 - [Sync](./docs/07-sync.md) — reactive client-side store over `watchRecords` 99 - - [Examples](./docs/08-examples.md) — reference deployments in the repo 100 99 - Frameworks: [SvelteKit + Cloudflare](./docs/frameworks/sveltekit-cloudflare.md) 101 100 102 101 ## Packages
-40
pnpm-lock.yaml
··· 186 186 specifier: ^5.7.3 187 187 version: 5.9.3 188 188 189 - apps/rsvp-atmo: 190 - dependencies: 191 - '@atmo-dev/contrail': 192 - specifier: workspace:* 193 - version: link:../../packages/contrail 194 - devDependencies: 195 - '@atcute/atproto': 196 - specifier: ^3.1.10 197 - version: 3.1.11 198 - '@atcute/client': 199 - specifier: ^4.2.1 200 - version: 4.2.1 201 - '@atcute/lex-cli': 202 - specifier: ^2.5.3 203 - version: 2.8.1 204 - '@atcute/lexicon-doc': 205 - specifier: ^2.1.2 206 - version: 2.2.0 207 - '@atcute/lexicons': 208 - specifier: ^1.2.9 209 - version: 1.3.0 210 - '@atmo-dev/contrail-lexicons': 211 - specifier: workspace:* 212 - version: link:../../packages/lexicons 213 - '@cloudflare/workers-types': 214 - specifier: ^4.20250124.0 215 - version: 4.20260424.1 216 - '@types/node': 217 - specifier: ^25.5.0 218 - version: 25.6.0 219 - tsx: 220 - specifier: ^4.21.0 221 - version: 4.21.0 222 - typescript: 223 - specifier: ^5.7.3 224 - version: 5.9.3 225 - wrangler: 226 - specifier: ^4.63.0 227 - version: 4.84.1(@cloudflare/workers-types@4.20260424.1) 228 - 229 189 apps/sveltekit-cloudflare-workers: 230 190 dependencies: 231 191 '@atcute/jetstream':
+19
.changeset/contrail-dev-command.md
··· 1 + --- 2 + "@atmo-dev/contrail": minor 3 + --- 4 + 5 + add `contrail dev` — local dev wrapper for cloudflare workers deployments. 6 + 7 + replaces `wrangler dev --test-scheduled` + a separate cron-trigger script with one command. on start it: 8 + 9 + 1. connects to your local D1 via wrangler's `getPlatformProxy`, inspects state 10 + 2. prompts to run `backfillAll` if no completed backfills exist yet 11 + 3. prompts to run `refresh` if the ingest cursor is older than 60 minutes (configurable with `--stale-after`) 12 + 4. spawns `wrangler dev --test-scheduled` 13 + 5. fires `GET /__scheduled?cron=...` every 60 seconds so the cron actually runs in local dev (wrangler's scheduler only works in deployed production) 14 + 15 + flags: `--cron <expr>` (default `"*/1 * * * *"`), `--stale-after <min>` (default 60), `--yes` to auto-accept prompts, plus the standard `--config` / `--root` / `--binding`. 16 + 17 + prompts are skipped in non-TTY environments (default-declined). 18 + 19 + also adds `--yes` to the CLI-wide arg parser.
+2
docs/01-indexing.md
··· 97 97 98 98 `ingest()` connects to Jetstream, streams events since the saved cursor, stops when caught up. Running every minute is fine — the next fire resumes where this one left off. Each cycle is bounded, so it can't blow past the Worker time limit. 99 99 100 + **Local dev:** wrangler's cron scheduler only runs in deployed production. For local dev use `pnpm contrail dev` — it runs `wrangler dev --test-scheduled`, fires `/__scheduled` on your configured cron interval, and prompts you to run backfill or refresh if the local DB looks stale on start. 101 + 100 102 ### Persistent (node / any long-lived server) 101 103 102 104 If your runtime can keep a socket open, skip the cron entirely:
-65
docs/08-examples.md
··· 1 - # Examples 2 - 3 - Every example lives in [`apps/`](https://github.com/flo-bit/contrail/tree/main/apps) and pins contrail as `workspace:*`. Clone the repo, `pnpm install`, and each one runs. 4 - 5 - ## `rsvp-atmo` — the reference deployment 6 - 7 - [`apps/rsvp-atmo`](https://github.com/flo-bit/contrail/tree/main/apps/rsvp-atmo) 8 - 9 - Cloudflare Workers + D1. Indexes `community.lexicon.calendar.event` and `rsvp`. Exposes the full spaces + community + realtime surface. Cron-driven Jetstream ingestion every minute. 10 - 11 - Use this if you're building on Workers and want a starting point that already has deploy config wired up. 12 - 13 - ```bash 14 - pnpm --filter rsvp-atmo dev # local wrangler + auto-cron 15 - pnpm --filter rsvp-atmo deploy # requires D1 database created 16 - pnpm --filter rsvp-atmo sync # discover + backfill against D1 17 - ``` 18 - 19 - ## `group-chat` — full app showcase 20 - 21 - [`apps/group-chat`](https://github.com/flo-bit/contrail/tree/main/apps/group-chat) 22 - 23 - SvelteKit + Cloudflare Workers. The one that exercises everything: permissioned spaces for private rooms, community-controlled DIDs for groups, client-side `contrail-sync` for reactive messages, Durable Object-hibernated WebSockets for realtime delivery, OAuth-based login. 24 - 25 - This is the canonical "what can contrail do" demo. If you're trying to understand how the pieces fit together end to end, read this app's code before anything else. 26 - 27 - ```bash 28 - pnpm --filter sveltekit-group-chat dev 29 - ``` 30 - 31 - ## `postgres` — Node + PG minimal 32 - 33 - [`apps/postgres`](https://github.com/flo-bit/contrail/tree/main/apps/postgres) 34 - 35 - The smallest possible Node deployment. Docker Compose for Postgres, three scripts: `sync` (discover + backfill), `ingest` (persistent Jetstream), `serve` (HTTP handler). Skips spaces/communities/realtime. 36 - 37 - Use this as a template if you're running on a normal server and don't need Cloudflare's bells. 38 - 39 - ```bash 40 - cd apps/postgres 41 - docker compose up -d 42 - pnpm sync 43 - pnpm serve 44 - ``` 45 - 46 - ## `cloudflare-workers` — minimal Workers 47 - 48 - [`apps/cloudflare-workers`](https://github.com/flo-bit/contrail/tree/main/apps/cloudflare-workers) 49 - 50 - The simplest working Worker. One collection (events), one HTTP handler, cron-driven ingest. No spaces, no communities. Good for reading top-to-bottom in one sitting to see what contrail does at minimum. 51 - 52 - ## `sveltekit-cloudflare-workers` — SvelteKit Statusphere 53 - 54 - [`apps/sveltekit-cloudflare-workers`](https://github.com/flo-bit/contrail/tree/main/apps/sveltekit-cloudflare-workers) 55 - 56 - A Statusphere-style SvelteKit app with OAuth login, contrail-indexed public records, and Cloudflare adapter. No spaces/communities — think "atproto blog or status post UI." Useful as a scaffold for public-only apps. 57 - 58 - ## Choosing a starting point 59 - 60 - | Need | Start from | 61 - |---|---| 62 - | "Just index some records" | `cloudflare-workers` or `postgres` | 63 - | "Index + SvelteKit UI, public only" | `sveltekit-cloudflare-workers` | 64 - | "Private rooms / group chat / full stack" | `group-chat` | 65 - | "Calendar-ish domain, Workers deploy" | `rsvp-atmo` |
+4 -2
apps/cloudflare-workers/README.md
··· 17 17 ```bash 18 18 pnpm install 19 19 npx wrangler d1 create contrail # copy database_id into wrangler.jsonc 20 - pnpm deploy # deploy the worker 21 20 pnpm contrail backfill --remote # discover + backfill historical events 21 + pnpm deploy # deploy the worker 22 22 ``` 23 23 24 24 then hit: ··· 30 30 ## local dev 31 31 32 32 ```bash 33 - pnpm dev # wrangler dev, cron fires every minute 33 + pnpm dev # wraps wrangler dev + auto-fires cron + prompts for backfill/refresh if needed 34 34 pnpm contrail backfill # backfill against the local D1 created by wrangler 35 35 ``` 36 + 37 + `pnpm dev` runs `contrail dev` under the hood. On start it inspects the local D1 and prompts to run backfill (if nothing's indexed yet) or refresh (if the ingest cursor is >1h old), then runs `wrangler dev --test-scheduled` with a 60s timer hitting `/__scheduled` so the cron actually fires locally. 36 38 37 39 ## extending 38 40
+1 -1
apps/cloudflare-workers/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": { 7 - "dev": "wrangler dev --test-scheduled", 7 + "dev": "contrail dev", 8 8 "deploy": "wrangler deploy" 9 9 }, 10 10 "dependencies": {
+7 -5
apps/sveltekit-cloudflare-workers/package.json
··· 5 5 "type": "module", 6 6 "scripts": { 7 7 "dev": "vite dev", 8 - "build": "tsx scripts/generate.ts && lex-cli generate && vite build && tsx scripts/append-scheduled.ts", 9 - "generate": "tsx scripts/generate.ts", 10 - "generate:pull": "tsx scripts/generate.ts && lex-cli pull && tsx scripts/generate.ts && lex-cli pull && lex-cli generate", 11 - "sync": "tsx scripts/sync.ts", 12 - "sync:remote": "tsx scripts/sync.ts --remote", 8 + "build": "contrail-lex generate && contrail-lex types && vite build && tsx scripts/append-scheduled.ts", 9 + "generate": "contrail-lex generate", 10 + "generate:pull": "contrail-lex all", 11 + "backfill": "contrail backfill", 12 + "backfill:remote": "contrail backfill --remote", 13 + "refresh": "contrail refresh", 14 + "refresh:remote": "contrail refresh --remote", 13 15 "preview": "vite preview", 14 16 "prepare": "svelte-kit sync || echo ''", 15 17 "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
-13
apps/sveltekit-cloudflare-workers/scripts/generate.ts
··· 1 - import { join, dirname } from 'path'; 2 - import { fileURLToPath } from 'url'; 3 - import { config } from '../src/lib/contrail.config'; 4 - import { generateLexicons } from '@atmo-dev/contrail-lexicons'; 5 - 6 - const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..'); 7 - 8 - generateLexicons({ 9 - config, 10 - rootDir: ROOT_DIR, 11 - outputDir: join(ROOT_DIR, 'lexicons', 'generated'), 12 - writeRuntimeFiles: true 13 - });
-68
apps/sveltekit-cloudflare-workers/scripts/sync.ts
··· 1 - /** 2 - * Discover users from relays and backfill their records from PDS. 3 - * 4 - * Usage: 5 - * pnpm sync # local D1 6 - * pnpm sync:remote # prod D1 7 - */ 8 - import { Contrail } from '@atmo-dev/contrail'; 9 - import { config } from '../src/lib/contrail.config'; 10 - import { getPlatformProxy } from 'wrangler'; 11 - 12 - function elapsed(start: number): string { 13 - const ms = Date.now() - start; 14 - if (ms < 1000) return `${ms}ms`; 15 - if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`; 16 - const mins = Math.floor(ms / 60_000); 17 - const secs = ((ms % 60_000) / 1000).toFixed(0); 18 - return `${mins}m ${secs}s`; 19 - } 20 - 21 - async function main() { 22 - const remote = process.argv.includes('--remote'); 23 - const syncStart = Date.now(); 24 - 25 - console.log(`=== Sync (${remote ? 'remote/prod' : 'local'} D1) ===\n`); 26 - 27 - const { env, dispose } = await getPlatformProxy<{ DB: D1Database }>({ 28 - environment: remote ? 'production' : undefined 29 - }); 30 - 31 - const contrail = new Contrail({ ...config, db: env.DB }); 32 - 33 - try { 34 - await contrail.init(); 35 - 36 - console.log('--- Discovery ---'); 37 - const discoveryStart = Date.now(); 38 - const discovered = await contrail.discover(); 39 - console.log(` Done: ${discovered.length} users in ${elapsed(discoveryStart)}\n`); 40 - 41 - console.log('--- Backfill ---'); 42 - const backfillStart = Date.now(); 43 - const total = await contrail.backfill({ 44 - concurrency: 100, 45 - onProgress: ({ records, usersComplete, usersTotal, usersFailed }) => { 46 - const secs = (Date.now() - backfillStart) / 1000; 47 - const rate = secs > 0 ? Math.round(records / secs) : 0; 48 - const failStr = usersFailed > 0 ? ` | ${usersFailed} failed` : ''; 49 - process.stdout.write( 50 - `\r ${records} records | ${usersComplete}/${usersTotal} users | ${rate}/s | ${elapsed(backfillStart)}${failStr} ` 51 - ); 52 - } 53 - }); 54 - process.stdout.write('\n'); 55 - console.log(` Done: ${total} records in ${elapsed(backfillStart)}\n`); 56 - 57 - console.log(`=== Finished in ${elapsed(syncStart)} ===`); 58 - console.log(` Discovered: ${discovered.length} users`); 59 - console.log(` Backfilled: ${total} records`); 60 - } finally { 61 - await dispose(); 62 - } 63 - } 64 - 65 - main().catch((err) => { 66 - console.error(err); 67 - process.exit(1); 68 - });
+134 -2
packages/contrail/src/cli.ts
··· 5 5 * Usage: 6 6 * contrail backfill [--remote] [--binding DB] [--config path] 7 7 * contrail refresh [--remote] [--ignore-window 60s] [--by-collection] 8 + * contrail dev [--binding DB] [--cron '*\/1 * * * *'] 8 9 * 9 10 * Config auto-detects at contrail.config.ts, src/contrail.config.ts, 10 11 * src/lib/contrail.config.ts, or app/contrail.config.ts (first match wins). 11 12 */ 13 + import { spawn } from "node:child_process"; 14 + import { createInterface } from "node:readline/promises"; 15 + import { stdin as input, stdout as output } from "node:process"; 12 16 import { findConfigFile, loadConfig, CONFIG_CANDIDATES_MESSAGE } from "./cli-config.js"; 13 17 import { backfillAll, refresh } from "./workers/backfill.js"; 18 + import { Contrail } from "./contrail.js"; 14 19 import type { CollectionStats, RefreshResult } from "./core/refresh.js"; 20 + import type { Database } from "./core/types.js"; 15 21 16 - type Subcommand = "backfill" | "refresh" | "help"; 22 + type Subcommand = "backfill" | "refresh" | "dev" | "help"; 17 23 18 24 const USAGE = `contrail <subcommand> [options] 19 25 20 26 Subcommands: 21 27 backfill One-time bulk load from each known DID's PDS (resumable) 22 28 refresh Fresh sweep: reconcile PDS vs DB, report missing + stale 29 + dev Local wrangler dev + auto-trigger cron + backfill/refresh prompts 23 30 help Print this message 24 31 25 32 Options (backfill): ··· 37 44 --concurrency <n> Passed to contrail.refresh(). Default: 50. 38 45 --ignore-window <s> Seconds of grace for stale-update detection. Default: 60. 39 46 --by-collection Print per-collection stats, not just totals. 47 + 48 + Options (dev): 49 + --config <path> Path to Contrail config file (TS or JS). 50 + --root <path> Project root for auto-detection. Default: CWD. 51 + --binding <name> D1 binding name in wrangler.jsonc. Default: "DB". 52 + --cron <expr> Cron expression to fire against /__scheduled. Default: "*/1 * * * *". 53 + --stale-after <min> Prompt to run refresh if the ingest cursor is older than this. Default: 60. 54 + --yes Accept all prompts without asking (CI-friendly). 40 55 `; 41 56 42 57 interface Args { ··· 48 63 concurrency?: number; 49 64 ignoreWindowMs?: number; 50 65 byCollection: boolean; 66 + cron?: string; 67 + staleAfterMin?: number; 68 + yes: boolean; 51 69 } 52 70 53 71 function parseArgs(argv: string[]): Args { ··· 60 78 let concurrency: number | undefined; 61 79 let ignoreWindowMs: number | undefined; 62 80 let byCollection = false; 81 + let cron: string | undefined; 82 + let staleAfterMin: number | undefined; 83 + let yes = false; 63 84 for (let i = 0; i < args.length; i++) { 64 85 const a = args[i]; 65 86 if (a === "--config") config = args[++i]; ··· 69 90 else if (a === "--concurrency") concurrency = parseInt(args[++i], 10); 70 91 else if (a === "--ignore-window") ignoreWindowMs = parseInt(args[++i], 10) * 1000; 71 92 else if (a === "--by-collection") byCollection = true; 93 + else if (a === "--cron") cron = args[++i]; 94 + else if (a === "--stale-after") staleAfterMin = parseInt(args[++i], 10); 95 + else if (a === "--yes" || a === "-y") yes = true; 72 96 else if (a === "-h" || a === "--help") 73 97 return { 74 98 cmd: "help", ··· 79 103 concurrency, 80 104 ignoreWindowMs, 81 105 byCollection, 106 + cron, 107 + staleAfterMin, 108 + yes, 82 109 }; 83 110 } 84 - return { cmd, config, root, remote, binding, concurrency, ignoreWindowMs, byCollection }; 111 + return { cmd, config, root, remote, binding, concurrency, ignoreWindowMs, byCollection, cron, staleAfterMin, yes }; 85 112 } 86 113 87 114 function resolveConfigPath(opts: Args): string | null { ··· 93 120 ); 94 121 } 95 122 return p; 123 + } 124 + 125 + async function promptYesNo(question: string, defaultYes: boolean, autoYes: boolean): Promise<boolean> { 126 + if (autoYes) return true; 127 + // Non-TTY → default-decline; the user isn't there to answer. 128 + if (!input.isTTY) return false; 129 + const rl = createInterface({ input, output }); 130 + try { 131 + const hint = defaultYes ? "[Y/n]" : "[y/N]"; 132 + const ans = (await rl.question(`${question} ${hint} `)).trim().toLowerCase(); 133 + if (ans === "") return defaultYes; 134 + return ans === "y" || ans === "yes"; 135 + } finally { 136 + rl.close(); 137 + } 96 138 } 97 139 98 140 function formatStats(s: CollectionStats): string { ··· 119 161 (result.usersFailed ? `, ${result.usersFailed} failed` : "") + 120 162 ` in ${(result.elapsedMs / 1000).toFixed(1)}s`); 121 163 console.log(` (ignore window: ${(result.ignoreWindowMs / 1000).toFixed(0)}s)`); 164 + } 165 + 166 + async function cmdDev(opts: Args): Promise<number> { 167 + const configPath = resolveConfigPath(opts); 168 + if (!configPath) return 1; 169 + const config = await loadConfig(configPath); 170 + 171 + // Pre-flight: connect to local D1 via wrangler's platform proxy, inspect 172 + // state, prompt. Then dispose before starting wrangler dev so the two 173 + // miniflare processes don't fight over the sqlite file. 174 + const { getPlatformProxy } = await import("wrangler"); 175 + const { env, dispose } = await getPlatformProxy(); 176 + const db = (env as Record<string, unknown>)[opts.binding] as Database | undefined; 177 + 178 + if (db) { 179 + const contrail = new Contrail(config); 180 + await contrail.init(db); 181 + 182 + const hasBackfilled = await db 183 + .prepare("SELECT 1 FROM backfills WHERE completed = 1 LIMIT 1") 184 + .first(); 185 + 186 + if (!hasBackfilled) { 187 + console.log("no backfilled users in the local DB yet."); 188 + if (await promptYesNo("run backfill now? (takes a few minutes)", true, opts.yes)) { 189 + await contrail.backfillAll({ concurrency: opts.concurrency ?? 100 }, db); 190 + } 191 + } else { 192 + const staleAfterMs = (opts.staleAfterMin ?? 60) * 60_000; 193 + const row = await db 194 + .prepare("SELECT time_us FROM cursor WHERE id = 1") 195 + .first<{ time_us: number }>(); 196 + if (row?.time_us) { 197 + const ageMs = Date.now() - Math.floor(row.time_us / 1000); 198 + if (ageMs > staleAfterMs) { 199 + const hrs = (ageMs / 3_600_000).toFixed(1); 200 + console.log(`ingest cursor is ${hrs}h old — you may have missed events.`); 201 + if (await promptYesNo("run refresh first?", true, opts.yes)) { 202 + await contrail.refresh({ ignoreWindowMs: opts.ignoreWindowMs }, db); 203 + } 204 + } 205 + } 206 + } 207 + } 208 + 209 + await dispose(); 210 + 211 + // Start wrangler dev + fire /__scheduled on a loop so the cron actually 212 + // runs in local dev (wrangler's cron scheduler only works in deployed 213 + // production; --test-scheduled enables the manual-trigger endpoint). 214 + const cron = opts.cron ?? "*/1 * * * *"; 215 + const cronUrl = `http://localhost:8787/__scheduled?cron=${encodeURIComponent(cron)}`; 216 + 217 + const wrangler = spawn("npx", ["wrangler", "dev", "--test-scheduled"], { 218 + stdio: "inherit", 219 + shell: process.platform === "win32", 220 + cwd: opts.root, 221 + }); 222 + 223 + // Give wrangler a few seconds to bind the port, then start the cron loop. 224 + const kickoff = setTimeout(() => { 225 + fetch(cronUrl).catch(() => {}); // fire-and-forget; first run 226 + console.log(`\nauto-ingest: firing ${cronUrl} every 60s\n`); 227 + }, 3_000); 228 + 229 + const interval = setInterval(() => { 230 + fetch(cronUrl).catch(() => {}); // wrangler may be restarting; swallow 231 + }, 60_000); 232 + 233 + const cleanup = () => { 234 + clearTimeout(kickoff); 235 + clearInterval(interval); 236 + try { 237 + wrangler.kill("SIGINT"); 238 + } catch { 239 + /* ignore */ 240 + } 241 + }; 242 + process.on("SIGINT", cleanup); 243 + process.on("SIGTERM", cleanup); 244 + 245 + return new Promise<number>((resolve) => { 246 + wrangler.on("exit", (code) => { 247 + clearTimeout(kickoff); 248 + clearInterval(interval); 249 + resolve(code ?? 0); 250 + }); 251 + }); 122 252 } 123 253 124 254 async function main(): Promise<number> { ··· 156 286 printRefreshReport(result, opts.byCollection); 157 287 return 0; 158 288 } 289 + 290 + if (opts.cmd === "dev") return cmdDev(opts); 159 291 160 292 console.error(USAGE); 161 293 return 1;
+10 -2
packages/lexicons/src/generate.ts
··· 247 247 properties[rd.refName] = { type: "ref", ref: `#ref${cap(rd.refName)}Record` }; 248 248 } 249 249 } 250 - return { type: "object", required: ["uri", "cid", "value"], properties }; 250 + // Required list is a superset of atproto's `#record` (`uri`, `cid`, `value`). 251 + // Contrail always populates the extras too, so including them here gives 252 + // contrail-aware clients strong typing without breaking atproto clients 253 + // that only check the standard three. 254 + return { 255 + type: "object", 256 + required: ["uri", "cid", "value", "did", "collection", "rkey", "time_us"], 257 + properties, 258 + }; 251 259 } 252 260 253 261 function buildHydrateDefs(relationDefs: RelationDef[]): Record<string, any> { ··· 738 746 writeLexicon(`${ns}.${shortName}.getRecord`, { 739 747 lexicon: 1, id: `${ns}.${shortName}.getRecord`, 740 748 defs: { 741 - main: { type: "query", description: `Get a single ${collection} record by AT URI`, parameters: { type: "params", required: ["uri"], properties: getParams }, output: { encoding: "application/json", schema: { type: "object", required: ["uri", "value"], properties: { ...buildRecordDef(collectionRef, countFields, relationDefs, referenceDefs).properties, profiles: { type: "array", items: { type: "ref", ref: "#profileEntry" } } } } } }, 749 + main: { type: "query", description: `Get a single ${collection} record by AT URI`, parameters: { type: "params", required: ["uri"], properties: getParams }, output: { encoding: "application/json", schema: { type: "object", required: ["uri", "value", "did", "collection", "rkey", "time_us"], properties: { ...buildRecordDef(collectionRef, countFields, relationDefs, referenceDefs).properties, profiles: { type: "array", items: { type: "ref", ref: "#profileEntry" } } } } } }, 742 750 ...hydrateDefs, ...refDefs, ...profileDefs(), 743 751 }, 744 752 });
+2 -1
apps/sveltekit-cloudflare-workers/src/routes/+layout.server.ts
··· 14 14 }); 15 15 16 16 if (!res.ok) return { did: locals.did, profile: null }; 17 + const entry = res.data.profiles?.[0]; 17 18 18 19 return { 19 20 did: locals.did, 20 - profile: extractProfile(res.data) 21 + profile: entry ? extractProfile(entry) : null 21 22 }; 22 23 } catch { 23 24 return { did: locals.did, profile: null };
+4 -2
apps/sveltekit-cloudflare-workers/src/routes/+page.svelte
··· 39 39 if (profiles[did]) return; 40 40 try { 41 41 const res = await contrailClient.get('statusphere.app.getProfile', { 42 - params: { actor: did } 42 + params: { actor: did as `did:${string}:${string}` } 43 43 }); 44 - profiles[did] = extractProfile(res.data); 44 + if (!res.ok) return; 45 + const entry = res.data.profiles?.[0]; 46 + if (entry) profiles[did] = extractProfile(entry); 45 47 } catch { 46 48 // ignore fetch errors 47 49 }
+2
apps/sveltekit-cloudflare-workers/src/lib/atproto/server/repo.remote.ts
··· 33 33 } 34 34 }); 35 35 36 + if (!response.ok) error(502, 'putRecord failed'); 37 + 36 38 // Immediately index the new/updated record in contrail 37 39 const { platform } = getRequestEvent(); 38 40 const db = platform?.env?.DB;
+5 -1
apps/cloudflare-workers/lexicons/generated/com/example/event/getRecord.json
··· 28 28 "type": "object", 29 29 "required": [ 30 30 "uri", 31 - "value" 31 + "value", 32 + "did", 33 + "collection", 34 + "rkey", 35 + "time_us" 32 36 ], 33 37 "properties": { 34 38 "uri": {
+5 -1
apps/cloudflare-workers/lexicons/generated/com/example/event/listRecords.json
··· 89 89 "required": [ 90 90 "uri", 91 91 "cid", 92 - "value" 92 + "value", 93 + "did", 94 + "collection", 95 + "rkey", 96 + "time_us" 93 97 ], 94 98 "properties": { 95 99 "uri": {
+5 -1
apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/status/getRecord.json
··· 28 28 "type": "object", 29 29 "required": [ 30 30 "uri", 31 - "value" 31 + "value", 32 + "did", 33 + "collection", 34 + "rkey", 35 + "time_us" 32 36 ], 33 37 "properties": { 34 38 "uri": {
+5 -1
apps/sveltekit-cloudflare-workers/lexicons/generated/statusphere/app/status/listRecords.json
··· 90 90 "required": [ 91 91 "uri", 92 92 "cid", 93 - "value" 93 + "value", 94 + "did", 95 + "collection", 96 + "rkey", 97 + "time_us" 94 98 ], 95 99 "properties": { 96 100 "uri": {
+5 -1
apps/group-chat/lexicons/generated/tools/atmo/chat/channel/getRecord.json
··· 37 37 "type": "object", 38 38 "required": [ 39 39 "uri", 40 - "value" 40 + "value", 41 + "did", 42 + "collection", 43 + "rkey", 44 + "time_us" 41 45 ], 42 46 "properties": { 43 47 "uri": {
+5 -1
apps/group-chat/lexicons/generated/tools/atmo/chat/channel/listRecords.json
··· 100 100 "required": [ 101 101 "uri", 102 102 "cid", 103 - "value" 103 + "value", 104 + "did", 105 + "collection", 106 + "rkey", 107 + "time_us" 104 108 ], 105 109 "properties": { 106 110 "uri": {
+5 -1
apps/group-chat/lexicons/generated/tools/atmo/chat/message/getRecord.json
··· 37 37 "type": "object", 38 38 "required": [ 39 39 "uri", 40 - "value" 40 + "value", 41 + "did", 42 + "collection", 43 + "rkey", 44 + "time_us" 41 45 ], 42 46 "properties": { 43 47 "uri": {
+5 -1
apps/group-chat/lexicons/generated/tools/atmo/chat/message/listRecords.json
··· 99 99 "required": [ 100 100 "uri", 101 101 "cid", 102 - "value" 102 + "value", 103 + "did", 104 + "collection", 105 + "rkey", 106 + "time_us" 103 107 ], 104 108 "properties": { 105 109 "uri": {
+5 -1
apps/group-chat/lexicons/generated/tools/atmo/chat/server/getRecord.json
··· 37 37 "type": "object", 38 38 "required": [ 39 39 "uri", 40 - "value" 40 + "value", 41 + "did", 42 + "collection", 43 + "rkey", 44 + "time_us" 41 45 ], 42 46 "properties": { 43 47 "uri": {
+5 -1
apps/group-chat/lexicons/generated/tools/atmo/chat/server/listRecords.json
··· 95 95 "required": [ 96 96 "uri", 97 97 "cid", 98 - "value" 98 + "value", 99 + "did", 100 + "collection", 101 + "rkey", 102 + "time_us" 99 103 ], 100 104 "properties": { 101 105 "uri": {