Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

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

feat: better oauth scope management

Hugo 111269ca dfcc6517

+675 -21
+10
app/islands/DeliveryLog.tsx
··· 77 77 }); 78 78 if (!res.ok) { 79 79 const data = await res.json(); 80 + if (data.error === "scope_insufficient") { 81 + const returnTo = window.location.pathname; 82 + window.location.href = `/auth/login?scope=full&returnTo=${encodeURIComponent(returnTo)}`; 83 + return; 84 + } 80 85 setError(data.error || "Failed to update"); 81 86 } else { 82 87 const newActive = !isActive; ··· 103 108 }); 104 109 if (!res.ok) { 105 110 const data = await res.json(); 111 + if (data.error === "scope_insufficient") { 112 + const returnTo = window.location.pathname; 113 + window.location.href = `/auth/login?scope=full&returnTo=${encodeURIComponent(returnTo)}`; 114 + return; 115 + } 106 116 setError(data.error || "Failed to update"); 107 117 } else { 108 118 const newDryRun = !isDryRun;
+5 -1
app/routes/api/automations/[rkey].test.ts
··· 49 49 const mockPutRecord = vi.mocked(putRecord); 50 50 const mockDeleteRecord = vi.mocked(deleteRecord); 51 51 52 - const TEST_USER = { did: "did:plc:testuser", handle: "test.bsky.social" }; 52 + const TEST_USER = { 53 + did: "did:plc:testuser", 54 + handle: "test.bsky.social", 55 + scope: "atproto transition:generic", 56 + }; 53 57 const TEST_AUTO = { 54 58 uri: "at://did:plc:testuser/run.airglow.automation/rk1", 55 59 did: "did:plc:testuser",
+10
app/routes/api/automations/[rkey].ts
··· 37 37 validateWebhookHeaders, 38 38 } from "@/actions/validation.js"; 39 39 import { notifyAutomationChange } from "@/jetstream/consumer.js"; 40 + import { actionsRequireFullScope, scopeCoversFullWrite } from "@/auth/client.js"; 40 41 41 42 function findAutomation(did: string, rkey: string) { 42 43 return db.query.automations.findFirst({ ··· 447 448 ...(a.comment ? { comment: a.comment } : {}), 448 449 }; 449 450 }); 451 + } 452 + 453 + // Block disabling dry run when scope doesn't cover write actions 454 + if ( 455 + !dryRun && 456 + actionsRequireFullScope(localActions.map((a) => a.$type)) && 457 + !scopeCoversFullWrite(user.scope) 458 + ) { 459 + return c.json({ error: "scope_insufficient", requiredScope: "full" }, 403); 450 460 } 451 461 452 462 // Update on PDS
+5 -1
app/routes/api/automations/[rkey]/logs.test.ts
··· 22 22 import { db } from "@/db/index.js"; 23 23 import { automations, deliveryLogs } from "@/db/schema.js"; 24 24 25 - const TEST_USER = { did: "did:plc:testuser", handle: "test.bsky.social" }; 25 + const TEST_USER = { 26 + did: "did:plc:testuser", 27 + handle: "test.bsky.social", 28 + scope: "atproto transition:generic", 29 + }; 26 30 const TEST_AUTO = { 27 31 uri: "at://did:plc:testuser/run.airglow.automation/rk1", 28 32 did: "did:plc:testuser",
+5 -1
app/routes/api/automations/index.test.ts
··· 48 48 const mockCreateRecord = vi.mocked(createRecord); 49 49 const mockDeleteRecord = vi.mocked(deleteRecord); 50 50 51 - const TEST_USER = { did: "did:plc:testuser", handle: "test.bsky.social" }; 51 + const TEST_USER = { 52 + did: "did:plc:testuser", 53 + handle: "test.bsky.social", 54 + scope: "atproto transition:generic", 55 + }; 52 56 53 57 function createTestApp() { 54 58 const app = new Hono<{ Variables: { user: typeof TEST_USER } }>();
+29 -7
app/routes/auth/callback.tsx
··· 49 49 const { session, state } = await client.callback(params); 50 50 51 51 const did = session.did; 52 - const handle = await resolveDidToHandle(state || did); 53 52 54 - // Upsert user 55 - await db.insert(users).values({ did, handle, createdAt: new Date() }).onConflictDoUpdate({ 56 - target: users.did, 57 - set: { handle }, 58 - }); 53 + // Parse state: plain string (normal login) or JSON (re-auth with returnTo) 54 + let handle: string; 55 + let returnTo: string | null = null; 56 + if (state?.startsWith("{")) { 57 + try { 58 + const parsed = JSON.parse(state) as { handle?: string; returnTo?: string }; 59 + handle = await resolveDidToHandle(parsed.handle || did); 60 + returnTo = parsed.returnTo || null; 61 + } catch { 62 + handle = await resolveDidToHandle(did); 63 + } 64 + } else { 65 + handle = await resolveDidToHandle(state || did); 66 + } 67 + 68 + // Read granted scope from token 69 + const tokenInfo = await session.getTokenInfo(false); 70 + const grantedScope = tokenInfo.scope; 71 + 72 + // Upsert user with scope 73 + await db 74 + .insert(users) 75 + .values({ did, handle, scope: grantedScope, createdAt: new Date() }) 76 + .onConflictDoUpdate({ 77 + target: users.did, 78 + set: { handle, scope: grantedScope }, 79 + }); 59 80 60 81 // Set session cookie 61 82 await setSignedCookie(c, COOKIE_NAME, did, config.cookieSecret, { ··· 66 87 maxAge: 60 * 60 * 24 * 30, // 30 days 67 88 }); 68 89 69 - return c.redirect("/dashboard"); 90 + const destination = returnTo?.startsWith("/") ? returnTo : "/dashboard"; 91 + return c.redirect(destination); 70 92 } catch (err) { 71 93 console.error("OAuth callback error:", err); 72 94 return c.render(
+26 -6
app/routes/auth/login.tsx
··· 1 1 import { createRoute } from "honox/factory"; 2 - import { getOAuthClient, resolveHandle, rewritePdsUrl } from "@/auth/client.js"; 2 + import { 3 + getOAuthClient, 4 + resolveHandle, 5 + rewritePdsUrl, 6 + OAUTH_SCOPE_BASIC, 7 + OAUTH_SCOPE_FULL, 8 + } from "@/auth/client.js"; 3 9 import { getSessionUser } from "@/auth/middleware.js"; 4 10 import { AppShell } from "../../components/Layout/AppShell/index.js"; 5 11 import { Header } from "../../components/Layout/Header/index.js"; ··· 19 25 20 26 export default createRoute(async (c) => { 21 27 const user = await getSessionUser(c); 22 - if (user) return c.redirect("/dashboard"); 28 + const scopeParam = c.req.query("scope"); 29 + // Skip redirect when re-authing for scope upgrade 30 + if (user && !scopeParam) return c.redirect("/dashboard"); 23 31 24 32 const error = c.req.query("error"); 33 + const prefillHandle = c.req.query("handle") ?? ""; 34 + const returnTo = c.req.query("returnTo"); 35 + 36 + const formParams = new URLSearchParams(); 37 + if (scopeParam) formParams.set("scope", scopeParam); 38 + if (returnTo) formParams.set("returnTo", returnTo); 39 + const formAction = "/auth/login" + (formParams.size ? `?${formParams}` : ""); 25 40 26 41 return c.render( 27 42 <AppShell header={<Header actions={<ThemeToggle />} />}> ··· 35 50 <p>Use your AT Protocol handle or DID to authenticate.</p> 36 51 </div> 37 52 {error && <Alert variant="error">{error}</Alert>} 38 - <form method="post" action="/auth/login"> 53 + <form method="post" action={formAction}> 39 54 <Stack gap={4}> 40 55 <div class={formGroup}> 41 56 <label class={loginLabel} for="handle"> ··· 47 62 type="text" 48 63 name="handle" 49 64 placeholder="you.bsky.social" 65 + value={prefillHandle} 50 66 required 51 67 autofocus 52 68 /> ··· 82 98 const input = handle.trim().replace(/^@/, ""); 83 99 const did = await resolveHandle(input); 84 100 const client = await getOAuthClient(); 85 - const url = await client.authorize(did, { 86 - state: input, 87 - }); 101 + 102 + const scopeParam = c.req.query("scope"); 103 + const scope = scopeParam === "full" ? OAUTH_SCOPE_FULL : OAUTH_SCOPE_BASIC; 104 + const returnTo = c.req.query("returnTo"); 105 + const state = returnTo ? JSON.stringify({ handle: input, returnTo }) : input; 106 + 107 + const url = await client.authorize(did, { state, scope }); 88 108 // Rewrite PDS hostname for browser → goes through Vite proxy in dev 89 109 const redirectUrl = rewritePdsUrl(url.toString(), "browser"); 90 110 return c.redirect(redirectUrl);
+18
app/routes/dashboard/automations/[rkey].tsx
··· 2 2 import { eq, and, desc } from "drizzle-orm"; 3 3 import { ArrowLeft, Copy, ExternalLink, Pencil, Filter, Database, Zap } from "../../../icons.js"; 4 4 import { opLabels, actionTypeLabels, operationLabels } from "@/automations/labels.js"; 5 + import { actionsRequireFullScope, scopeCoversFullWrite } from "@/auth/client.js"; 5 6 import { db } from "@/db/index.js"; 6 7 import { automations, deliveryLogs } from "@/db/schema.js"; 7 8 import { AppShell } from "../../../components/Layout/AppShell/index.js"; ··· 11 12 import { Card } from "../../../components/Card/index.js"; 12 13 import { Badge } from "../../../components/Badge/index.js"; 13 14 import { Button } from "../../../components/Button/index.js"; 15 + import { Alert } from "../../../components/Alert/index.js"; 14 16 import { DescriptionList } from "../../../components/DescriptionList/index.js"; 15 17 import { CodeBlock, InlineCode } from "../../../components/CodeBlock/index.js"; 16 18 import { NsidCode } from "../../../components/NsidCode/index.js"; ··· 53 55 }); 54 56 const hasMore = rawLogs.length > 50; 55 57 const logs = rawLogs.slice(0, 50); 58 + 59 + const needsScopeUpgrade = 60 + actionsRequireFullScope(auto.actions.map((a) => a.$type)) && !scopeCoversFullWrite(user.scope); 56 61 57 62 return c.render( 58 63 <AppShell header={<Header user={user} actions={<ThemeToggle />} />}> ··· 82 87 </div> 83 88 } 84 89 /> 90 + 91 + {needsScopeUpgrade && ( 92 + <Alert variant="warning"> 93 + This automation includes actions that write to your account. To disable dry run mode, 94 + you need to{" "} 95 + <a 96 + href={`/auth/login?scope=full&handle=${encodeURIComponent(user.handle)}&returnTo=${encodeURIComponent(`/dashboard/automations/${rkey}`)}`} 97 + > 98 + re-authorize with broader permissions 99 + </a> 100 + . 101 + </Alert> 102 + )} 85 103 86 104 <Stack gap={6}> 87 105 <Card variant="flat">
+23 -4
lib/auth/client.ts
··· 16 16 import { sessionStore, stateStore } from "./storage.js"; 17 17 18 18 const KEY_PATH = resolve("./data/oauth-key.json"); 19 - const OAUTH_SCOPE = "atproto transition:generic"; 19 + 20 + /** Maximum scope declared in client metadata (superset of all tiers). */ 21 + export const OAUTH_SCOPE_MAX = "atproto transition:generic repo:run.airglow.automation"; 22 + /** Tier 1: minimal scope for sign-in and webhook-only automations. */ 23 + export const OAUTH_SCOPE_BASIC = "atproto repo:run.airglow.automation"; 24 + /** Tier 2: full write access for bsky-post, record, patch-record actions. */ 25 + export const OAUTH_SCOPE_FULL = OAUTH_SCOPE_MAX; 26 + 27 + const FULL_SCOPE_ACTION_TYPES = new Set(["record", "bsky-post", "patch-record"]); 28 + 29 + /** Returns true if any action type requires full (transition:generic) scope. */ 30 + export function actionsRequireFullScope(actionTypes: string[]): boolean { 31 + return actionTypes.some((t) => FULL_SCOPE_ACTION_TYPES.has(t)); 32 + } 33 + 34 + /** Returns true if the granted scope covers full write access. */ 35 + export function scopeCoversFullWrite(scope: string | null): boolean { 36 + if (!scope) return false; 37 + return scope.split(" ").includes("transition:generic"); 38 + } 20 39 21 40 const pdsUrl = config.pdsUrl; 22 41 const isLocalDev = ··· 146 165 const redirectUri = config.publicUrl.replace("localhost", "127.0.0.1") + "/auth/callback"; 147 166 client = new NodeOAuthClient({ 148 167 clientMetadata: { 149 - client_id: `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(OAUTH_SCOPE)}`, 168 + client_id: `http://localhost?redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(OAUTH_SCOPE_MAX)}`, 150 169 client_name: "Airglow", 151 170 client_uri: config.publicUrl, 152 171 redirect_uris: [redirectUri], 153 - scope: OAUTH_SCOPE, 172 + scope: OAUTH_SCOPE_MAX, 154 173 response_types: ["code"], 155 174 grant_types: ["authorization_code", "refresh_token"], 156 175 application_type: "native", ··· 170 189 client_name: "Airglow", 171 190 client_uri: config.publicUrl, 172 191 redirect_uris: [`${config.publicUrl}/auth/callback`], 173 - scope: OAUTH_SCOPE, 192 + scope: OAUTH_SCOPE_MAX, 174 193 response_types: ["code"], 175 194 grant_types: ["authorization_code", "refresh_token"], 176 195 application_type: "web",
+2 -1
lib/auth/middleware.ts
··· 11 11 id: number; 12 12 did: string; 13 13 handle: string; 14 + scope: string | null; 14 15 }; 15 16 16 17 export async function getSessionUser(c: Context): Promise<SessionUser | null> { ··· 22 23 }); 23 24 if (!user) return null; 24 25 25 - return { id: user.id, did: user.did, handle: user.handle }; 26 + return { id: user.id, did: user.did, handle: user.handle, scope: user.scope }; 26 27 } 27 28 28 29 /** Middleware that requires authentication. Redirects to /auth/login if not signed in. */
+1
lib/db/migrations/0004_certain_marvel_boy.sql
··· 1 + ALTER TABLE `users` ADD `scope` text;
+533
lib/db/migrations/meta/0004_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "00a1b843-a8c9-42da-be5f-23dbc64261c9", 5 + "prevId": "540ef8f8-f159-4c01-bef8-ecb6ceb13ae5", 6 + "tables": { 7 + "automations": { 8 + "name": "automations", 9 + "columns": { 10 + "uri": { 11 + "name": "uri", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "did": { 18 + "name": "did", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "rkey": { 25 + "name": "rkey", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "name": { 32 + "name": "name", 33 + "type": "text", 34 + "primaryKey": false, 35 + "notNull": true, 36 + "autoincrement": false 37 + }, 38 + "description": { 39 + "name": "description", 40 + "type": "text", 41 + "primaryKey": false, 42 + "notNull": false, 43 + "autoincrement": false 44 + }, 45 + "lexicon": { 46 + "name": "lexicon", 47 + "type": "text", 48 + "primaryKey": false, 49 + "notNull": true, 50 + "autoincrement": false 51 + }, 52 + "operation": { 53 + "name": "operation", 54 + "type": "text", 55 + "primaryKey": false, 56 + "notNull": true, 57 + "autoincrement": false, 58 + "default": "'[\"create\"]'" 59 + }, 60 + "actions": { 61 + "name": "actions", 62 + "type": "text", 63 + "primaryKey": false, 64 + "notNull": true, 65 + "autoincrement": false, 66 + "default": "'[]'" 67 + }, 68 + "fetches": { 69 + "name": "fetches", 70 + "type": "text", 71 + "primaryKey": false, 72 + "notNull": true, 73 + "autoincrement": false, 74 + "default": "'[]'" 75 + }, 76 + "conditions": { 77 + "name": "conditions", 78 + "type": "text", 79 + "primaryKey": false, 80 + "notNull": true, 81 + "autoincrement": false, 82 + "default": "'[]'" 83 + }, 84 + "active": { 85 + "name": "active", 86 + "type": "integer", 87 + "primaryKey": false, 88 + "notNull": true, 89 + "autoincrement": false, 90 + "default": false 91 + }, 92 + "dry_run": { 93 + "name": "dry_run", 94 + "type": "integer", 95 + "primaryKey": false, 96 + "notNull": true, 97 + "autoincrement": false, 98 + "default": false 99 + }, 100 + "indexed_at": { 101 + "name": "indexed_at", 102 + "type": "integer", 103 + "primaryKey": false, 104 + "notNull": true, 105 + "autoincrement": false 106 + } 107 + }, 108 + "indexes": {}, 109 + "foreignKeys": {}, 110 + "compositePrimaryKeys": {}, 111 + "uniqueConstraints": {}, 112 + "checkConstraints": {} 113 + }, 114 + "delivery_logs": { 115 + "name": "delivery_logs", 116 + "columns": { 117 + "id": { 118 + "name": "id", 119 + "type": "integer", 120 + "primaryKey": true, 121 + "notNull": true, 122 + "autoincrement": true 123 + }, 124 + "automation_uri": { 125 + "name": "automation_uri", 126 + "type": "text", 127 + "primaryKey": false, 128 + "notNull": true, 129 + "autoincrement": false 130 + }, 131 + "action_index": { 132 + "name": "action_index", 133 + "type": "integer", 134 + "primaryKey": false, 135 + "notNull": true, 136 + "autoincrement": false, 137 + "default": 0 138 + }, 139 + "event_time_us": { 140 + "name": "event_time_us", 141 + "type": "integer", 142 + "primaryKey": false, 143 + "notNull": true, 144 + "autoincrement": false 145 + }, 146 + "payload": { 147 + "name": "payload", 148 + "type": "text", 149 + "primaryKey": false, 150 + "notNull": false, 151 + "autoincrement": false 152 + }, 153 + "status_code": { 154 + "name": "status_code", 155 + "type": "integer", 156 + "primaryKey": false, 157 + "notNull": false, 158 + "autoincrement": false 159 + }, 160 + "message": { 161 + "name": "message", 162 + "type": "text", 163 + "primaryKey": false, 164 + "notNull": false, 165 + "autoincrement": false 166 + }, 167 + "error": { 168 + "name": "error", 169 + "type": "text", 170 + "primaryKey": false, 171 + "notNull": false, 172 + "autoincrement": false 173 + }, 174 + "dry_run": { 175 + "name": "dry_run", 176 + "type": "integer", 177 + "primaryKey": false, 178 + "notNull": true, 179 + "autoincrement": false, 180 + "default": false 181 + }, 182 + "attempt": { 183 + "name": "attempt", 184 + "type": "integer", 185 + "primaryKey": false, 186 + "notNull": true, 187 + "autoincrement": false, 188 + "default": 1 189 + }, 190 + "created_at": { 191 + "name": "created_at", 192 + "type": "integer", 193 + "primaryKey": false, 194 + "notNull": true, 195 + "autoincrement": false 196 + } 197 + }, 198 + "indexes": {}, 199 + "foreignKeys": { 200 + "delivery_logs_automation_uri_automations_uri_fk": { 201 + "name": "delivery_logs_automation_uri_automations_uri_fk", 202 + "tableFrom": "delivery_logs", 203 + "tableTo": "automations", 204 + "columnsFrom": [ 205 + "automation_uri" 206 + ], 207 + "columnsTo": [ 208 + "uri" 209 + ], 210 + "onDelete": "cascade", 211 + "onUpdate": "no action" 212 + } 213 + }, 214 + "compositePrimaryKeys": {}, 215 + "uniqueConstraints": {}, 216 + "checkConstraints": {} 217 + }, 218 + "favicon_cache": { 219 + "name": "favicon_cache", 220 + "columns": { 221 + "domain": { 222 + "name": "domain", 223 + "type": "text", 224 + "primaryKey": true, 225 + "notNull": true, 226 + "autoincrement": false 227 + }, 228 + "data": { 229 + "name": "data", 230 + "type": "text", 231 + "primaryKey": false, 232 + "notNull": true, 233 + "autoincrement": false 234 + }, 235 + "content_type": { 236 + "name": "content_type", 237 + "type": "text", 238 + "primaryKey": false, 239 + "notNull": true, 240 + "autoincrement": false 241 + }, 242 + "fetched_at": { 243 + "name": "fetched_at", 244 + "type": "integer", 245 + "primaryKey": false, 246 + "notNull": true, 247 + "autoincrement": false 248 + } 249 + }, 250 + "indexes": {}, 251 + "foreignKeys": {}, 252 + "compositePrimaryKeys": {}, 253 + "uniqueConstraints": {}, 254 + "checkConstraints": {} 255 + }, 256 + "lexicon_cache": { 257 + "name": "lexicon_cache", 258 + "columns": { 259 + "nsid": { 260 + "name": "nsid", 261 + "type": "text", 262 + "primaryKey": true, 263 + "notNull": true, 264 + "autoincrement": false 265 + }, 266 + "schema": { 267 + "name": "schema", 268 + "type": "text", 269 + "primaryKey": false, 270 + "notNull": true, 271 + "autoincrement": false 272 + }, 273 + "fetched_at": { 274 + "name": "fetched_at", 275 + "type": "integer", 276 + "primaryKey": false, 277 + "notNull": true, 278 + "autoincrement": false 279 + } 280 + }, 281 + "indexes": {}, 282 + "foreignKeys": {}, 283 + "compositePrimaryKeys": {}, 284 + "uniqueConstraints": {}, 285 + "checkConstraints": {} 286 + }, 287 + "oauth_sessions": { 288 + "name": "oauth_sessions", 289 + "columns": { 290 + "key": { 291 + "name": "key", 292 + "type": "text", 293 + "primaryKey": true, 294 + "notNull": true, 295 + "autoincrement": false 296 + }, 297 + "value": { 298 + "name": "value", 299 + "type": "text", 300 + "primaryKey": false, 301 + "notNull": true, 302 + "autoincrement": false 303 + }, 304 + "expires_at": { 305 + "name": "expires_at", 306 + "type": "integer", 307 + "primaryKey": false, 308 + "notNull": false, 309 + "autoincrement": false 310 + } 311 + }, 312 + "indexes": {}, 313 + "foreignKeys": {}, 314 + "compositePrimaryKeys": {}, 315 + "uniqueConstraints": {}, 316 + "checkConstraints": {} 317 + }, 318 + "oauth_states": { 319 + "name": "oauth_states", 320 + "columns": { 321 + "key": { 322 + "name": "key", 323 + "type": "text", 324 + "primaryKey": true, 325 + "notNull": true, 326 + "autoincrement": false 327 + }, 328 + "value": { 329 + "name": "value", 330 + "type": "text", 331 + "primaryKey": false, 332 + "notNull": true, 333 + "autoincrement": false 334 + }, 335 + "expires_at": { 336 + "name": "expires_at", 337 + "type": "integer", 338 + "primaryKey": false, 339 + "notNull": false, 340 + "autoincrement": false 341 + } 342 + }, 343 + "indexes": {}, 344 + "foreignKeys": {}, 345 + "compositePrimaryKeys": {}, 346 + "uniqueConstraints": {}, 347 + "checkConstraints": {} 348 + }, 349 + "secret_events": { 350 + "name": "secret_events", 351 + "columns": { 352 + "id": { 353 + "name": "id", 354 + "type": "integer", 355 + "primaryKey": true, 356 + "notNull": true, 357 + "autoincrement": true 358 + }, 359 + "did": { 360 + "name": "did", 361 + "type": "text", 362 + "primaryKey": false, 363 + "notNull": true, 364 + "autoincrement": false 365 + }, 366 + "name": { 367 + "name": "name", 368 + "type": "text", 369 + "primaryKey": false, 370 + "notNull": true, 371 + "autoincrement": false 372 + }, 373 + "action": { 374 + "name": "action", 375 + "type": "text", 376 + "primaryKey": false, 377 + "notNull": true, 378 + "autoincrement": false 379 + }, 380 + "created_at": { 381 + "name": "created_at", 382 + "type": "integer", 383 + "primaryKey": false, 384 + "notNull": true, 385 + "autoincrement": false 386 + } 387 + }, 388 + "indexes": {}, 389 + "foreignKeys": {}, 390 + "compositePrimaryKeys": {}, 391 + "uniqueConstraints": {}, 392 + "checkConstraints": {} 393 + }, 394 + "user_secrets": { 395 + "name": "user_secrets", 396 + "columns": { 397 + "id": { 398 + "name": "id", 399 + "type": "integer", 400 + "primaryKey": true, 401 + "notNull": true, 402 + "autoincrement": true 403 + }, 404 + "did": { 405 + "name": "did", 406 + "type": "text", 407 + "primaryKey": false, 408 + "notNull": true, 409 + "autoincrement": false 410 + }, 411 + "name": { 412 + "name": "name", 413 + "type": "text", 414 + "primaryKey": false, 415 + "notNull": true, 416 + "autoincrement": false 417 + }, 418 + "encrypted_value": { 419 + "name": "encrypted_value", 420 + "type": "blob", 421 + "primaryKey": false, 422 + "notNull": true, 423 + "autoincrement": false 424 + }, 425 + "created_at": { 426 + "name": "created_at", 427 + "type": "integer", 428 + "primaryKey": false, 429 + "notNull": true, 430 + "autoincrement": false 431 + }, 432 + "updated_at": { 433 + "name": "updated_at", 434 + "type": "integer", 435 + "primaryKey": false, 436 + "notNull": true, 437 + "autoincrement": false 438 + } 439 + }, 440 + "indexes": { 441 + "user_secrets_did_name_unique": { 442 + "name": "user_secrets_did_name_unique", 443 + "columns": [ 444 + "did", 445 + "name" 446 + ], 447 + "isUnique": true 448 + } 449 + }, 450 + "foreignKeys": { 451 + "user_secrets_did_users_did_fk": { 452 + "name": "user_secrets_did_users_did_fk", 453 + "tableFrom": "user_secrets", 454 + "tableTo": "users", 455 + "columnsFrom": [ 456 + "did" 457 + ], 458 + "columnsTo": [ 459 + "did" 460 + ], 461 + "onDelete": "cascade", 462 + "onUpdate": "no action" 463 + } 464 + }, 465 + "compositePrimaryKeys": {}, 466 + "uniqueConstraints": {}, 467 + "checkConstraints": {} 468 + }, 469 + "users": { 470 + "name": "users", 471 + "columns": { 472 + "id": { 473 + "name": "id", 474 + "type": "integer", 475 + "primaryKey": true, 476 + "notNull": true, 477 + "autoincrement": true 478 + }, 479 + "did": { 480 + "name": "did", 481 + "type": "text", 482 + "primaryKey": false, 483 + "notNull": true, 484 + "autoincrement": false 485 + }, 486 + "handle": { 487 + "name": "handle", 488 + "type": "text", 489 + "primaryKey": false, 490 + "notNull": true, 491 + "autoincrement": false 492 + }, 493 + "scope": { 494 + "name": "scope", 495 + "type": "text", 496 + "primaryKey": false, 497 + "notNull": false, 498 + "autoincrement": false 499 + }, 500 + "created_at": { 501 + "name": "created_at", 502 + "type": "integer", 503 + "primaryKey": false, 504 + "notNull": true, 505 + "autoincrement": false 506 + } 507 + }, 508 + "indexes": { 509 + "users_did_unique": { 510 + "name": "users_did_unique", 511 + "columns": [ 512 + "did" 513 + ], 514 + "isUnique": true 515 + } 516 + }, 517 + "foreignKeys": {}, 518 + "compositePrimaryKeys": {}, 519 + "uniqueConstraints": {}, 520 + "checkConstraints": {} 521 + } 522 + }, 523 + "views": {}, 524 + "enums": {}, 525 + "_meta": { 526 + "schemas": {}, 527 + "tables": {}, 528 + "columns": {} 529 + }, 530 + "internal": { 531 + "indexes": {} 532 + } 533 + }
+7
lib/db/migrations/meta/_journal.json
··· 29 29 "when": 1776255747958, 30 30 "tag": "0003_familiar_hellfire_club", 31 31 "breakpoints": true 32 + }, 33 + { 34 + "idx": 4, 35 + "version": "6", 36 + "when": 1776417811155, 37 + "tag": "0004_certain_marvel_boy", 38 + "breakpoints": true 32 39 } 33 40 ] 34 41 }
+1
lib/db/schema.ts
··· 4 4 id: integer("id").primaryKey({ autoIncrement: true }), 5 5 did: text("did").notNull().unique(), 6 6 handle: text("handle").notNull(), 7 + scope: text("scope"), 7 8 createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), 8 9 }); 9 10