@parlor/auth 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -2
  3. package/dist/account.d.ts +59 -0
  4. package/dist/account.d.ts.map +1 -0
  5. package/dist/account.js +127 -0
  6. package/dist/account.js.map +1 -0
  7. package/dist/audit.d.ts +10 -0
  8. package/dist/audit.d.ts.map +1 -0
  9. package/dist/audit.js +130 -0
  10. package/dist/audit.js.map +1 -0
  11. package/dist/better-auth.d.ts +46 -0
  12. package/dist/better-auth.d.ts.map +1 -0
  13. package/dist/better-auth.js +120 -0
  14. package/dist/better-auth.js.map +1 -0
  15. package/dist/client/index.d.ts +79 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +554 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/database.d.ts +7 -0
  20. package/dist/database.d.ts.map +1 -0
  21. package/dist/database.js +11 -0
  22. package/dist/database.js.map +1 -0
  23. package/dist/devices.d.ts +157 -0
  24. package/dist/devices.d.ts.map +1 -0
  25. package/dist/devices.js +403 -0
  26. package/dist/devices.js.map +1 -0
  27. package/dist/es256.d.ts +4 -0
  28. package/dist/es256.d.ts.map +1 -0
  29. package/dist/es256.js +44 -0
  30. package/dist/es256.js.map +1 -0
  31. package/dist/grants.d.ts +4 -0
  32. package/dist/grants.d.ts.map +1 -0
  33. package/dist/grants.js +6 -0
  34. package/dist/grants.js.map +1 -0
  35. package/dist/hosts.d.ts +116 -0
  36. package/dist/hosts.d.ts.map +1 -0
  37. package/dist/hosts.js +538 -0
  38. package/dist/hosts.js.map +1 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +9 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/migrations.d.ts +3 -0
  44. package/dist/migrations.d.ts.map +1 -0
  45. package/dist/migrations.js +17 -0
  46. package/dist/migrations.js.map +1 -0
  47. package/dist/native-sign-in.d.ts +45 -0
  48. package/dist/native-sign-in.d.ts.map +1 -0
  49. package/dist/native-sign-in.js +183 -0
  50. package/dist/native-sign-in.js.map +1 -0
  51. package/dist/pages.d.ts +4 -0
  52. package/dist/pages.d.ts.map +1 -0
  53. package/dist/pages.js +53 -0
  54. package/dist/pages.js.map +1 -0
  55. package/dist/provider.d.ts +81 -0
  56. package/dist/provider.d.ts.map +1 -0
  57. package/dist/provider.js +301 -0
  58. package/dist/provider.js.map +1 -0
  59. package/dist/revocation.d.ts +51 -0
  60. package/dist/revocation.d.ts.map +1 -0
  61. package/dist/revocation.js +233 -0
  62. package/dist/revocation.js.map +1 -0
  63. package/dist/routes.d.ts +15 -0
  64. package/dist/routes.d.ts.map +1 -0
  65. package/dist/routes.js +116 -0
  66. package/dist/routes.js.map +1 -0
  67. package/dist/schema.d.ts +2970 -0
  68. package/dist/schema.d.ts.map +1 -0
  69. package/dist/schema.js +308 -0
  70. package/dist/schema.js.map +1 -0
  71. package/dist/statements.d.ts +92 -0
  72. package/dist/statements.d.ts.map +1 -0
  73. package/dist/statements.js +199 -0
  74. package/dist/statements.js.map +1 -0
  75. package/dist/tokens.d.ts +69 -0
  76. package/dist/tokens.d.ts.map +1 -0
  77. package/dist/tokens.js +134 -0
  78. package/dist/tokens.js.map +1 -0
  79. package/dist/turn-keys.d.ts +16 -0
  80. package/dist/turn-keys.d.ts.map +1 -0
  81. package/dist/turn-keys.js +108 -0
  82. package/dist/turn-keys.js.map +1 -0
  83. package/package.json +57 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,301 @@
1
+ import { ANONYMOUS, asId, } from "@parlor/core";
2
+ import { createRegistry } from "@parlor/core/registry";
3
+ import { AuthFailure, REVOCATION_HORIZON_MS, } from "@parlor/server";
4
+ import { betterAuth } from "better-auth";
5
+ import { createLocalJWKSet, decodeProtectedHeader, errors, jwtVerify, } from "jose";
6
+ import { accountActions } from "./account.js";
7
+ import { AUDIT_MIGRATION, createAuditLog } from "./audit.js";
8
+ import { AUTH_PLUGIN, bridgeOf, MCP_SCOPE, } from "./better-auth.js";
9
+ import { createDeviceRegistry, deviceAccountActions, DEVICE_LINKING_MIGRATION, DEVICES_MIGRATION, } from "./devices.js";
10
+ import { grantIdOf } from "./grants.js";
11
+ import { createGrantKeys, hostAccountActions, hostPrincipalOf, HOSTS_MIGRATION, hostRoutes, verifyHostAssertion, } from "./hosts.js";
12
+ import { AUTH_MIGRATIONS } from "./migrations.js";
13
+ import { RevocationSet } from "./revocation.js";
14
+ import { authRoutes } from "./routes.js";
15
+ import { agentPrincipalOf, credentialOf, delegate, verifyTurnToken, } from "./tokens.js";
16
+ import { createTurnKeyStore, TURN_KEYS_MIGRATION, } from "./turn-keys.js";
17
+ /** Stands in for the instance until the first call needs it. */
18
+ function deferredInstance(options) {
19
+ let created;
20
+ const current = () => (created ??= betterAuth(options));
21
+ return {
22
+ options,
23
+ handler: (request) => current().handler(request),
24
+ api: { getSession: (input) => current().api.getSession(input) },
25
+ get $context() {
26
+ return current().$context;
27
+ },
28
+ };
29
+ }
30
+ function hasCookie(request, name) {
31
+ const cookie = request.headers.get("cookie");
32
+ if (!cookie)
33
+ return false;
34
+ return cookie.split(";").some((part) => part.trim().startsWith(`${name}=`));
35
+ }
36
+ /**
37
+ * Sessions become user principals: a cookie on the same-origin web, a bearer token in
38
+ * native shells. Access tokens Better Auth issued to MCP clients become agent principals
39
+ * without a turn.
40
+ */
41
+ export function betterAuthProvider(provided) {
42
+ const instance = "instance" in provided ? provided.instance : deferredInstance(provided.options);
43
+ const bridge = bridgeOf(instance.options);
44
+ const settings = bridge.settings;
45
+ let revocations;
46
+ let keys;
47
+ let grantKeys;
48
+ let turnKeys;
49
+ let devices;
50
+ const clientNames = new Map();
51
+ const authenticatedAt = new WeakMap();
52
+ const installed = () => {
53
+ if (!grantKeys || !turnKeys || !devices) {
54
+ throw new Error("The auth provider is not installed.");
55
+ }
56
+ return { grantKeys, turnKeys, devices };
57
+ };
58
+ const started = () => {
59
+ if (!revocations)
60
+ throw new Error("The auth provider is not installed.");
61
+ return revocations;
62
+ };
63
+ let preparingTurnKeys = false;
64
+ const ready = async () => {
65
+ await started().ready();
66
+ if (!preparingTurnKeys) {
67
+ preparingTurnKeys = true;
68
+ installed()
69
+ .turnKeys.keys()
70
+ .catch(() => {
71
+ preparingTurnKeys = false;
72
+ });
73
+ }
74
+ };
75
+ const loadKeys = async () => {
76
+ const response = await instance.handler(new Request(`${settings.baseURL}/jwks`));
77
+ if (!response.ok)
78
+ throw new Error(`Better Auth answered /jwks with ${response.status}`);
79
+ return createLocalJWKSet((await response.json()));
80
+ };
81
+ const verify = async (token) => {
82
+ const options = {
83
+ issuer: settings.baseURL,
84
+ audience: settings.resource,
85
+ typ: "at+jwt",
86
+ };
87
+ keys ??= await loadKeys();
88
+ try {
89
+ return (await jwtVerify(token, keys, options)).payload;
90
+ }
91
+ catch (error) {
92
+ if (!(error instanceof errors.JWKSNoMatchingKey))
93
+ throw error;
94
+ keys = await loadKeys();
95
+ return (await jwtVerify(token, keys, options)).payload;
96
+ }
97
+ };
98
+ const clientName = (clientId) => {
99
+ let name = clientNames.get(clientId);
100
+ if (!name) {
101
+ name = instance.$context
102
+ .then((context) => context.adapter.findOne({
103
+ model: "oauthClient",
104
+ where: [{ field: "clientId", value: clientId }],
105
+ }))
106
+ .then((client) => client?.name || clientId);
107
+ name.catch(() => clientNames.delete(clientId));
108
+ clientNames.set(clientId, name);
109
+ }
110
+ return name;
111
+ };
112
+ const agentOf = async (token) => {
113
+ let payload;
114
+ try {
115
+ payload = await verify(token);
116
+ }
117
+ catch (error) {
118
+ if (error instanceof errors.JWTExpired) {
119
+ throw new AuthFailure("expired", "The access token expired.");
120
+ }
121
+ throw new AuthFailure("invalid", "The access token is invalid.");
122
+ }
123
+ const { sub, iat } = payload;
124
+ const clientId = payload.client_id;
125
+ const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
126
+ if (typeof sub !== "string" || typeof clientId !== "string" || iat === undefined) {
127
+ throw new AuthFailure("invalid", "The access token is missing claims.");
128
+ }
129
+ if (!scopes.includes(MCP_SCOPE)) {
130
+ throw new AuthFailure("invalid", `The access token lacks the "${MCP_SCOPE}" scope.`);
131
+ }
132
+ const grantId = grantIdOf(sub, clientId);
133
+ if (await started().isRevoked({ kind: "grant", id: grantId }, iat * 1000)) {
134
+ throw new AuthFailure("revoked", "Access for this agent was revoked.");
135
+ }
136
+ return {
137
+ kind: "agent",
138
+ userId: asId("user", sub),
139
+ grantId,
140
+ agent: {
141
+ name: await clientName(clientId),
142
+ verified: clientId.startsWith("https://"),
143
+ },
144
+ };
145
+ };
146
+ const userOf = async (request) => {
147
+ const found = await instance.api.getSession({ headers: request.headers });
148
+ if (!found)
149
+ return undefined;
150
+ return {
151
+ kind: "user",
152
+ userId: asId("user", found.user.id),
153
+ sessionId: found.session.id,
154
+ };
155
+ };
156
+ const publicUrlOf = (request) => new URL(new URL(request.url).pathname, settings.origin).href;
157
+ const hostOf = async (request, assertion) => {
158
+ const grant = await verifyHostAssertion(assertion, { method: request.method, url: publicUrlOf(request) }, installed().grantKeys, Date.now());
159
+ if (await started().isRevoked({ kind: "grant", id: grant.grantId })) {
160
+ throw new AuthFailure("revoked", "This computer was disconnected.");
161
+ }
162
+ return hostPrincipalOf(grant);
163
+ };
164
+ const turnOf = async (token) => {
165
+ const store = installed().turnKeys;
166
+ let turnTokenKeys = await store.keys();
167
+ const kid = decodeProtectedHeader(token).kid;
168
+ if (kid !== undefined && !turnTokenKeys.publicKeys.has(kid)) {
169
+ turnTokenKeys = await store.refresh();
170
+ }
171
+ const claims = await verifyTurnToken(turnTokenKeys, token, Date.now());
172
+ if (await started().isRevoked({ kind: "grant", id: claims.gid })) {
173
+ throw new AuthFailure("revoked", "This computer was disconnected.");
174
+ }
175
+ return agentPrincipalOf(claims);
176
+ };
177
+ const authenticateRequest = async (request) => {
178
+ await ready();
179
+ const credential = credentialOf(request.headers.get("authorization"));
180
+ switch (credential?.kind) {
181
+ case "host":
182
+ return hostOf(request, credential.assertion);
183
+ case "turn":
184
+ return turnOf(credential.token);
185
+ case "access":
186
+ return agentOf(credential.token);
187
+ case "session": {
188
+ const user = await userOf(request);
189
+ if (!user)
190
+ throw new AuthFailure("invalid", "The session is invalid or has ended.");
191
+ return user;
192
+ }
193
+ case undefined:
194
+ break;
195
+ }
196
+ const { authCookies } = await instance.$context;
197
+ if (!hasCookie(request, authCookies.sessionToken.name))
198
+ return ANONYMOUS;
199
+ return (await userOf(request)) ?? ANONYMOUS;
200
+ };
201
+ return {
202
+ name: AUTH_PLUGIN,
203
+ migrations: [
204
+ ...AUTH_MIGRATIONS,
205
+ HOSTS_MIGRATION,
206
+ DEVICES_MIGRATION,
207
+ TURN_KEYS_MIGRATION,
208
+ AUDIT_MIGRATION,
209
+ DEVICE_LINKING_MIGRATION,
210
+ ],
211
+ enforcing: true,
212
+ setup(host) {
213
+ const { database } = host;
214
+ if (!database)
215
+ throw new Error("The auth plugin needs a database.");
216
+ const set = new RevocationSet({
217
+ database,
218
+ horizonMs: REVOCATION_HORIZON_MS,
219
+ });
220
+ revocations = set;
221
+ const auditLog = createAuditLog(database);
222
+ bridge.revoke = (notice) => database.transaction((tx) => RevocationSet.revoke(tx, notice));
223
+ grantKeys = createGrantKeys(database);
224
+ devices = createDeviceRegistry(database);
225
+ turnKeys = createTurnKeyStore({
226
+ database,
227
+ issuer: settings.baseURL,
228
+ secret: async () => (await instance.$context).secret,
229
+ });
230
+ const hostDeps = {
231
+ host,
232
+ settings,
233
+ };
234
+ return {
235
+ actions: createRegistry({
236
+ ...accountActions({ instance, host, revoke: bridge.revoke }).actions,
237
+ ...hostAccountActions(hostDeps).actions,
238
+ ...deviceAccountActions({ host, registry: devices }).actions,
239
+ }),
240
+ routes: [...authRoutes({ instance, settings, host }), ...hostRoutes(hostDeps)],
241
+ audit: (record) => auditLog.record(record),
242
+ close: async () => {
243
+ await Promise.all([set.stop(), auditLog.stop()]);
244
+ },
245
+ };
246
+ },
247
+ async authenticate(request) {
248
+ const startedAt = Date.now();
249
+ const principal = await authenticateRequest(request);
250
+ if (principal.kind !== "anonymous")
251
+ authenticatedAt.set(principal, startedAt);
252
+ return principal;
253
+ },
254
+ challenge() {
255
+ const metadata = `${settings.origin}/.well-known/oauth-protected-resource${new URL(settings.resource).pathname}`;
256
+ return Response.json({ error: { code: "unauthenticated", message: "Sign in through OAuth first." } }, {
257
+ status: 401,
258
+ headers: {
259
+ "www-authenticate": `Bearer resource_metadata="${metadata}", scope="${MCP_SCOPE}"`,
260
+ },
261
+ });
262
+ },
263
+ async delegate(host, turn) {
264
+ await ready();
265
+ return delegate(await installed().turnKeys.keys(), host, turn, Date.now());
266
+ },
267
+ async hostActive(grantId) {
268
+ const revoked = await started().isRevoked({ kind: "grant", id: grantId });
269
+ return !revoked && (await installed().grantKeys.get(grantId)) !== undefined;
270
+ },
271
+ async device(userId, deviceId) {
272
+ const registered = await installed().devices.get(userId, deviceId);
273
+ if (!registered)
274
+ return undefined;
275
+ const { id, name, platform, signingKey, presenceKey, vouches } = registered;
276
+ return {
277
+ id,
278
+ name,
279
+ platform,
280
+ signingKey,
281
+ presenceKey,
282
+ ...(vouches ? { vouches } : {}),
283
+ };
284
+ },
285
+ async account(userId) {
286
+ const { adapter } = await instance.$context;
287
+ const user = await adapter.findOne({
288
+ model: "user",
289
+ where: [{ field: "id", value: userId }],
290
+ });
291
+ return user ? { name: user.name, email: user.email } : undefined;
292
+ },
293
+ revoked(principal) {
294
+ const notice = principal.kind === "user"
295
+ ? { kind: "session", id: principal.sessionId }
296
+ : { kind: "grant", id: principal.grantId };
297
+ return started().signal([notice], authenticatedAt.get(principal));
298
+ },
299
+ };
300
+ }
301
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentPrincipal,\n ANONYMOUS,\n asId,\n type AuthenticatedPrincipal,\n type DeviceId,\n type GrantId,\n type HostPrincipal,\n type Principal,\n type UserId,\n type UserPrincipal,\n} from \"@parlor/core\";\nimport { createRegistry } from \"@parlor/core/registry\";\nimport {\n type AccountProfile,\n AuthFailure,\n type AuthProvider,\n type Delegation,\n type DeviceKeys,\n type PluginHost,\n REVOCATION_HORIZON_MS,\n type TurnDelegation,\n} from \"@parlor/server\";\nimport { betterAuth, type BetterAuthOptions } from \"better-auth\";\nimport {\n createLocalJWKSet,\n decodeProtectedHeader,\n errors,\n type JSONWebKeySet,\n type JWTPayload,\n jwtVerify,\n} from \"jose\";\n\nimport { accountActions } from \"./account.js\";\nimport { AUDIT_MIGRATION, createAuditLog } from \"./audit.js\";\nimport {\n AUTH_PLUGIN,\n bridgeOf,\n MCP_SCOPE,\n type ParlorAuthSettings,\n} from \"./better-auth.js\";\nimport {\n createDeviceRegistry,\n type DeviceRegistry,\n deviceAccountActions,\n DEVICE_LINKING_MIGRATION,\n DEVICES_MIGRATION,\n} from \"./devices.js\";\nimport { grantIdOf } from \"./grants.js\";\nimport {\n createGrantKeys,\n type GrantKeys,\n hostAccountActions,\n hostPrincipalOf,\n HOSTS_MIGRATION,\n hostRoutes,\n verifyHostAssertion,\n} from \"./hosts.js\";\nimport { AUTH_MIGRATIONS } from \"./migrations.js\";\nimport { RevocationSet } from \"./revocation.js\";\nimport { authRoutes } from \"./routes.js\";\nimport {\n agentPrincipalOf,\n credentialOf,\n delegate,\n type HostAssertion,\n type TurnToken,\n verifyTurnToken,\n} from \"./tokens.js\";\nimport {\n createTurnKeyStore,\n TURN_KEYS_MIGRATION,\n type TurnKeyStore,\n} from \"./turn-keys.js\";\n\n/** The slice of a Better Auth instance Parlor relies on. */\nexport interface BetterAuthInstance {\n readonly options: BetterAuthOptions;\n handler(request: Request): Promise<Response>;\n readonly api: {\n getSession(input: { headers: Headers }): Promise<{\n readonly session: { readonly id: string; readonly userId: string };\n readonly user: {\n readonly id: string;\n readonly name: string;\n readonly email: string;\n };\n } | null>;\n };\n readonly $context: Promise<BetterAuthContext>;\n}\n\ninterface Where {\n readonly field: string;\n readonly value: string;\n}\n\nexport interface BetterAuthContext {\n readonly authCookies: { readonly sessionToken: { readonly name: string } };\n readonly adapter: {\n findMany<T>(input: { model: string; where?: Where[] }): Promise<T[]>;\n findOne<T>(input: { model: string; where: Where[] }): Promise<T | null>;\n deleteMany(input: { model: string; where: Where[] }): Promise<number>;\n };\n readonly secret: string;\n readonly internalAdapter: {\n listSessions(userId: string): Promise<StoredSession[]>;\n deleteSession(token: string): Promise<void>;\n };\n}\n\nexport interface StoredSession {\n readonly id: string;\n readonly token: string;\n readonly userId: string;\n readonly createdAt: Date;\n readonly updatedAt: Date;\n readonly expiresAt: Date;\n readonly ipAddress?: string | null | undefined;\n readonly userAgent?: string | null | undefined;\n}\n\nexport type BetterAuthProviderOptions =\n | { readonly instance: BetterAuthInstance }\n | {\n /**\n * The options for `betterAuth()`, which the provider calls on the first request.\n * Better Auth starts querying the database as soon as it is created, and some\n * runtimes, such as Cloudflare's, allow no I/O while the app loads.\n */\n readonly options: BetterAuthOptions;\n };\n\n/** Stands in for the instance until the first call needs it. */\nfunction deferredInstance(options: BetterAuthOptions): BetterAuthInstance {\n let created: BetterAuthInstance | undefined;\n const current = () =>\n (created ??= betterAuth(options) as unknown as BetterAuthInstance);\n return {\n options,\n handler: (request) => current().handler(request),\n api: { getSession: (input) => current().api.getSession(input) },\n get $context() {\n return current().$context;\n },\n };\n}\n\nfunction hasCookie(request: Request, name: string): boolean {\n const cookie = request.headers.get(\"cookie\");\n if (!cookie) return false;\n return cookie.split(\";\").some((part) => part.trim().startsWith(`${name}=`));\n}\n\n/**\n * Sessions become user principals: a cookie on the same-origin web, a bearer token in\n * native shells. Access tokens Better Auth issued to MCP clients become agent principals\n * without a turn.\n */\nexport function betterAuthProvider(provided: BetterAuthProviderOptions): AuthProvider {\n const instance =\n \"instance\" in provided ? provided.instance : deferredInstance(provided.options);\n const bridge = bridgeOf(instance.options);\n const settings: ParlorAuthSettings = bridge.settings;\n let revocations: RevocationSet | undefined;\n let keys: ReturnType<typeof createLocalJWKSet> | undefined;\n let grantKeys: GrantKeys | undefined;\n let turnKeys: TurnKeyStore | undefined;\n let devices: DeviceRegistry | undefined;\n const clientNames = new Map<string, Promise<string>>();\n const authenticatedAt = new WeakMap<object, number>();\n\n const installed = () => {\n if (!grantKeys || !turnKeys || !devices) {\n throw new Error(\"The auth provider is not installed.\");\n }\n return { grantKeys, turnKeys, devices };\n };\n\n const started = () => {\n if (!revocations) throw new Error(\"The auth provider is not installed.\");\n return revocations;\n };\n\n let preparingTurnKeys = false;\n const ready = async (): Promise<void> => {\n await started().ready();\n if (!preparingTurnKeys) {\n preparingTurnKeys = true;\n installed()\n .turnKeys.keys()\n .catch(() => {\n preparingTurnKeys = false;\n });\n }\n };\n\n const loadKeys = async () => {\n const response = await instance.handler(new Request(`${settings.baseURL}/jwks`));\n if (!response.ok)\n throw new Error(`Better Auth answered /jwks with ${response.status}`);\n return createLocalJWKSet((await response.json()) as JSONWebKeySet);\n };\n\n const verify = async (token: string): Promise<JWTPayload> => {\n const options = {\n issuer: settings.baseURL,\n audience: settings.resource,\n typ: \"at+jwt\",\n };\n keys ??= await loadKeys();\n try {\n return (await jwtVerify(token, keys, options)).payload;\n } catch (error) {\n if (!(error instanceof errors.JWKSNoMatchingKey)) throw error;\n keys = await loadKeys();\n return (await jwtVerify(token, keys, options)).payload;\n }\n };\n\n const clientName = (clientId: string): Promise<string> => {\n let name = clientNames.get(clientId);\n if (!name) {\n name = instance.$context\n .then((context) =>\n context.adapter.findOne<{ name?: string | null }>({\n model: \"oauthClient\",\n where: [{ field: \"clientId\", value: clientId }],\n }),\n )\n .then((client) => client?.name || clientId);\n name.catch(() => clientNames.delete(clientId));\n clientNames.set(clientId, name);\n }\n return name;\n };\n\n const agentOf = async (token: string): Promise<AgentPrincipal> => {\n let payload: JWTPayload;\n try {\n payload = await verify(token);\n } catch (error) {\n if (error instanceof errors.JWTExpired) {\n throw new AuthFailure(\"expired\", \"The access token expired.\");\n }\n throw new AuthFailure(\"invalid\", \"The access token is invalid.\");\n }\n const { sub, iat } = payload;\n const clientId = payload.client_id;\n const scopes = typeof payload.scope === \"string\" ? payload.scope.split(\" \") : [];\n if (typeof sub !== \"string\" || typeof clientId !== \"string\" || iat === undefined) {\n throw new AuthFailure(\"invalid\", \"The access token is missing claims.\");\n }\n if (!scopes.includes(MCP_SCOPE)) {\n throw new AuthFailure(\n \"invalid\",\n `The access token lacks the \"${MCP_SCOPE}\" scope.`,\n );\n }\n const grantId = grantIdOf(sub, clientId);\n if (await started().isRevoked({ kind: \"grant\", id: grantId }, iat * 1000)) {\n throw new AuthFailure(\"revoked\", \"Access for this agent was revoked.\");\n }\n return {\n kind: \"agent\",\n userId: asId(\"user\", sub),\n grantId,\n agent: {\n name: await clientName(clientId),\n verified: clientId.startsWith(\"https://\"),\n },\n };\n };\n\n const userOf = async (request: Request): Promise<UserPrincipal | undefined> => {\n const found = await instance.api.getSession({ headers: request.headers });\n if (!found) return undefined;\n return {\n kind: \"user\",\n userId: asId(\"user\", found.user.id),\n sessionId: found.session.id,\n };\n };\n\n const publicUrlOf = (request: Request): string =>\n new URL(new URL(request.url).pathname, settings.origin).href;\n\n const hostOf = async (\n request: Request,\n assertion: HostAssertion,\n ): Promise<HostPrincipal> => {\n const grant = await verifyHostAssertion(\n assertion,\n { method: request.method, url: publicUrlOf(request) },\n installed().grantKeys,\n Date.now(),\n );\n if (await started().isRevoked({ kind: \"grant\", id: grant.grantId })) {\n throw new AuthFailure(\"revoked\", \"This computer was disconnected.\");\n }\n return hostPrincipalOf(grant);\n };\n\n const turnOf = async (token: TurnToken): Promise<AgentPrincipal> => {\n const store = installed().turnKeys;\n let turnTokenKeys = await store.keys();\n const kid = decodeProtectedHeader(token).kid;\n if (kid !== undefined && !turnTokenKeys.publicKeys.has(kid)) {\n turnTokenKeys = await store.refresh();\n }\n const claims = await verifyTurnToken(turnTokenKeys, token, Date.now());\n if (await started().isRevoked({ kind: \"grant\", id: claims.gid })) {\n throw new AuthFailure(\"revoked\", \"This computer was disconnected.\");\n }\n return agentPrincipalOf(claims);\n };\n\n const authenticateRequest = async (request: Request): Promise<Principal> => {\n await ready();\n const credential = credentialOf(request.headers.get(\"authorization\"));\n switch (credential?.kind) {\n case \"host\":\n return hostOf(request, credential.assertion);\n case \"turn\":\n return turnOf(credential.token);\n case \"access\":\n return agentOf(credential.token);\n case \"session\": {\n const user = await userOf(request);\n if (!user)\n throw new AuthFailure(\"invalid\", \"The session is invalid or has ended.\");\n return user;\n }\n case undefined:\n break;\n }\n const { authCookies } = await instance.$context;\n if (!hasCookie(request, authCookies.sessionToken.name)) return ANONYMOUS;\n return (await userOf(request)) ?? ANONYMOUS;\n };\n\n return {\n name: AUTH_PLUGIN,\n migrations: [\n ...AUTH_MIGRATIONS,\n HOSTS_MIGRATION,\n DEVICES_MIGRATION,\n TURN_KEYS_MIGRATION,\n AUDIT_MIGRATION,\n DEVICE_LINKING_MIGRATION,\n ],\n enforcing: true,\n setup(host: PluginHost) {\n const { database } = host;\n if (!database) throw new Error(\"The auth plugin needs a database.\");\n const set = new RevocationSet({\n database,\n horizonMs: REVOCATION_HORIZON_MS,\n });\n revocations = set;\n const auditLog = createAuditLog(database);\n bridge.revoke = (notice) =>\n database.transaction((tx) => RevocationSet.revoke(tx, notice));\n grantKeys = createGrantKeys(database);\n devices = createDeviceRegistry(database);\n turnKeys = createTurnKeyStore({\n database,\n issuer: settings.baseURL,\n secret: async () => (await instance.$context).secret,\n });\n const hostDeps = {\n host,\n settings,\n };\n return {\n actions: createRegistry({\n ...accountActions({ instance, host, revoke: bridge.revoke }).actions,\n ...hostAccountActions(hostDeps).actions,\n ...deviceAccountActions({ host, registry: devices }).actions,\n }),\n routes: [...authRoutes({ instance, settings, host }), ...hostRoutes(hostDeps)],\n audit: (record) => auditLog.record(record),\n close: async () => {\n await Promise.all([set.stop(), auditLog.stop()]);\n },\n };\n },\n async authenticate(request: Request): Promise<Principal> {\n const startedAt = Date.now();\n const principal = await authenticateRequest(request);\n if (principal.kind !== \"anonymous\") authenticatedAt.set(principal, startedAt);\n return principal;\n },\n challenge(): Response {\n const metadata = `${settings.origin}/.well-known/oauth-protected-resource${new URL(settings.resource).pathname}`;\n return Response.json(\n { error: { code: \"unauthenticated\", message: \"Sign in through OAuth first.\" } },\n {\n status: 401,\n headers: {\n \"www-authenticate\": `Bearer resource_metadata=\"${metadata}\", scope=\"${MCP_SCOPE}\"`,\n },\n },\n );\n },\n async delegate(host: HostPrincipal, turn: TurnDelegation): Promise<Delegation> {\n await ready();\n return delegate(await installed().turnKeys.keys(), host, turn, Date.now());\n },\n async hostActive(grantId: GrantId): Promise<boolean> {\n const revoked = await started().isRevoked({ kind: \"grant\", id: grantId });\n return !revoked && (await installed().grantKeys.get(grantId)) !== undefined;\n },\n async device(userId: UserId, deviceId: DeviceId): Promise<DeviceKeys | undefined> {\n const registered = await installed().devices.get(userId, deviceId);\n if (!registered) return undefined;\n const { id, name, platform, signingKey, presenceKey, vouches } = registered;\n return {\n id,\n name,\n platform,\n signingKey,\n presenceKey,\n ...(vouches ? { vouches } : {}),\n };\n },\n async account(userId: UserId): Promise<AccountProfile | undefined> {\n const { adapter } = await instance.$context;\n const user = await adapter.findOne<{ name: string; email: string }>({\n model: \"user\",\n where: [{ field: \"id\", value: userId }],\n });\n return user ? { name: user.name, email: user.email } : undefined;\n },\n revoked(principal: AuthenticatedPrincipal): AbortSignal {\n const notice =\n principal.kind === \"user\"\n ? ({ kind: \"session\", id: principal.sessionId } as const)\n : ({ kind: \"grant\", id: principal.grantId } as const);\n return started().signal([notice], authenticatedAt.get(principal));\n },\n };\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import type { DataProvider } from "@parlor/core";
2
+ export declare const REVOCATION_CHANNEL = "parlor_auth";
3
+ /** NOTIFY payload. Ids only, never content. */
4
+ export interface RevocationNotice {
5
+ readonly kind: "session" | "grant";
6
+ readonly id: string;
7
+ }
8
+ export interface RevocationSetOptions {
9
+ readonly database: DataProvider;
10
+ /**
11
+ * The longest any credential this set guards stays valid. Older revocations are
12
+ * forgotten: nothing they revoke can still verify.
13
+ */
14
+ readonly horizonMs: number;
15
+ readonly now?: (() => number) | undefined;
16
+ readonly relistenBaseMs?: number | undefined;
17
+ readonly relistenMaxMs?: number | undefined;
18
+ }
19
+ /**
20
+ * `following` answers from memory, kept current by NOTIFY. `checking` answers with one
21
+ * primary-key lookup per call: poll-only providers always do, and a following set falls
22
+ * back to it while its LISTEN connection is down.
23
+ */
24
+ export type RevocationMode = "starting" | "following" | "checking";
25
+ /**
26
+ * Every instance's view of what was revoked within the horizon. It loads before it
27
+ * admits anyone, and whenever its LISTEN connection drops it checks the table per call
28
+ * until it listens again and reloads, because a NOTIFY sent meanwhile is lost.
29
+ */
30
+ export declare class RevocationSet {
31
+ #private;
32
+ constructor(options: RevocationSetOptions);
33
+ get mode(): RevocationMode;
34
+ /** Resolves once the set can answer; a failed start is retried by the next call. */
35
+ ready(): Promise<void>;
36
+ /**
37
+ * Revoked when a revocation of the notice is at or after `issuedAt`; without it, when
38
+ * any revocation of the notice is within the horizon.
39
+ */
40
+ isRevoked(notice: RevocationNotice, issuedAt?: number): Promise<boolean>;
41
+ /**
42
+ * Aborts when any of the notices is revoked at or after `since` (default: now). The
43
+ * watch is registered first and the revocations already known are checked after, so one
44
+ * that landed between `since` and this call still aborts it.
45
+ */
46
+ signal(notices: readonly RevocationNotice[], since?: number): AbortSignal;
47
+ /** Records the revocation in the caller's transaction; the NOTIFY goes out on commit. */
48
+ static revoke(tx: unknown, notice: RevocationNotice, at?: number): Promise<void>;
49
+ stop(): Promise<void>;
50
+ }
51
+ //# sourceMappingURL=revocation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"revocation.d.ts","sourceRoot":"","sources":["../src/revocation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKjD,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAMD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,CAAC;AAWnE;;;;GAIG;AACH,qBAAa,aAAa;;IAkBxB,YAAY,OAAO,EAAE,oBAAoB,EAMxC;IAED,IAAI,IAAI,IAAI,cAAc,CAEzB;IAED,oFAAoF;IACpF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrB;IAED;;;OAGG;IACG,SAAS,CAAC,MAAM,EAAE,gBAAgB,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAOxE;IAED;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,GAAE,MAAoB,GAAG,WAAW,CAcrF;IAED,yFAAyF;IACzF,OAAa,MAAM,CACjB,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,gBAAgB,EACxB,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CAUf;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAO1B;CAwJF"}
@@ -0,0 +1,233 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { rowsOf } from "./database.js";
3
+ export const REVOCATION_CHANNEL = "parlor_auth";
4
+ const keyOf = ({ kind, id }) => `${kind}:${id}`;
5
+ const owner = Symbol("parlor.revocation.owner");
6
+ /**
7
+ * Every instance's view of what was revoked within the horizon. It loads before it
8
+ * admits anyone, and whenever its LISTEN connection drops it checks the table per call
9
+ * until it listens again and reloads, because a NOTIFY sent meanwhile is lost.
10
+ */
11
+ export class RevocationSet {
12
+ #database;
13
+ #horizonMs;
14
+ #now;
15
+ #relistenBaseMs;
16
+ #relistenMaxMs;
17
+ #revoked = new Map();
18
+ #watches = new Map();
19
+ #forget = new FinalizationRegistry((key) => this.#sweepWatch(key));
20
+ #mode = "starting";
21
+ #ready;
22
+ #generation = 0;
23
+ #unlisten;
24
+ #reconnecting = false;
25
+ #retry;
26
+ #sweptAt = 0;
27
+ #stopped = false;
28
+ constructor(options) {
29
+ this.#database = options.database;
30
+ this.#horizonMs = options.horizonMs;
31
+ this.#now = options.now ?? Date.now;
32
+ this.#relistenBaseMs = options.relistenBaseMs ?? 1000;
33
+ this.#relistenMaxMs = options.relistenMaxMs ?? 30_000;
34
+ }
35
+ get mode() {
36
+ return this.#mode;
37
+ }
38
+ /** Resolves once the set can answer; a failed start is retried by the next call. */
39
+ ready() {
40
+ this.#ready ??= this.#start().catch((error) => {
41
+ this.#ready = undefined;
42
+ throw error;
43
+ });
44
+ return this.#ready;
45
+ }
46
+ /**
47
+ * Revoked when a revocation of the notice is at or after `issuedAt`; without it, when
48
+ * any revocation of the notice is within the horizon.
49
+ */
50
+ async isRevoked(notice, issuedAt = 0) {
51
+ await this.ready();
52
+ const at = this.#mode === "following"
53
+ ? this.#revoked.get(keyOf(notice))
54
+ : await this.#lookup(notice);
55
+ return at !== undefined && at >= Math.max(issuedAt, this.#now() - this.#horizonMs);
56
+ }
57
+ /**
58
+ * Aborts when any of the notices is revoked at or after `since` (default: now). The
59
+ * watch is registered first and the revocations already known are checked after, so one
60
+ * that landed between `since` and this call still aborts it.
61
+ */
62
+ signal(notices, since = this.#now()) {
63
+ const controller = new AbortController();
64
+ // The signal pins its controller, so a watch lives exactly as long as its holder.
65
+ Object.defineProperty(controller.signal, owner, { value: controller });
66
+ for (const notice of notices) {
67
+ const key = keyOf(notice);
68
+ const watch = this.#watches.get(key);
69
+ if (watch)
70
+ watch.since = Math.min(watch.since, since);
71
+ else
72
+ this.#watches.set(key, { controllers: new Set(), since });
73
+ this.#watches.get(key).controllers.add(new WeakRef(controller));
74
+ this.#forget.register(controller.signal, key);
75
+ }
76
+ this.#abortIfRevoked(controller, notices, since);
77
+ return controller.signal;
78
+ }
79
+ /** Records the revocation in the caller's transaction; the NOTIFY goes out on commit. */
80
+ static async revoke(tx, notice, at = Date.now()) {
81
+ const db = tx;
82
+ await db.execute(sql `
83
+ insert into parlor_auth_revocations (kind, id, revoked_at)
84
+ values (${notice.kind}, ${notice.id}, ${new Date(at).toISOString()})
85
+ on conflict (kind, id) do update set revoked_at = excluded.revoked_at`);
86
+ const payload = { kind: notice.kind, id: notice.id, at };
87
+ await db.execute(sql `select pg_notify(${REVOCATION_CHANNEL}, ${JSON.stringify(payload)})`);
88
+ }
89
+ async stop() {
90
+ this.#stopped = true;
91
+ this.#generation += 1;
92
+ if (this.#retry !== undefined)
93
+ clearTimeout(this.#retry);
94
+ const unlisten = this.#unlisten;
95
+ this.#unlisten = undefined;
96
+ await unlisten?.().catch(() => { });
97
+ }
98
+ async #start() {
99
+ await this.#prune();
100
+ if (!this.#database.listen) {
101
+ this.#mode = "checking";
102
+ return;
103
+ }
104
+ try {
105
+ await this.#connect(this.#now() - this.#horizonMs);
106
+ }
107
+ catch (error) {
108
+ console.warn("[parlor] revocations: LISTEN failed, checking per call:", error);
109
+ this.#mode = "checking";
110
+ this.#reconnect(this.#now());
111
+ }
112
+ }
113
+ async #connect(since) {
114
+ const generation = ++this.#generation;
115
+ const unlisten = await this.#database.listen(REVOCATION_CHANNEL, (payload) => this.#receive(payload), { onLost: () => this.#lost(generation) });
116
+ if (generation !== this.#generation) {
117
+ await unlisten().catch(() => { });
118
+ return;
119
+ }
120
+ this.#unlisten = unlisten;
121
+ const revocations = await this.#load(since);
122
+ if (generation !== this.#generation)
123
+ return;
124
+ for (const revocation of revocations)
125
+ this.#apply(revocation);
126
+ this.#mode = "following";
127
+ }
128
+ #lost(generation) {
129
+ if (generation !== this.#generation || this.#stopped)
130
+ return;
131
+ this.#generation += 1;
132
+ this.#unlisten = undefined;
133
+ this.#mode = "checking";
134
+ this.#reconnect(this.#now());
135
+ }
136
+ #reconnect(lostAt) {
137
+ if (this.#reconnecting)
138
+ return;
139
+ this.#reconnecting = true;
140
+ let delay = this.#relistenBaseMs;
141
+ const attempt = async () => {
142
+ this.#retry = undefined;
143
+ if (this.#stopped)
144
+ return;
145
+ try {
146
+ await this.#connect(Math.min(lostAt, this.#now() - this.#horizonMs));
147
+ this.#reconnecting = false;
148
+ }
149
+ catch (error) {
150
+ console.warn("[parlor] revocations: LISTEN failed, retrying:", error);
151
+ this.#retry = setTimeout(attempt, delay);
152
+ delay = Math.min(delay * 2, this.#relistenMaxMs);
153
+ }
154
+ };
155
+ this.#retry = setTimeout(attempt, 0);
156
+ }
157
+ #receive(payload) {
158
+ try {
159
+ this.#apply(JSON.parse(payload));
160
+ }
161
+ catch {
162
+ console.warn("[parlor] revocations: ignored a malformed notice");
163
+ }
164
+ }
165
+ #apply(revocation) {
166
+ const key = keyOf(revocation);
167
+ const known = this.#revoked.get(key);
168
+ if (known === undefined || revocation.at > known)
169
+ this.#revoked.set(key, revocation.at);
170
+ this.#sweepExpired();
171
+ const watch = this.#watches.get(key);
172
+ if (!watch || revocation.at < watch.since)
173
+ return;
174
+ this.#watches.delete(key);
175
+ for (const ref of watch.controllers)
176
+ ref.deref()?.abort();
177
+ }
178
+ #abortIfRevoked(controller, notices, since) {
179
+ const floor = Math.max(since, this.#now() - this.#horizonMs);
180
+ if (this.#mode === "following") {
181
+ const hit = notices.some((notice) => (this.#revoked.get(keyOf(notice)) ?? -1) >= floor);
182
+ if (hit)
183
+ controller.abort();
184
+ return;
185
+ }
186
+ Promise.all(notices.map((notice) => this.#lookup(notice))).then((found) => {
187
+ if (found.some((at) => at !== undefined && at >= floor))
188
+ controller.abort();
189
+ }, () => controller.abort());
190
+ }
191
+ #sweepExpired() {
192
+ const now = this.#now();
193
+ if (now - this.#sweptAt < this.#horizonMs)
194
+ return;
195
+ this.#sweptAt = now;
196
+ for (const [key, at] of this.#revoked) {
197
+ if (at < now - this.#horizonMs)
198
+ this.#revoked.delete(key);
199
+ }
200
+ }
201
+ #sweepWatch(key) {
202
+ const watch = this.#watches.get(key);
203
+ if (!watch)
204
+ return;
205
+ for (const ref of watch.controllers)
206
+ if (!ref.deref())
207
+ watch.controllers.delete(ref);
208
+ if (watch.controllers.size === 0)
209
+ this.#watches.delete(key);
210
+ }
211
+ async #lookup(notice) {
212
+ const rows = rowsOf(await this.#database.read().execute(sql `
213
+ select revoked_at from parlor_auth_revocations
214
+ where kind = ${notice.kind} and id = ${notice.id}`));
215
+ const row = rows[0];
216
+ return row ? new Date(row.revoked_at).getTime() : undefined;
217
+ }
218
+ async #load(since) {
219
+ const rows = rowsOf(await this.#database.read().execute(sql `
220
+ select kind, id, revoked_at from parlor_auth_revocations
221
+ where revoked_at >= ${new Date(since).toISOString()}`));
222
+ return rows.map((row) => ({
223
+ kind: row.kind,
224
+ id: row.id,
225
+ at: new Date(row.revoked_at).getTime(),
226
+ }));
227
+ }
228
+ async #prune() {
229
+ const cutoff = new Date(this.#now() - this.#horizonMs).toISOString();
230
+ await this.#database.read().execute(sql `delete from parlor_auth_revocations where revoked_at < ${cutoff}`);
231
+ }
232
+ }
233
+ //# sourceMappingURL=revocation.js.map