History

deploy install url

Rhys Sullivan committed 9 months ago

Commit c28dabb1 parent f0c7f537 files · +370 −37Browse files

Unified · Split

7 files
  1. executor/apps/web/next.config.ts +11 −0
  2. executor/apps/web/src/app/install/route.ts +7 −0
  3. executor/convex/_generated/api.d.ts +29 −0
  4. executor/convex/database.ts +112 −0
  5. executor/convex/schema.ts +33 −0
  6. executor/install +61 −18
  7. executor/lib/anonymous-oauth.ts +117 −19
executor/apps/web/next.config.ts+11−0
@@ -8,6 +8,8 @@ const appShellRewriteExclusions = [ · 7 unmodified lines
8 "sign-up(?:/|$)",8 "sign-up(?:/|$)",
9 "sign-out(?:/|$)",9 "sign-out(?:/|$)",
10 "callback(?:/|$)",10 "callback(?:/|$)",
11 "install(?:/|$)",
12 "install\\.sh(?:/|$)",
11 "static-app-shell(?:/|$)",13 "static-app-shell(?:/|$)",
12 ".*\\..*",14 ".*\\..*",
13].join("|");15].join("|");
@@ -29,6 +31,15 @@ const nextConfig: NextConfig = { · 15 unmodified lines
29 },31 },
30 ];32 ];
31 },33 },
34 async redirects() {
35 return [
36 {
37 source: "/install.sh",
38 destination: "/install",
39 permanent: false,
40 },
41 ];
42 },
32};43};
33 44
34export default nextConfig;45export default nextConfig;
executor/apps/web/src/app/install/route.ts+7−0
@@ -0,0 +1,7 @@
1const DEFAULT_INSTALL_SCRIPT_URL =
2 "https://raw.githubusercontent.com/RhysSullivan/executor/main/executor/install";
3
4export async function GET(): Promise<Response> {
5 const target = process.env.EXECUTOR_INSTALL_SCRIPT_URL ?? DEFAULT_INSTALL_SCRIPT_URL;
6 return Response.redirect(target, 302);
7}
executor/convex/_generated/api.d.ts+29−0
@@ -548,6 +548,18 @@ export declare const internal: { · 547 unmodified lines
548 { sourceId: string; workspaceId: Id<"workspaces"> },548 { sourceId: string; workspaceId: Id<"workspaces"> },
549 any549 any
550 >;550 >;
551 getActiveAnonymousOauthSigningKey: FunctionReference<
552 "query",
553 "internal",
554 {},
555 any
556 >;
557 getAnonymousOauthClient: FunctionReference<
558 "query",
559 "internal",
560 { clientId: string },
561 any
562 >;
551 getApproval: FunctionReference<563 getApproval: FunctionReference<
552 "query",564 "query",
553 "internal",565 "internal",
@@ -639,6 +651,12 @@ export declare const internal: { · 85 unmodified lines
639 { taskId: string },651 { taskId: string },
640 any652 any
641 >;653 >;
654 registerAnonymousOauthClient: FunctionReference<
655 "mutation",
656 "internal",
657 { clientId: string; clientName?: string; redirectUris: Array<string> },
658 any
659 >;
642 resolveApproval: FunctionReference<660 resolveApproval: FunctionReference<
643 "mutation",661 "mutation",
644 "internal",662 "internal",
@@ -661,6 +679,17 @@ export declare const internal: { · 16 unmodified lines
661 },679 },
662 any680 any
663 >;681 >;
682 storeAnonymousOauthSigningKey: FunctionReference<
683 "mutation",
684 "internal",
685 {
686 algorithm: string;
687 keyId: string;
688 privateKeyJwk: any;
689 publicKeyJwk: any;
690 },
691 any
692 >;
664 upsertAccessPolicy: FunctionReference<693 upsertAccessPolicy: FunctionReference<
665 "mutation",694 "mutation",
666 "internal",695 "internal",
executor/convex/database.ts+112−0
@@ -1133,3 +1133,115 @@ export const listTaskEvents = internalQuery({ · 1132 unmodified lines
1133 return docs.map(mapTaskEvent);1133 return docs.map(mapTaskEvent);
1134 },1134 },
1135});1135});
1136
1137// ── Anonymous OAuth Signing Keys ─────────────────────────────────────────────
1138
1139/**
1140 * Get the active anonymous OAuth signing key pair.
1141 * Returns null if no key has been generated yet.
1142 */
1143export const getActiveAnonymousOauthSigningKey = internalQuery({
1144 args: {},
1145 handler: async (ctx) => {
1146 const doc = await ctx.db
1147 .query("anonymousOauthSigningKeys")
1148 .withIndex("by_status", (q) => q.eq("status", "active"))
1149 .first();
1150
1151 if (!doc) return null;
1152
1153 return {
1154 keyId: doc.keyId,
1155 algorithm: doc.algorithm,
1156 privateKeyJwk: doc.privateKeyJwk,
1157 publicKeyJwk: doc.publicKeyJwk,
1158 createdAt: doc.createdAt,
1159 };
1160 },
1161});
1162
1163/**
1164 * Store a new anonymous OAuth signing key pair, rotating any previously active key.
1165 */
1166export const storeAnonymousOauthSigningKey = internalMutation({
1167 args: {
1168 keyId: v.string(),
1169 algorithm: v.string(),
1170 privateKeyJwk: v.any(),
1171 publicKeyJwk: v.any(),
1172 },
1173 handler: async (ctx, args) => {
1174 const now = Date.now();
1175
1176 // Rotate any currently active keys
1177 const activeKeys = await ctx.db
1178 .query("anonymousOauthSigningKeys")
1179 .withIndex("by_status", (q) => q.eq("status", "active"))
1180 .collect();
1181
1182 for (const key of activeKeys) {
1183 await ctx.db.patch(key._id, { status: "rotated", rotatedAt: now });
1184 }
1185
1186 // Insert the new active key
1187 await ctx.db.insert("anonymousOauthSigningKeys", {
1188 keyId: args.keyId,
1189 algorithm: args.algorithm,
1190 privateKeyJwk: args.privateKeyJwk,
1191 publicKeyJwk: args.publicKeyJwk,
1192 status: "active",
1193 createdAt: now,
1194 });
1195 },
1196});
1197
1198// ── Anonymous OAuth Client Registrations ────────────────────────────────────
1199
1200/**
1201 * Register a new anonymous OAuth client (RFC 7591 dynamic client registration).
1202 */
1203export const registerAnonymousOauthClient = internalMutation({
1204 args: {
1205 clientId: v.string(),
1206 clientName: v.optional(v.string()),
1207 redirectUris: v.array(v.string()),
1208 },
1209 handler: async (ctx, args) => {
1210 const now = Date.now();
1211 await ctx.db.insert("anonymousOauthClients", {
1212 clientId: args.clientId,
1213 clientName: args.clientName,
1214 redirectUris: args.redirectUris,
1215 createdAt: now,
1216 });
1217
1218 return {
1219 client_id: args.clientId,
1220 client_name: args.clientName,
1221 redirect_uris: args.redirectUris,
1222 created_at: now,
1223 };
1224 },
1225});
1226
1227/**
1228 * Look up an anonymous OAuth client by client_id.
1229 */
1230export const getAnonymousOauthClient = internalQuery({
1231 args: { clientId: v.string() },
1232 handler: async (ctx, args) => {
1233 const doc = await ctx.db
1234 .query("anonymousOauthClients")
1235 .withIndex("by_client_id", (q) => q.eq("clientId", args.clientId))
1236 .unique();
1237
1238 if (!doc) return null;
1239
1240 return {
1241 client_id: doc.clientId,
1242 client_name: doc.clientName,
1243 redirect_uris: doc.redirectUris,
1244 created_at: doc.createdAt,
1245 };
1246 },
1247});
executor/convex/schema.ts+33−0
@@ -313,4 +313,37 @@ export default defineSchema({ · 312 unmodified lines
313 .index("by_session_id", ["sessionId"])313 .index("by_session_id", ["sessionId"])
314 .index("by_workspace_actor", ["workspaceId", "actorId"])314 .index("by_workspace_actor", ["workspaceId", "actorId"])
315 .index("by_account", ["accountId"]),315 .index("by_account", ["accountId"]),
316
317 // ── Anonymous OAuth: Self-issued auth server for anonymous MCP clients ────
318
319 /**
320 * Signing key pair for the self-issued anonymous OAuth server.
321 * Persisted so tokens survive gateway restarts.
322 * Typically only one "active" row at a time.
323 */
324 anonymousOauthSigningKeys: defineTable({
325 keyId: v.string(), // kid claim: anon_key_<short-uuid>
326 algorithm: v.string(), // e.g. "RS256"
327 /** JWK-encoded private key (JSON object). */
328 privateKeyJwk: v.any(),
329 /** JWK-encoded public key (JSON object). */
330 publicKeyJwk: v.any(),
331 status: v.union(v.literal("active"), v.literal("rotated")),
332 createdAt: v.number(),
333 rotatedAt: v.optional(v.number()),
334 })
335 .index("by_key_id", ["keyId"])
336 .index("by_status", ["status"]),
337
338 /**
339 * Dynamic client registrations (RFC 7591) for the anonymous OAuth flow.
340 * Persisted so MCP clients don't need to re-register after a gateway restart.
341 */
342 anonymousOauthClients: defineTable({
343 clientId: v.string(), // domain ID: anon_client_<uuid>
344 clientName: v.optional(v.string()),
345 redirectUris: v.array(v.string()),
346 createdAt: v.number(),
347 })
348 .index("by_client_id", ["clientId"]),
316});349});
executor/install+61−18
@@ -5,7 +5,10 @@ APP="executor" · 4 unmodified lines
5REPO="${EXECUTOR_REPO:-RhysSullivan/assistant}"5REPO="${EXECUTOR_REPO:-RhysSullivan/assistant}"
6INSTALL_DIR="${EXECUTOR_INSTALL_DIR:-$HOME/.executor/bin}"6INSTALL_DIR="${EXECUTOR_INSTALL_DIR:-$HOME/.executor/bin}"
7REQUESTED_VERSION="${VERSION:-}"7REQUESTED_VERSION="${VERSION:-}"
8BINARY_PATH=""
8NO_MODIFY_PATH="0"9NO_MODIFY_PATH="0"
10SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
11LOCAL_BINARY_CANDIDATE="$SCRIPT_DIR/dist/executor"
9 12
10RED='\033[0;31m'13RED='\033[0;31m'
11MUTED='\033[0;2m'14MUTED='\033[0;2m'
@@ -20,6 +23,7 @@ Usage: · 8 unmodified lines
20 23
21Options:24Options:
22 -v, --version <version> Install specific release tag (with or without leading v)25 -v, --version <version> Install specific release tag (with or without leading v)
26 -b, --binary <path> Install from a local prebuilt binary
23 --no-modify-path Do not append executor bin directory to shell rc27 --no-modify-path Do not append executor bin directory to shell rc
24 -h, --help Show this help28 -h, --help Show this help
25 29
@@ -43,6 +47,14 @@ while [[ $# -gt 0 ]]; do · 17 unmodified lines
43 fi47 fi
44 shift 248 shift 2
45 ;;49 ;;
50 -b|--binary)
51 BINARY_PATH="${2:-}"
52 if [[ -z "$BINARY_PATH" ]]; then
53 echo -e "${RED}Missing value for --binary${NC}"
54 exit 1
55 fi
56 shift 2
57 ;;
46 --no-modify-path)58 --no-modify-path)
47 NO_MODIFY_PATH="1"59 NO_MODIFY_PATH="1"
48 shift60 shift
@@ -80,30 +92,61 @@ archive_ext="tar.gz" · 31 unmodified lines
80filename="${APP}-${os}-${arch}.${archive_ext}"92filename="${APP}-${os}-${arch}.${archive_ext}"
81mkdir -p "$INSTALL_DIR"93mkdir -p "$INSTALL_DIR"
82 94
83if [[ -z "$REQUESTED_VERSION" ]]; then95install_local_binary() {
84 url="https://github.com/${REPO}/releases/latest/download/${filename}"96 local src="$1"
85 version="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')"97 if [[ ! -f "$src" ]]; then
86else98 echo -e "${RED}Local binary not found: ${src}${NC}"
87 version="${REQUESTED_VERSION#v}"99 exit 1
88 url="https://github.com/${REPO}/releases/download/v${version}/${filename}"100 fi
89fi101 cp "$src" "$INSTALL_DIR/$APP"
102 chmod 755 "$INSTALL_DIR/$APP"
103}
90 104
91tmp_dir="$(mktemp -d)"105if [[ -n "$BINARY_PATH" ]]; then
92trap 'rm -rf "$tmp_dir"' EXIT106 echo -e "${MUTED}Installing ${APP}${NC} from local binary: $BINARY_PATH"
107 install_local_binary "$BINARY_PATH"
108else
109 if [[ -z "$REQUESTED_VERSION" ]]; then
110 url="https://github.com/${REPO}/releases/latest/download/${filename}"
111 release_json="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null || true)"
112 version="$(printf "%s" "$release_json" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p')"
113 if [[ -z "$version" ]]; then
114 version="latest"
115 fi
116 else
117 version="${REQUESTED_VERSION#v}"
118 url="https://github.com/${REPO}/releases/download/v${version}/${filename}"
119 fi
93 120
94echo -e "${MUTED}Installing ${APP}${NC} ${version:-latest}"121 tmp_dir="$(mktemp -d)"
95curl -fsSL "$url" -o "$tmp_dir/$filename"122 trap 'rm -rf "$tmp_dir"' EXIT
123
124 echo -e "${MUTED}Installing ${APP}${NC} ${version:-latest}"
125 if ! curl -fsSL "$url" -o "$tmp_dir/$filename"; then
126 if [[ -f "$LOCAL_BINARY_CANDIDATE" ]]; then
127 echo -e "${MUTED}Release asset not found; using local binary at ${LOCAL_BINARY_CANDIDATE}${NC}"
128 install_local_binary "$LOCAL_BINARY_CANDIDATE"
129 else
130 echo -e "${RED}Could not download release asset: ${url}${NC}"
131 echo ""
132 echo "If you are developing locally, build and install from source:"
133 echo " bun run --cwd executor build:binary"
134 echo " bash executor/install --binary executor/dist/executor"
135 exit 1
136 fi
137 else
138 tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
96 139
97tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"140 if [[ ! -f "$tmp_dir/$APP" ]]; then
141 echo -e "${RED}Archive did not contain expected binary '${APP}'${NC}"
142 exit 1
143 fi
98 144
99if [[ ! -f "$tmp_dir/$APP" ]]; then145 mv "$tmp_dir/$APP" "$INSTALL_DIR/$APP"
100 echo -e "${RED}Archive did not contain expected binary '${APP}'${NC}"146 chmod 755 "$INSTALL_DIR/$APP"
101 exit 1147 fi
102fi148fi
103 149
104mv "$tmp_dir/$APP" "$INSTALL_DIR/$APP"
105chmod 755 "$INSTALL_DIR/$APP"
106
107if [[ "$NO_MODIFY_PATH" != "1" ]]; then150if [[ "$NO_MODIFY_PATH" != "1" ]]; then
108 shell_name="$(basename "${SHELL:-}")"151 shell_name="$(basename "${SHELL:-}")"
109 case "$shell_name" in152 case "$shell_name" in
executor/lib/anonymous-oauth.ts+117−19
@@ -18,6 +18,13 @@ · 17 unmodified lines
18 * (`anon_<uuid>`) which the gateway resolves via18 * (`anon_<uuid>`) which the gateway resolves via
19 * `bootstrapAnonymousSession` — identical to the existing anonymous flow19 * `bootstrapAnonymousSession` — identical to the existing anonymous flow
20 * but now behind a proper Bearer token.20 * but now behind a proper Bearer token.
21 *
22 * Storage is pluggable via the `OAuthStorage` interface:
23 * - `InMemoryOAuthStorage` — ephemeral, for tests and single-process dev
24 * - `ConvexOAuthStorage` — persists keys & client registrations to Convex,
25 * so tokens survive gateway restarts
26 *
27 * Authorization codes are always in-memory (short-lived, single-use).
21 */28 */
22 29
23import {30import {
@@ -25,6 +32,7 @@ import { · 1 unmodified line
25 createLocalJWKSet,32 createLocalJWKSet,
26 exportJWK,33 exportJWK,
27 generateKeyPair,34 generateKeyPair,
35 importJWK,
28 jwtVerify,36 jwtVerify,
29 type JWK,37 type JWK,
30 type CryptoKey as JoseCryptoKey,38 type CryptoKey as JoseCryptoKey,
@@ -58,6 +66,73 @@ export interface AnonOAuthConfig { · 27 unmodified lines
58 accessTokenTtlSeconds?: number;66 accessTokenTtlSeconds?: number;
59 /** Authorization code TTL in seconds. Default 120 s. */67 /** Authorization code TTL in seconds. Default 120 s. */
60 codeExpirySeconds?: number;68 codeExpirySeconds?: number;
69 /** Pluggable storage backend for keys & client registrations. */
70 storage?: OAuthStorage;
71}
72
73// ---------------------------------------------------------------------------
74// Storage interface
75// ---------------------------------------------------------------------------
76
77export interface StoredSigningKey {
78 keyId: string;
79 algorithm: string;
80 privateKeyJwk: JWK;
81 publicKeyJwk: JWK;
82}
83
84export interface OAuthStorage {
85 /**
86 * Load the active signing key pair.
87 * Returns null if no key has been generated yet.
88 */
89 getActiveSigningKey(): Promise<StoredSigningKey | null>;
90
91 /**
92 * Store a new signing key pair (rotating any previous active key).
93 */
94 storeSigningKey(key: StoredSigningKey): Promise<void>;
95
96 /**
97 * Register a new OAuth client. Returns the registration.
98 */
99 registerClient(registration: AnonOAuthClientRegistration): Promise<AnonOAuthClientRegistration>;
100
101 /**
102 * Look up an OAuth client by client_id. Returns null if not found.
103 */
104 getClient(clientId: string): Promise<AnonOAuthClientRegistration | null>;
105}
106
107// ---------------------------------------------------------------------------
108// In-memory storage (for tests and single-process dev)
109// ---------------------------------------------------------------------------
110
111export class InMemoryOAuthStorage implements OAuthStorage {
112 private signingKey: StoredSigningKey | null = null;
113 private readonly clients = new Map<string, AnonOAuthClientRegistration>();
114
115 async getActiveSigningKey(): Promise<StoredSigningKey | null> {
116 return this.signingKey;
117 }
118
119 async storeSigningKey(key: StoredSigningKey): Promise<void> {
120 this.signingKey = key;
121 }
122
123 async registerClient(registration: AnonOAuthClientRegistration): Promise<AnonOAuthClientRegistration> {
124 this.clients.set(registration.client_id, registration);
125 return registration;
126 }
127
128 async getClient(clientId: string): Promise<AnonOAuthClientRegistration | null> {
129 return this.clients.get(clientId) ?? null;
130 }
131
132 /** Test helper: number of registered clients. */
133 get clientCount(): number {
134 return this.clients.size;
135 }
61}136}
62 137
63// ---------------------------------------------------------------------------138// ---------------------------------------------------------------------------
@@ -68,13 +143,12 @@ export class AnonymousOAuthServer { · 4 unmodified lines
68 private readonly issuer: string;143 private readonly issuer: string;
69 private readonly accessTokenTtlSeconds: number;144 private readonly accessTokenTtlSeconds: number;
70 private readonly codeExpirySeconds: number;145 private readonly codeExpirySeconds: number;
146 private readonly storage: OAuthStorage;
71 147
72 private privateKey!: JoseCryptoKey;148 private privateKey!: JoseCryptoKey;
73 private publicJwk!: JWK;149 private publicJwk!: JWK;
74 private keyId!: string;150 private keyId!: string;
75 151
76 /** In-memory client registrations (ephemeral — survives process lifetime). */
77 private readonly clients = new Map<string, AnonOAuthClientRegistration>();
78 /** In-memory authorization codes (short-lived, cleaned up on use). */152 /** In-memory authorization codes (short-lived, cleaned up on use). */
79 private readonly codes = new Map<string, AuthorizationCode>();153 private readonly codes = new Map<string, AuthorizationCode>();
80 154
@@ -82,25 +156,53 @@ export class AnonymousOAuthServer { · 1 unmodified line
82 this.issuer = config.issuer.replace(/\/+$/, "");156 this.issuer = config.issuer.replace(/\/+$/, "");
83 this.accessTokenTtlSeconds = config.accessTokenTtlSeconds ?? 24 * 60 * 60;157 this.accessTokenTtlSeconds = config.accessTokenTtlSeconds ?? 24 * 60 * 60;
84 this.codeExpirySeconds = config.codeExpirySeconds ?? 120;158 this.codeExpirySeconds = config.codeExpirySeconds ?? 120;
159 this.storage = config.storage ?? new InMemoryOAuthStorage();
85 }160 }
86 161
87 // -------------------------------------------------------------------------162 // -------------------------------------------------------------------------
88 // Initialization163 // Initialization
89 // -------------------------------------------------------------------------164 // -------------------------------------------------------------------------
90 165
91 /** Generate an ephemeral RSA key pair. Must be called once before use. */166 /**
167 * Initialize the OAuth server.
168 *
169 * Attempts to load an existing signing key from storage. If none exists,
170 * generates a new RSA key pair and persists it. Either way, the private
171 * key and public JWK are cached locally for fast in-process operations.
172 */
92 async init(): Promise<void> {173 async init(): Promise<void> {
174 const existing = await this.storage.getActiveSigningKey();
175
176 if (existing) {
177 // Import the persisted key pair into CryptoKey objects
178 this.keyId = existing.keyId;
179 this.publicJwk = { ...existing.publicKeyJwk, kid: this.keyId, use: "sig", alg: existing.algorithm };
180 this.privateKey = await importJWK(existing.privateKeyJwk, existing.algorithm) as JoseCryptoKey;
181 return;
182 }
183
184 // No existing key — generate a fresh pair and persist it
93 const { privateKey, publicKey } = await generateKeyPair("RS256");185 const { privateKey, publicKey } = await generateKeyPair("RS256");
94 this.privateKey = privateKey;186 this.privateKey = privateKey;
95 this.keyId = `anon-${crypto.randomUUID().slice(0, 8)}`;187 this.keyId = `anon_key_${crypto.randomUUID().slice(0, 8)}`;
96 const jwk = await exportJWK(publicKey);188
97 this.publicJwk = { ...jwk, kid: this.keyId, use: "sig", alg: "RS256" };189 const publicJwk = await exportJWK(publicKey);
190 this.publicJwk = { ...publicJwk, kid: this.keyId, use: "sig", alg: "RS256" };
191
192 const privateKeyJwk = await exportJWK(privateKey);
193
194 await this.storage.storeSigningKey({
195 keyId: this.keyId,
196 algorithm: "RS256",
197 privateKeyJwk,
198 publicKeyJwk: publicJwk,
199 });
98 }200 }
99 201
100 /** Import an existing key pair (for tests or persistence). */202 /** Import an existing key pair (for tests or manual persistence). */
101 async initWithKeys(privateKey: JoseCryptoKey, publicJwk: JWK): Promise<void> {203 async initWithKeys(privateKey: JoseCryptoKey, publicJwk: JWK): Promise<void> {
102 this.privateKey = privateKey;204 this.privateKey = privateKey;
103 this.keyId = publicJwk.kid ?? `anon-${crypto.randomUUID().slice(0, 8)}`;205 this.keyId = publicJwk.kid ?? `anon_key_${crypto.randomUUID().slice(0, 8)}`;
104 this.publicJwk = { ...publicJwk, kid: this.keyId, use: "sig", alg: "RS256" };206 this.publicJwk = { ...publicJwk, kid: this.keyId, use: "sig", alg: "RS256" };
105 }207 }
106 208
@@ -135,10 +237,10 @@ export class AnonymousOAuthServer { · 28 unmodified lines
135 // RFC 7591 — Dynamic Client Registration237 // RFC 7591 — Dynamic Client Registration
136 // -------------------------------------------------------------------------238 // -------------------------------------------------------------------------
137 239
138 registerClient(body: {240 async registerClient(body: {
139 redirect_uris?: string[];241 redirect_uris?: string[];
140 client_name?: string;242 client_name?: string;
141 }): AnonOAuthClientRegistration {243 }): Promise<AnonOAuthClientRegistration> {
142 const redirectUris = body.redirect_uris;244 const redirectUris = body.redirect_uris;
143 if (!Array.isArray(redirectUris) || redirectUris.length === 0) {245 if (!Array.isArray(redirectUris) || redirectUris.length === 0) {
144 throw new OAuthBadRequest("redirect_uris is required and must be non-empty");246 throw new OAuthBadRequest("redirect_uris is required and must be non-empty");
@@ -157,8 +259,8 @@ export class AnonymousOAuthServer { · 12 unmodified lines
157 redirect_uris: redirectUris,259 redirect_uris: redirectUris,
158 created_at: Date.now(),260 created_at: Date.now(),
159 };261 };
160 this.clients.set(clientId, registration);262
161 return registration;263 return await this.storage.registerClient(registration);
162 }264 }
163 265
164 // -------------------------------------------------------------------------266 // -------------------------------------------------------------------------
@@ -170,7 +272,7 @@ export class AnonymousOAuthServer { · 5 unmodified lines
170 * user to authenticate — we auto-approve and redirect back immediately272 * user to authenticate — we auto-approve and redirect back immediately
171 * with an authorization code.273 * with an authorization code.
172 */274 */
173 authorize(params: URLSearchParams): { redirectTo: string } {275 async authorize(params: URLSearchParams): Promise<{ redirectTo: string }> {
174 const responseType = params.get("response_type");276 const responseType = params.get("response_type");
175 if (responseType !== "code") {277 if (responseType !== "code") {
176 throw new OAuthBadRequest("response_type must be 'code'");278 throw new OAuthBadRequest("response_type must be 'code'");
@@ -181,7 +283,7 @@ export class AnonymousOAuthServer { · 4 unmodified lines
181 throw new OAuthBadRequest("client_id is required");283 throw new OAuthBadRequest("client_id is required");
182 }284 }
183 285
184 const client = this.clients.get(clientId);286 const client = await this.storage.getClient(clientId);
185 if (!client) {287 if (!client) {
186 throw new OAuthBadRequest("Unknown client_id");288 throw new OAuthBadRequest("Unknown client_id");
187 }289 }
@@ -203,7 +305,7 @@ export class AnonymousOAuthServer { · 15 unmodified lines
203 // Generate anonymous identity305 // Generate anonymous identity
204 const actorId = `anon_${crypto.randomUUID()}`;306 const actorId = `anon_${crypto.randomUUID()}`;
205 307
206 // Issue authorization code308 // Issue authorization code (always in-memory — short-lived, single-use)
207 const code = crypto.randomUUID();309 const code = crypto.randomUUID();
208 this.codes.set(code, {310 this.codes.set(code, {
209 code,311 code,
@@ -331,10 +433,6 @@ export class AnonymousOAuthServer { · 121 unmodified lines
331 return this.issuer;433 return this.issuer;
332 }434 }
333 435
334 getClientCount(): number {
335 return this.clients.size;
336 }
337
338 getCodeCount(): number {436 getCodeCount(): number {
339 return this.codes.size;437 return this.codes.size;
340 }438 }