@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,403 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { decodeBase64url, enrollmentBytes } from "@parlor/capabilities/encoding";
3
+ import { ActionError, asId, defineAction, } from "@parlor/core";
4
+ import { createRegistry } from "@parlor/core/registry";
5
+ import { sql } from "drizzle-orm";
6
+ import { z } from "zod";
7
+ import { requireChat, signedIn } from "./account.js";
8
+ import { jwkOf, rowsOf } from "./database.js";
9
+ import { verifyEs256 } from "./es256.js";
10
+ import { LINK_CODE_LENGTH, normalizeLinkCode, removeStatement, verifyStatement, vouchStatement, } from "./statements.js";
11
+ const parsed = (value) => typeof value === "string" ? JSON.parse(value) : value;
12
+ export function createDeviceRegistry(database) {
13
+ return {
14
+ async get(userId, deviceId) {
15
+ const [row] = rowsOf(await database.read().execute(sql `
16
+ select id, user_id, name, platform, signing_key, presence_key, vouches,
17
+ (extract(epoch from created_at) * 1000)::bigint as created_ms
18
+ from parlor_auth_devices
19
+ where id = ${deviceId} and user_id = ${userId} and revoked_at is null`));
20
+ if (!row)
21
+ return undefined;
22
+ const presence = parsed(row.presence_key);
23
+ const vouches = parsed(row.vouches) ?? [];
24
+ return {
25
+ id: asId("device", row.id),
26
+ userId: asId("user", row.user_id),
27
+ name: row.name,
28
+ platform: row.platform,
29
+ signingKey: jwkOf(row.signing_key),
30
+ presenceKey: presence ?? undefined,
31
+ ...(vouches.length > 0 ? { vouches } : {}),
32
+ createdAt: Number(row.created_ms),
33
+ };
34
+ },
35
+ };
36
+ }
37
+ export const DEVICES_TAG = "parlor_auth_devices";
38
+ export const DEVICES_MIGRATION = {
39
+ id: "auth/0004_devices",
40
+ sql: `
41
+ create table parlor_auth_devices (
42
+ id text primary key,
43
+ user_id text not null,
44
+ name text not null,
45
+ platform text not null,
46
+ signing_key jsonb not null,
47
+ presence_key jsonb,
48
+ created_at timestamptz not null default now(),
49
+ revoked_at timestamptz
50
+ );
51
+ create index parlor_auth_devices_user on parlor_auth_devices (user_id);
52
+ create unique index parlor_auth_devices_signing_key on parlor_auth_devices
53
+ (user_id, (signing_key->>'x'), (signing_key->>'y')) where revoked_at is null;
54
+
55
+ create table parlor_auth_device_challenges (
56
+ challenge text primary key,
57
+ user_id text not null,
58
+ expires_at timestamptz not null
59
+ )`,
60
+ };
61
+ export const DEVICE_LINKING_MIGRATION = {
62
+ id: "auth/0007_device_linking",
63
+ sql: `
64
+ alter table parlor_auth_devices add column vouches jsonb not null default '[]'::jsonb;
65
+ alter table parlor_auth_devices add column link_code text;
66
+ alter table parlor_auth_devices add column link_expires_at timestamptz;
67
+ create index parlor_auth_devices_link_code on parlor_auth_devices (user_id, link_code)
68
+ where link_code is not null`,
69
+ };
70
+ const CHALLENGE_TTL_MS = 5 * 60_000;
71
+ const LINK_TTL_MS = 15 * 60_000;
72
+ const VOUCH_MAX_AGE_MS = 5 * 60_000;
73
+ const linkCodeInput = z
74
+ .string()
75
+ .min(LINK_CODE_LENGTH)
76
+ .max(32)
77
+ .transform(normalizeLinkCode)
78
+ .refine((code) => /^[0-9A-HJKMNP-TV-Z]{10}$/.test(code), "Enter the 10-character code.");
79
+ const P256 = { name: "ECDSA", namedCurve: "P-256" };
80
+ const jwk = z.record(z.string(), z.unknown());
81
+ const deviceSignature = z.object({
82
+ keyId: z.string(),
83
+ presence: z.enum(["none", "user"]),
84
+ format: z.enum(["es256-raw", "webauthn"]),
85
+ signature: z.string(),
86
+ authenticatorData: z.string().optional(),
87
+ clientDataJSON: z.string().optional(),
88
+ });
89
+ const presenceInput = z.discriminatedUnion("format", [
90
+ z.object({
91
+ format: z.literal("webauthn"),
92
+ credentialId: z.string().min(1).max(1024),
93
+ rpId: z.string().min(1).max(255),
94
+ publicKey: jwk,
95
+ attestation: z.string().min(1),
96
+ }),
97
+ z.object({
98
+ format: z.literal("es256-raw"),
99
+ keyId: z.string().min(1),
100
+ publicKey: jwk,
101
+ }),
102
+ ]);
103
+ function databaseOf(host) {
104
+ if (!host.database)
105
+ throw new Error("Devices need a database.");
106
+ return host.database;
107
+ }
108
+ const invalid = (message) => new ActionError("invalid_input", message);
109
+ async function pointOf(key) {
110
+ try {
111
+ const imported = await crypto.subtle.importKey("jwk", key, P256, true, [
112
+ "verify",
113
+ ]);
114
+ const { kty, crv, x, y } = await crypto.subtle.exportKey("jwk", imported);
115
+ return { kty: kty, crv: crv, x: x, y: y };
116
+ }
117
+ catch {
118
+ throw invalid("A device key must be an ECDSA P-256 public key.");
119
+ }
120
+ }
121
+ async function presenceOf(input) {
122
+ if (!input)
123
+ return undefined;
124
+ const publicKey = await pointOf(input.publicKey);
125
+ if (input.format === "webauthn") {
126
+ const { credentialId, rpId, attestation } = input;
127
+ return {
128
+ stored: { format: "webauthn", credentialId, rpId, publicKey },
129
+ proven: { format: "webauthn", credentialId, publicKey, attestation },
130
+ };
131
+ }
132
+ const key = { format: "es256-raw", keyId: input.keyId, publicKey };
133
+ return { stored: key, proven: key };
134
+ }
135
+ const isoOf = (value) => new Date(value).toISOString();
136
+ /**
137
+ * A device showing a link code, found by the code (normalized) or, from its QR, by its id;
138
+ * only `userId`'s own device, still within its 15 minutes.
139
+ */
140
+ export async function findPendingDevice(database, userId, by) {
141
+ const match = "code" in by
142
+ ? sql `link_code = ${normalizeLinkCode(by.code)}`
143
+ : sql `id = ${by.device} and link_code is not null`;
144
+ const [row] = rowsOf(await database.read().execute(sql `
145
+ select id, user_id, name, platform, signing_key, presence_key, vouches, 0 as created_ms
146
+ from parlor_auth_devices
147
+ where user_id = ${userId} and ${match}
148
+ and link_expires_at > ${new Date().toISOString()} and revoked_at is null`));
149
+ if (!row)
150
+ return undefined;
151
+ const presence = parsed(row.presence_key);
152
+ return {
153
+ id: asId("device", row.id),
154
+ name: row.name,
155
+ platform: row.platform,
156
+ signingKey: jwkOf(row.signing_key),
157
+ ...(presence ? { presenceKey: presence } : {}),
158
+ };
159
+ }
160
+ export function deviceAccountActions(deps) {
161
+ return createRegistry({
162
+ devices: defineAction({
163
+ description: "List the devices that sign this account's chat messages.",
164
+ input: z.object({}),
165
+ reads: [DEVICES_TAG],
166
+ run: async (_input, ctx) => {
167
+ const user = signedIn(ctx);
168
+ const rows = rowsOf(await databaseOf(deps.host).read().execute(sql `
169
+ select id, name, platform, created_at, jsonb_array_length(vouches) > 0 as vouched,
170
+ coalesce(link_expires_at > ${new Date().toISOString()}, false) as linking,
171
+ vouches -> -1 ->> 'voucher' as linked_by
172
+ from parlor_auth_devices
173
+ where user_id = ${user.userId} and revoked_at is null order by created_at, id`));
174
+ return rows.map((row) => ({
175
+ id: asId("device", row.id),
176
+ name: row.name,
177
+ platform: row.platform,
178
+ current: row.id === user.deviceId,
179
+ createdAt: isoOf(row.created_at),
180
+ vouched: row.vouched,
181
+ linking: row.linking,
182
+ ...(row.linked_by ? { linkedBy: row.linked_by } : {}),
183
+ }));
184
+ },
185
+ }),
186
+ deviceChallenge: defineAction({
187
+ description: "A single-use challenge for enrolling this device.",
188
+ input: z.object({}),
189
+ run: async (_input, ctx) => {
190
+ const user = signedIn(ctx);
191
+ await requireChat(deps.host, ctx);
192
+ const challenge = randomBytes(32).toString("base64url");
193
+ await databaseOf(deps.host).transaction(async (handle) => {
194
+ const tx = handle;
195
+ await tx.execute(sql `delete from parlor_auth_device_challenges where expires_at < ${new Date().toISOString()}`);
196
+ await tx.execute(sql `
197
+ insert into parlor_auth_device_challenges (challenge, user_id, expires_at)
198
+ values (${challenge}, ${user.userId},
199
+ ${new Date(Date.now() + CHALLENGE_TTL_MS).toISOString()})`);
200
+ });
201
+ return { challenge };
202
+ },
203
+ }),
204
+ enrollDevice: defineAction({
205
+ description: "Register this device's public keys, proven by signing a challenge.",
206
+ input: z.object({
207
+ name: z.string().min(1).max(80),
208
+ platform: z.string().min(1).max(32),
209
+ challenge: z.string().min(1).max(256),
210
+ signing: z.object({ keyId: z.string().min(1), publicKey: jwk }),
211
+ presence: presenceInput.optional(),
212
+ proof: deviceSignature,
213
+ }),
214
+ run: async (input, ctx) => {
215
+ const user = signedIn(ctx);
216
+ await requireChat(deps.host, ctx);
217
+ const signing = await pointOf(input.signing.publicKey);
218
+ const presence = await presenceOf(input.presence);
219
+ const { proof } = input;
220
+ let proven = false;
221
+ try {
222
+ proven =
223
+ proof.format === "es256-raw" &&
224
+ proof.presence === "none" &&
225
+ (await verifyEs256(signing, decodeBase64url(proof.signature), enrollmentBytes({
226
+ challenge: decodeBase64url(input.challenge),
227
+ signing,
228
+ presence: presence?.proven,
229
+ })));
230
+ }
231
+ catch {
232
+ proven = false;
233
+ }
234
+ if (!proven)
235
+ throw invalid("The device could not prove it holds its keys.");
236
+ const enrolled = await databaseOf(deps.host).transaction(async (handle) => {
237
+ const tx = handle;
238
+ const consumed = rowsOf(await tx.execute(sql `
239
+ delete from parlor_auth_device_challenges
240
+ where challenge = ${input.challenge} and user_id = ${user.userId}
241
+ and expires_at >= ${new Date().toISOString()}
242
+ returning challenge`));
243
+ if (consumed.length === 0)
244
+ throw invalid("This challenge is unknown or expired.");
245
+ const id = asId("device", randomBytes(16).toString("base64url"));
246
+ const [inserted] = rowsOf(await tx.execute(sql `
247
+ insert into parlor_auth_devices (id, user_id, name, platform, signing_key, presence_key)
248
+ values (${id}, ${user.userId}, ${input.name}, ${input.platform},
249
+ ${JSON.stringify(signing)}::jsonb,
250
+ ${presence ? JSON.stringify(presence.stored) : null}::jsonb)
251
+ on conflict (user_id, (signing_key->>'x'), (signing_key->>'y'))
252
+ where revoked_at is null do nothing
253
+ returning id`));
254
+ if (!inserted) {
255
+ const [existing] = rowsOf(await tx.execute(sql `
256
+ select id from parlor_auth_devices
257
+ where user_id = ${user.userId} and revoked_at is null
258
+ and signing_key->>'x' = ${signing.x} and signing_key->>'y' = ${signing.y}`));
259
+ return { id: asId("device", existing.id), added: false };
260
+ }
261
+ return { id, added: true };
262
+ });
263
+ if (enrolled.added) {
264
+ await deps.host.announce(ctx.scope, [[DEVICES_TAG, enrolled.id]]);
265
+ }
266
+ return { id: enrolled.id };
267
+ },
268
+ }),
269
+ startDeviceLink: defineAction({
270
+ description: "Show this device's link code for 15 minutes, so a linked device can find it.",
271
+ input: z.object({ device: z.string().min(1), code: linkCodeInput }),
272
+ run: async ({ device, code }, ctx) => {
273
+ const user = signedIn(ctx);
274
+ await requireChat(deps.host, ctx);
275
+ const expiresAt = new Date(Date.now() + LINK_TTL_MS).toISOString();
276
+ const started = rowsOf(await databaseOf(deps.host).transaction((handle) => handle.execute(sql `
277
+ update parlor_auth_devices set link_code = ${code}, link_expires_at = ${expiresAt}
278
+ where id = ${device} and user_id = ${user.userId} and revoked_at is null
279
+ returning id`)));
280
+ if (started.length === 0)
281
+ throw new ActionError("not_found", `No device ${device}`);
282
+ return { expiresAt };
283
+ },
284
+ }),
285
+ resolveDeviceLink: defineAction({
286
+ description: "Find this account's device showing a link code, to check it and vouch for it.",
287
+ input: z.union([
288
+ z.object({ code: linkCodeInput }),
289
+ z.object({ device: z.string().min(1).max(64) }),
290
+ ]),
291
+ run: async (input, ctx) => {
292
+ const user = signedIn(ctx);
293
+ await requireChat(deps.host, ctx);
294
+ const found = await findPendingDevice(databaseOf(deps.host), user.userId, input);
295
+ if (!found) {
296
+ throw new ActionError("not_found", "No device of yours shows this code. Check it, or show a new code on the device.");
297
+ }
298
+ return found;
299
+ },
300
+ }),
301
+ vouchDevice: defineAction({
302
+ description: "Vouch, with this device's presence key, that a device showing a link code is the user's.",
303
+ input: z.object({
304
+ device: z.string().min(1),
305
+ voucher: z.string().min(1),
306
+ app: z.string().url(),
307
+ commitment: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
308
+ at: z.number(),
309
+ signature: deviceSignature,
310
+ }),
311
+ run: async (input, ctx) => {
312
+ const user = signedIn(ctx);
313
+ await requireChat(deps.host, ctx);
314
+ const voucher = await deps.registry.get(user.userId, asId("device", input.voucher));
315
+ if (!voucher?.presenceKey) {
316
+ throw new ActionError("forbidden", "Only a linked device that can confirm it is you can link another device.");
317
+ }
318
+ const outcome = await verifyStatement({
319
+ statement: vouchStatement(input.app, input.voucher, input.device, input.commitment, input.at),
320
+ deviceId: voucher.id,
321
+ signature: input.signature,
322
+ }, {
323
+ deviceId: voucher.id,
324
+ signingKey: voucher.signingKey,
325
+ presenceKey: voucher.presenceKey,
326
+ }, { requirePresence: true, now: Date.now(), maxAgeMs: VOUCH_MAX_AGE_MS });
327
+ if (outcome !== "valid")
328
+ throw invalid("This confirmation did not check out.");
329
+ const vouch = {
330
+ voucher: voucher.id,
331
+ commitment: input.commitment,
332
+ at: input.at,
333
+ signature: input.signature,
334
+ };
335
+ const vouched = rowsOf(await databaseOf(deps.host).transaction((handle) => handle.execute(sql `
336
+ update parlor_auth_devices
337
+ set vouches = vouches || ${JSON.stringify([vouch])}::jsonb,
338
+ link_code = null, link_expires_at = null
339
+ where id = ${input.device} and user_id = ${user.userId} and revoked_at is null
340
+ and link_code is not null and link_expires_at > ${new Date().toISOString()}
341
+ returning id`)));
342
+ if (vouched.length === 0) {
343
+ throw new ActionError("not_found", "That device's code expired. Show a new code on it and try again.");
344
+ }
345
+ await deps.host.announce(ctx.scope, [[DEVICES_TAG, input.device]]);
346
+ return { id: input.device };
347
+ },
348
+ }),
349
+ removeDevice: defineAction({
350
+ description: "Remove a device, confirmed with this device's presence key; no computer accepts its signatures afterwards.",
351
+ input: z.object({
352
+ device: z.string().min(1),
353
+ actor: z.string().min(1),
354
+ app: z.string().url(),
355
+ at: z.number(),
356
+ signature: deviceSignature,
357
+ }),
358
+ run: async (input, ctx) => {
359
+ const user = signedIn(ctx);
360
+ const actor = await deps.registry.get(user.userId, asId("device", input.actor));
361
+ if (!actor?.presenceKey) {
362
+ throw new ActionError("forbidden", "Only a linked device that can confirm it is you can remove a device.");
363
+ }
364
+ const outcome = await verifyStatement({
365
+ statement: removeStatement(input.app, input.actor, input.device, input.at),
366
+ deviceId: actor.id,
367
+ signature: input.signature,
368
+ }, {
369
+ deviceId: actor.id,
370
+ signingKey: actor.signingKey,
371
+ presenceKey: actor.presenceKey,
372
+ }, { requirePresence: true, now: Date.now(), maxAgeMs: VOUCH_MAX_AGE_MS });
373
+ if (outcome !== "valid")
374
+ throw invalid("This confirmation did not check out.");
375
+ const removed = rowsOf(await databaseOf(deps.host).transaction((handle) => handle.execute(sql `
376
+ update parlor_auth_devices set revoked_at = now(), link_code = null
377
+ where id = ${input.device} and user_id = ${user.userId} and revoked_at is null
378
+ returning id`)));
379
+ if (removed.length === 0) {
380
+ throw new ActionError("not_found", `No device ${input.device}`);
381
+ }
382
+ await deps.host.announce(ctx.scope, [[DEVICES_TAG, input.device]]);
383
+ return { id: input.device };
384
+ },
385
+ }),
386
+ revokeDevice: defineAction({
387
+ description: "Deprecated: use removeDevice, which a linked device confirms. Remove a device; no computer accepts its signatures afterwards.",
388
+ input: z.object({ id: z.string().min(1) }),
389
+ run: async ({ id }, ctx) => {
390
+ const user = signedIn(ctx);
391
+ const revoked = rowsOf(await databaseOf(deps.host).transaction((handle) => handle.execute(sql `
392
+ update parlor_auth_devices set revoked_at = now()
393
+ where id = ${id} and user_id = ${user.userId} and revoked_at is null
394
+ returning id`)));
395
+ if (revoked.length === 0)
396
+ throw new ActionError("not_found", `No device ${id}`);
397
+ await deps.host.announce(ctx.scope, [[DEVICES_TAG, id]]);
398
+ return { id };
399
+ },
400
+ }),
401
+ });
402
+ }
403
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../src/devices.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\n\nimport type { PresencePublicKey } from \"@parlor/capabilities\";\nimport { decodeBase64url, enrollmentBytes } from \"@parlor/capabilities/encoding\";\nimport {\n ActionError,\n asId,\n type DataProvider,\n defineAction,\n type DeviceId,\n type UserId,\n} from \"@parlor/core\";\nimport { type ActionsOf, createRegistry } from \"@parlor/core/registry\";\nimport type {\n DeviceKeys,\n DeviceVouch,\n Migration,\n PluginHost,\n PresenceKey,\n} from \"@parlor/server\";\nimport { sql } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { requireChat, signedIn } from \"./account.js\";\nimport { type Database, jwkOf, rowsOf } from \"./database.js\";\nimport { verifyEs256 } from \"./es256.js\";\nimport {\n LINK_CODE_LENGTH,\n normalizeLinkCode,\n removeStatement,\n verifyStatement,\n vouchStatement,\n} from \"./statements.js\";\n\nexport type { PresenceKey } from \"@parlor/server\";\n\nexport interface RegisteredDevice extends DeviceKeys {\n readonly userId: UserId;\n readonly createdAt: number;\n readonly revokedAt?: number | undefined;\n}\n\nexport interface DeviceRegistry {\n /** A revoked device reads as undefined, so no host accepts its signatures again. */\n get(userId: UserId, deviceId: DeviceId): Promise<RegisteredDevice | undefined>;\n}\n\ninterface DeviceRow {\n readonly id: string;\n readonly user_id: string;\n readonly name: string;\n readonly platform: string;\n readonly signing_key: JsonWebKey | string;\n readonly presence_key: PresenceKey | string | null;\n readonly created_ms: string | number;\n readonly vouches: DeviceVouch[] | string | null;\n}\n\nconst parsed = <T>(value: T | string | null): T | null =>\n typeof value === \"string\" ? (JSON.parse(value) as T) : value;\n\nexport function createDeviceRegistry(database: DataProvider): DeviceRegistry {\n return {\n async get(userId, deviceId) {\n const [row] = rowsOf<DeviceRow>(\n await (database.read() as Database).execute(sql`\n select id, user_id, name, platform, signing_key, presence_key, vouches,\n (extract(epoch from created_at) * 1000)::bigint as created_ms\n from parlor_auth_devices\n where id = ${deviceId} and user_id = ${userId} and revoked_at is null`),\n );\n if (!row) return undefined;\n const presence = parsed(row.presence_key);\n const vouches = parsed(row.vouches) ?? [];\n return {\n id: asId(\"device\", row.id),\n userId: asId(\"user\", row.user_id),\n name: row.name,\n platform: row.platform,\n signingKey: jwkOf(row.signing_key),\n presenceKey: presence ?? undefined,\n ...(vouches.length > 0 ? { vouches } : {}),\n createdAt: Number(row.created_ms),\n };\n },\n };\n}\n\nexport const DEVICES_TAG = \"parlor_auth_devices\";\n\nexport const DEVICES_MIGRATION: Migration = {\n id: \"auth/0004_devices\",\n sql: `\n create table parlor_auth_devices (\n id text primary key,\n user_id text not null,\n name text not null,\n platform text not null,\n signing_key jsonb not null,\n presence_key jsonb,\n created_at timestamptz not null default now(),\n revoked_at timestamptz\n );\n create index parlor_auth_devices_user on parlor_auth_devices (user_id);\n create unique index parlor_auth_devices_signing_key on parlor_auth_devices\n (user_id, (signing_key->>'x'), (signing_key->>'y')) where revoked_at is null;\n\n create table parlor_auth_device_challenges (\n challenge text primary key,\n user_id text not null,\n expires_at timestamptz not null\n )`,\n};\n\nexport const DEVICE_LINKING_MIGRATION: Migration = {\n id: \"auth/0007_device_linking\",\n sql: `\n alter table parlor_auth_devices add column vouches jsonb not null default '[]'::jsonb;\n alter table parlor_auth_devices add column link_code text;\n alter table parlor_auth_devices add column link_expires_at timestamptz;\n create index parlor_auth_devices_link_code on parlor_auth_devices (user_id, link_code)\n where link_code is not null`,\n};\n\nexport interface ConnectedDevice {\n readonly id: DeviceId;\n readonly name: string;\n readonly platform: string;\n readonly current: boolean;\n readonly createdAt: string;\n /** A linked device vouched for it; the computer pins it on its first prompt. */\n readonly vouched: boolean;\n /** It is showing a link code now. */\n readonly linking: boolean;\n /** The device whose vouch linked it last. */\n readonly linkedBy?: string | undefined;\n}\n\n/** A device waiting to be linked, as the one linking it checks it against the code. */\nexport interface PendingDevice {\n readonly id: DeviceId;\n readonly name: string;\n readonly platform: string;\n readonly signingKey: JsonWebKey;\n readonly presenceKey?: PresenceKey | undefined;\n}\n\nconst CHALLENGE_TTL_MS = 5 * 60_000;\nconst LINK_TTL_MS = 15 * 60_000;\nconst VOUCH_MAX_AGE_MS = 5 * 60_000;\nconst linkCodeInput = z\n .string()\n .min(LINK_CODE_LENGTH)\n .max(32)\n .transform(normalizeLinkCode)\n .refine(\n (code) => /^[0-9A-HJKMNP-TV-Z]{10}$/.test(code),\n \"Enter the 10-character code.\",\n );\nconst P256 = { name: \"ECDSA\", namedCurve: \"P-256\" } as const;\n\nconst jwk = z.record(z.string(), z.unknown());\n\nconst deviceSignature = z.object({\n keyId: z.string(),\n presence: z.enum([\"none\", \"user\"]),\n format: z.enum([\"es256-raw\", \"webauthn\"]),\n signature: z.string(),\n authenticatorData: z.string().optional(),\n clientDataJSON: z.string().optional(),\n});\n\nconst presenceInput = z.discriminatedUnion(\"format\", [\n z.object({\n format: z.literal(\"webauthn\"),\n credentialId: z.string().min(1).max(1024),\n rpId: z.string().min(1).max(255),\n publicKey: jwk,\n attestation: z.string().min(1),\n }),\n z.object({\n format: z.literal(\"es256-raw\"),\n keyId: z.string().min(1),\n publicKey: jwk,\n }),\n]);\n\nexport interface DeviceActionDeps {\n readonly host: PluginHost;\n readonly registry: DeviceRegistry;\n}\n\ninterface DeviceListRow {\n readonly id: string;\n readonly name: string;\n readonly platform: string;\n readonly created_at: string | Date;\n readonly vouched: boolean;\n readonly linking: boolean;\n readonly linked_by: string | null;\n}\n\nfunction databaseOf(host: PluginHost): DataProvider {\n if (!host.database) throw new Error(\"Devices need a database.\");\n return host.database;\n}\n\nconst invalid = (message: string) => new ActionError(\"invalid_input\", message);\n\nasync function pointOf(key: Record<string, unknown>): Promise<JsonWebKey> {\n try {\n const imported = await crypto.subtle.importKey(\"jwk\", key as JsonWebKey, P256, true, [\n \"verify\",\n ]);\n const { kty, crv, x, y } = await crypto.subtle.exportKey(\"jwk\", imported);\n return { kty: kty!, crv: crv!, x: x!, y: y! };\n } catch {\n throw invalid(\"A device key must be an ECDSA P-256 public key.\");\n }\n}\n\ninterface EnrolledPresence {\n readonly stored: PresenceKey;\n readonly proven: PresencePublicKey;\n}\n\nasync function presenceOf(\n input: z.output<typeof presenceInput> | undefined,\n): Promise<EnrolledPresence | undefined> {\n if (!input) return undefined;\n const publicKey = await pointOf(input.publicKey);\n if (input.format === \"webauthn\") {\n const { credentialId, rpId, attestation } = input;\n return {\n stored: { format: \"webauthn\", credentialId, rpId, publicKey },\n proven: { format: \"webauthn\", credentialId, publicKey, attestation },\n };\n }\n const key: PresencePublicKey = { format: \"es256-raw\", keyId: input.keyId, publicKey };\n return { stored: key, proven: key };\n}\n\nconst isoOf = (value: string | Date): string => new Date(value).toISOString();\n\n/**\n * A device showing a link code, found by the code (normalized) or, from its QR, by its id;\n * only `userId`'s own device, still within its 15 minutes.\n */\nexport async function findPendingDevice(\n database: DataProvider,\n userId: UserId,\n by: { readonly code: string } | { readonly device: string },\n): Promise<PendingDevice | undefined> {\n const match =\n \"code\" in by\n ? sql`link_code = ${normalizeLinkCode(by.code)}`\n : sql`id = ${by.device} and link_code is not null`;\n const [row] = rowsOf<DeviceRow>(\n await (database.read() as Database).execute(sql`\n select id, user_id, name, platform, signing_key, presence_key, vouches, 0 as created_ms\n from parlor_auth_devices\n where user_id = ${userId} and ${match}\n and link_expires_at > ${new Date().toISOString()} and revoked_at is null`),\n );\n if (!row) return undefined;\n const presence = parsed(row.presence_key);\n return {\n id: asId(\"device\", row.id),\n name: row.name,\n platform: row.platform,\n signingKey: jwkOf(row.signing_key),\n ...(presence ? { presenceKey: presence } : {}),\n };\n}\n\nexport function deviceAccountActions(deps: DeviceActionDeps) {\n return createRegistry({\n devices: defineAction({\n description: \"List the devices that sign this account's chat messages.\",\n input: z.object({}),\n reads: [DEVICES_TAG],\n run: async (_input, ctx): Promise<ConnectedDevice[]> => {\n const user = signedIn(ctx);\n const rows = rowsOf<DeviceListRow>(\n await (databaseOf(deps.host).read() as Database).execute(sql`\n select id, name, platform, created_at, jsonb_array_length(vouches) > 0 as vouched,\n coalesce(link_expires_at > ${new Date().toISOString()}, false) as linking,\n vouches -> -1 ->> 'voucher' as linked_by\n from parlor_auth_devices\n where user_id = ${user.userId} and revoked_at is null order by created_at, id`),\n );\n return rows.map((row) => ({\n id: asId(\"device\", row.id),\n name: row.name,\n platform: row.platform,\n current: row.id === user.deviceId,\n createdAt: isoOf(row.created_at),\n vouched: row.vouched,\n linking: row.linking,\n ...(row.linked_by ? { linkedBy: row.linked_by } : {}),\n }));\n },\n }),\n deviceChallenge: defineAction({\n description: \"A single-use challenge for enrolling this device.\",\n input: z.object({}),\n run: async (_input, ctx): Promise<{ readonly challenge: string }> => {\n const user = signedIn(ctx);\n await requireChat(deps.host, ctx);\n const challenge = randomBytes(32).toString(\"base64url\");\n await databaseOf(deps.host).transaction(async (handle) => {\n const tx = handle as Database;\n await tx.execute(\n sql`delete from parlor_auth_device_challenges where expires_at < ${new Date().toISOString()}`,\n );\n await tx.execute(sql`\n insert into parlor_auth_device_challenges (challenge, user_id, expires_at)\n values (${challenge}, ${user.userId},\n ${new Date(Date.now() + CHALLENGE_TTL_MS).toISOString()})`);\n });\n return { challenge };\n },\n }),\n enrollDevice: defineAction({\n description: \"Register this device's public keys, proven by signing a challenge.\",\n input: z.object({\n name: z.string().min(1).max(80),\n platform: z.string().min(1).max(32),\n challenge: z.string().min(1).max(256),\n signing: z.object({ keyId: z.string().min(1), publicKey: jwk }),\n presence: presenceInput.optional(),\n proof: deviceSignature,\n }),\n run: async (input, ctx): Promise<{ readonly id: DeviceId }> => {\n const user = signedIn(ctx);\n await requireChat(deps.host, ctx);\n const signing = await pointOf(input.signing.publicKey);\n const presence = await presenceOf(input.presence);\n const { proof } = input;\n let proven = false;\n try {\n proven =\n proof.format === \"es256-raw\" &&\n proof.presence === \"none\" &&\n (await verifyEs256(\n signing,\n decodeBase64url(proof.signature),\n enrollmentBytes({\n challenge: decodeBase64url(input.challenge),\n signing,\n presence: presence?.proven,\n }),\n ));\n } catch {\n proven = false;\n }\n if (!proven) throw invalid(\"The device could not prove it holds its keys.\");\n const enrolled = await databaseOf(deps.host).transaction(async (handle) => {\n const tx = handle as Database;\n const consumed = rowsOf<{ readonly challenge: string }>(\n await tx.execute(sql`\n delete from parlor_auth_device_challenges\n where challenge = ${input.challenge} and user_id = ${user.userId}\n and expires_at >= ${new Date().toISOString()}\n returning challenge`),\n );\n if (consumed.length === 0)\n throw invalid(\"This challenge is unknown or expired.\");\n const id = asId(\"device\", randomBytes(16).toString(\"base64url\"));\n const [inserted] = rowsOf<{ readonly id: string }>(\n await tx.execute(sql`\n insert into parlor_auth_devices (id, user_id, name, platform, signing_key, presence_key)\n values (${id}, ${user.userId}, ${input.name}, ${input.platform},\n ${JSON.stringify(signing)}::jsonb,\n ${presence ? JSON.stringify(presence.stored) : null}::jsonb)\n on conflict (user_id, (signing_key->>'x'), (signing_key->>'y'))\n where revoked_at is null do nothing\n returning id`),\n );\n if (!inserted) {\n const [existing] = rowsOf<{ readonly id: string }>(\n await tx.execute(sql`\n select id from parlor_auth_devices\n where user_id = ${user.userId} and revoked_at is null\n and signing_key->>'x' = ${signing.x!} and signing_key->>'y' = ${signing.y!}`),\n );\n return { id: asId(\"device\", existing!.id), added: false };\n }\n return { id, added: true };\n });\n if (enrolled.added) {\n await deps.host.announce(ctx.scope, [[DEVICES_TAG, enrolled.id]]);\n }\n return { id: enrolled.id };\n },\n }),\n startDeviceLink: defineAction({\n description:\n \"Show this device's link code for 15 minutes, so a linked device can find it.\",\n input: z.object({ device: z.string().min(1), code: linkCodeInput }),\n run: async ({ device, code }, ctx): Promise<{ readonly expiresAt: string }> => {\n const user = signedIn(ctx);\n await requireChat(deps.host, ctx);\n const expiresAt = new Date(Date.now() + LINK_TTL_MS).toISOString();\n const started = rowsOf<{ readonly id: string }>(\n await databaseOf(deps.host).transaction((handle) =>\n (handle as Database).execute(sql`\n update parlor_auth_devices set link_code = ${code}, link_expires_at = ${expiresAt}\n where id = ${device} and user_id = ${user.userId} and revoked_at is null\n returning id`),\n ),\n );\n if (started.length === 0)\n throw new ActionError(\"not_found\", `No device ${device}`);\n return { expiresAt };\n },\n }),\n resolveDeviceLink: defineAction({\n description:\n \"Find this account's device showing a link code, to check it and vouch for it.\",\n input: z.union([\n z.object({ code: linkCodeInput }),\n z.object({ device: z.string().min(1).max(64) }),\n ]),\n run: async (input, ctx): Promise<PendingDevice> => {\n const user = signedIn(ctx);\n await requireChat(deps.host, ctx);\n const found = await findPendingDevice(databaseOf(deps.host), user.userId, input);\n if (!found) {\n throw new ActionError(\n \"not_found\",\n \"No device of yours shows this code. Check it, or show a new code on the device.\",\n );\n }\n return found;\n },\n }),\n vouchDevice: defineAction({\n description:\n \"Vouch, with this device's presence key, that a device showing a link code is the user's.\",\n input: z.object({\n device: z.string().min(1),\n voucher: z.string().min(1),\n app: z.string().url(),\n commitment: z.string().regex(/^[A-Za-z0-9_-]{43}$/),\n at: z.number(),\n signature: deviceSignature,\n }),\n run: async (input, ctx): Promise<{ readonly id: string }> => {\n const user = signedIn(ctx);\n await requireChat(deps.host, ctx);\n const voucher = await deps.registry.get(\n user.userId,\n asId(\"device\", input.voucher),\n );\n if (!voucher?.presenceKey) {\n throw new ActionError(\n \"forbidden\",\n \"Only a linked device that can confirm it is you can link another device.\",\n );\n }\n const outcome = await verifyStatement(\n {\n statement: vouchStatement(\n input.app,\n input.voucher,\n input.device,\n input.commitment,\n input.at,\n ),\n deviceId: voucher.id,\n signature: input.signature as never,\n },\n {\n deviceId: voucher.id,\n signingKey: voucher.signingKey,\n presenceKey: voucher.presenceKey,\n },\n { requirePresence: true, now: Date.now(), maxAgeMs: VOUCH_MAX_AGE_MS },\n );\n if (outcome !== \"valid\") throw invalid(\"This confirmation did not check out.\");\n const vouch: DeviceVouch = {\n voucher: voucher.id,\n commitment: input.commitment,\n at: input.at,\n signature: input.signature,\n };\n const vouched = rowsOf<{ readonly id: string }>(\n await databaseOf(deps.host).transaction((handle) =>\n (handle as Database).execute(sql`\n update parlor_auth_devices\n set vouches = vouches || ${JSON.stringify([vouch])}::jsonb,\n link_code = null, link_expires_at = null\n where id = ${input.device} and user_id = ${user.userId} and revoked_at is null\n and link_code is not null and link_expires_at > ${new Date().toISOString()}\n returning id`),\n ),\n );\n if (vouched.length === 0) {\n throw new ActionError(\n \"not_found\",\n \"That device's code expired. Show a new code on it and try again.\",\n );\n }\n await deps.host.announce(ctx.scope, [[DEVICES_TAG, input.device]]);\n return { id: input.device };\n },\n }),\n removeDevice: defineAction({\n description:\n \"Remove a device, confirmed with this device's presence key; no computer accepts its signatures afterwards.\",\n input: z.object({\n device: z.string().min(1),\n actor: z.string().min(1),\n app: z.string().url(),\n at: z.number(),\n signature: deviceSignature,\n }),\n run: async (input, ctx): Promise<{ readonly id: string }> => {\n const user = signedIn(ctx);\n const actor = await deps.registry.get(user.userId, asId(\"device\", input.actor));\n if (!actor?.presenceKey) {\n throw new ActionError(\n \"forbidden\",\n \"Only a linked device that can confirm it is you can remove a device.\",\n );\n }\n const outcome = await verifyStatement(\n {\n statement: removeStatement(input.app, input.actor, input.device, input.at),\n deviceId: actor.id,\n signature: input.signature as never,\n },\n {\n deviceId: actor.id,\n signingKey: actor.signingKey,\n presenceKey: actor.presenceKey,\n },\n { requirePresence: true, now: Date.now(), maxAgeMs: VOUCH_MAX_AGE_MS },\n );\n if (outcome !== \"valid\") throw invalid(\"This confirmation did not check out.\");\n const removed = rowsOf<{ readonly id: string }>(\n await databaseOf(deps.host).transaction((handle) =>\n (handle as Database).execute(sql`\n update parlor_auth_devices set revoked_at = now(), link_code = null\n where id = ${input.device} and user_id = ${user.userId} and revoked_at is null\n returning id`),\n ),\n );\n if (removed.length === 0) {\n throw new ActionError(\"not_found\", `No device ${input.device}`);\n }\n await deps.host.announce(ctx.scope, [[DEVICES_TAG, input.device]]);\n return { id: input.device };\n },\n }),\n revokeDevice: defineAction({\n description:\n \"Deprecated: use removeDevice, which a linked device confirms. Remove a device; no computer accepts its signatures afterwards.\",\n input: z.object({ id: z.string().min(1) }),\n run: async ({ id }, ctx): Promise<{ readonly id: string }> => {\n const user = signedIn(ctx);\n const revoked = rowsOf<{ readonly id: string }>(\n await databaseOf(deps.host).transaction((handle) =>\n (handle as Database).execute(sql`\n update parlor_auth_devices set revoked_at = now()\n where id = ${id} and user_id = ${user.userId} and revoked_at is null\n returning id`),\n ),\n );\n if (revoked.length === 0) throw new ActionError(\"not_found\", `No device ${id}`);\n await deps.host.announce(ctx.scope, [[DEVICES_TAG, id]]);\n return { id };\n },\n }),\n });\n}\n\nexport type DeviceAccountActions = ActionsOf<ReturnType<typeof deviceAccountActions>>;\n"]}
@@ -0,0 +1,4 @@
1
+ /** WebAuthn signs in ASN.1 DER; WebCrypto verifies the raw `r || s`. */
2
+ export declare function derToRaw(der: Uint8Array): Uint8Array | undefined;
3
+ export declare function verifyEs256(publicKey: JsonWebKey, signature: Uint8Array, data: Uint8Array): Promise<boolean>;
4
+ //# sourceMappingURL=es256.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"es256.d.ts","sourceRoot":"","sources":["../src/es256.ts"],"names":[],"mappings":"AAIA,wEAAwE;AACxE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CA2BhE;AAED,wBAAsB,WAAW,CAC/B,SAAS,EAAE,UAAU,EACrB,SAAS,EAAE,UAAU,EACrB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,OAAO,CAAC,CAQlB"}
package/dist/es256.js ADDED
@@ -0,0 +1,44 @@
1
+ const P256 = { name: "ECDSA", namedCurve: "P-256" };
2
+ const ES256 = { name: "ECDSA", hash: "SHA-256" };
3
+ const P256_SCALAR_BYTES = 32;
4
+ /** WebAuthn signs in ASN.1 DER; WebCrypto verifies the raw `r || s`. */
5
+ export function derToRaw(der) {
6
+ let at = 0;
7
+ const expect = (tag) => der[at++] === tag;
8
+ const length = () => {
9
+ const first = der[at++];
10
+ if (first === undefined || first < 0x80)
11
+ return first;
12
+ if (first !== 0x81)
13
+ return undefined;
14
+ return der[at++];
15
+ };
16
+ const integer = () => {
17
+ if (!expect(0x02))
18
+ return undefined;
19
+ const size = length();
20
+ if (size === undefined)
21
+ return undefined;
22
+ const bytes = der.slice(at, at + size);
23
+ at += size;
24
+ const trimmed = bytes.slice(bytes.findIndex((byte) => byte !== 0));
25
+ if (bytes.length !== size || trimmed.length > P256_SCALAR_BYTES)
26
+ return undefined;
27
+ return trimmed;
28
+ };
29
+ if (!expect(0x30) || length() !== der.length - at)
30
+ return undefined;
31
+ const r = integer();
32
+ const s = integer();
33
+ if (!r || !s || at !== der.length)
34
+ return undefined;
35
+ const raw = new Uint8Array(2 * P256_SCALAR_BYTES);
36
+ raw.set(r, P256_SCALAR_BYTES - r.length);
37
+ raw.set(s, 2 * P256_SCALAR_BYTES - s.length);
38
+ return raw;
39
+ }
40
+ export async function verifyEs256(publicKey, signature, data) {
41
+ const key = await crypto.subtle.importKey("jwk", publicKey, P256, false, ["verify"]);
42
+ return crypto.subtle.verify(ES256, key, new Uint8Array(signature), new Uint8Array(data));
43
+ }
44
+ //# sourceMappingURL=es256.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"es256.js","sourceRoot":"","sources":["../src/es256.ts"],"names":[],"mappings":"AAAA,MAAM,IAAI,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAW,CAAC;AAC7D,MAAM,KAAK,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAW,CAAC;AAC1D,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B,wEAAwE;AACxE,MAAM,UAAU,QAAQ,CAAC,GAAe;IACtC,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC;IAClD,MAAM,MAAM,GAAG,GAAG,EAAE;QAClB,MAAM,KAAK,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QACxB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,IAAI;YAAE,OAAO,KAAK,CAAC;QACtD,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,SAAS,CAAC;QACrC,OAAO,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IACnB,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO,SAAS,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC;QACtB,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC;QACvC,EAAE,IAAI,IAAI,CAAC;QACX,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;QACnE,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,OAAO,CAAC,MAAM,GAAG,iBAAiB;YAAE,OAAO,SAAS,CAAC;QAClF,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,SAAS,CAAC;IACpE,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;IACpB,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC;IACpB,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC;IAClD,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,SAAqB,EACrB,SAAqB,EACrB,IAAgB;IAEhB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrF,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CACzB,KAAK,EACL,GAAG,EACH,IAAI,UAAU,CAAC,SAAS,CAAC,EACzB,IAAI,UAAU,CAAC,IAAI,CAAC,CACrB,CAAC;AACJ,CAAC","sourcesContent":["const P256 = { name: \"ECDSA\", namedCurve: \"P-256\" } as const;\nconst ES256 = { name: \"ECDSA\", hash: \"SHA-256\" } as const;\nconst P256_SCALAR_BYTES = 32;\n\n/** WebAuthn signs in ASN.1 DER; WebCrypto verifies the raw `r || s`. */\nexport function derToRaw(der: Uint8Array): Uint8Array | undefined {\n let at = 0;\n const expect = (tag: number) => der[at++] === tag;\n const length = () => {\n const first = der[at++];\n if (first === undefined || first < 0x80) return first;\n if (first !== 0x81) return undefined;\n return der[at++];\n };\n const integer = () => {\n if (!expect(0x02)) return undefined;\n const size = length();\n if (size === undefined) return undefined;\n const bytes = der.slice(at, at + size);\n at += size;\n const trimmed = bytes.slice(bytes.findIndex((byte) => byte !== 0));\n if (bytes.length !== size || trimmed.length > P256_SCALAR_BYTES) return undefined;\n return trimmed;\n };\n if (!expect(0x30) || length() !== der.length - at) return undefined;\n const r = integer();\n const s = integer();\n if (!r || !s || at !== der.length) return undefined;\n const raw = new Uint8Array(2 * P256_SCALAR_BYTES);\n raw.set(r, P256_SCALAR_BYTES - r.length);\n raw.set(s, 2 * P256_SCALAR_BYTES - s.length);\n return raw;\n}\n\nexport async function verifyEs256(\n publicKey: JsonWebKey,\n signature: Uint8Array,\n data: Uint8Array,\n): Promise<boolean> {\n const key = await crypto.subtle.importKey(\"jwk\", publicKey, P256, false, [\"verify\"]);\n return crypto.subtle.verify(\n ES256,\n key,\n new Uint8Array(signature),\n new Uint8Array(data),\n );\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { type GrantId } from "@parlor/core";
2
+ /** One grant per user and OAuth client; re-authorizing after a revocation mints newer tokens. */
3
+ export declare function grantIdOf(userId: string, clientId: string): GrantId;
4
+ //# sourceMappingURL=grants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grants.d.ts","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,iGAAiG;AACjG,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEnE"}
package/dist/grants.js ADDED
@@ -0,0 +1,6 @@
1
+ import { asId } from "@parlor/core";
2
+ /** One grant per user and OAuth client; re-authorizing after a revocation mints newer tokens. */
3
+ export function grantIdOf(userId, clientId) {
4
+ return asId("grant", `oauth:${userId}:${clientId}`);
5
+ }
6
+ //# sourceMappingURL=grants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grants.js","sourceRoot":"","sources":["../src/grants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,cAAc,CAAC;AAElD,iGAAiG;AACjG,MAAM,UAAU,SAAS,CAAC,MAAc,EAAE,QAAgB;IACxD,OAAO,IAAI,CAAC,OAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,EAAE,CAAC,CAAC;AACtD,CAAC","sourcesContent":["import { asId, type GrantId } from \"@parlor/core\";\n\n/** One grant per user and OAuth client; re-authorizing after a revocation mints newer tokens. */\nexport function grantIdOf(userId: string, clientId: string): GrantId {\n return asId(\"grant\", `oauth:${userId}:${clientId}`);\n}\n"]}