@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,199 @@
1
+ import { canonicalBytes, decodeBase64url, encodeBase64url, } from "@parlor/capabilities/encoding";
2
+ import { derToRaw, verifyEs256 } from "./es256.js";
3
+ const FUTURE_SKEW_MS = 5 * 60_000;
4
+ const USER_PRESENT = 0x01;
5
+ const USER_VERIFIED = 0x04;
6
+ const AUTH_DATA_MIN_BYTES = 37;
7
+ const sha256 = async (bytes) => new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
8
+ const digestOf = async (bytes) => encodeBase64url(await sha256(bytes));
9
+ export async function promptStatement(app, prompt, at) {
10
+ return {
11
+ v: 1,
12
+ kind: "prompt",
13
+ app,
14
+ threadId: prompt.threadId,
15
+ turnId: prompt.turnId,
16
+ agent: `${prompt.agent.hostId}/${prompt.agent.agentId}`,
17
+ digest: await digestOf(new TextEncoder().encode(prompt.text)),
18
+ at,
19
+ };
20
+ }
21
+ export async function answerStatement(app, answer, at) {
22
+ return {
23
+ v: 1,
24
+ kind: "answer",
25
+ app,
26
+ threadId: answer.threadId,
27
+ askId: answer.askId,
28
+ choice: answer.values === undefined
29
+ ? answer.choice
30
+ : await digestOf(canonicalBytes({ choice: answer.choice, values: answer.values })),
31
+ at,
32
+ };
33
+ }
34
+ export async function signedPrompt(app, item) {
35
+ const { signed, agent, text } = item.body;
36
+ if (!signed)
37
+ return undefined;
38
+ return {
39
+ statement: await promptStatement(app, { threadId: item.threadId, turnId: item.turnId, agent, text }, signed.at),
40
+ deviceId: signed.deviceId,
41
+ signature: signed.signature,
42
+ };
43
+ }
44
+ export function statementBytes(statement) {
45
+ return canonicalBytes(statement);
46
+ }
47
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
48
+ export const LINK_CODE_LENGTH = 10;
49
+ /**
50
+ * What a new device shows to be linked: a hash of its keys, so whoever links it checks the
51
+ * keys the backend hands over are the ones this device holds.
52
+ */
53
+ export async function deviceCommitment(app, deviceId, signingKey, presenceKey) {
54
+ const point = (key) => ({ crv: key.crv, kty: key.kty, x: key.x, y: key.y });
55
+ return digestOf(canonicalBytes({
56
+ v: 1,
57
+ kind: "device",
58
+ app: new URL(app).origin,
59
+ deviceId,
60
+ signing: point(signingKey),
61
+ presence: presenceKey ? point(presenceKey) : null,
62
+ }));
63
+ }
64
+ /** The commitment's first 50 bits in Crockford base32, short enough to type. */
65
+ export function linkCode(commitment) {
66
+ const bytes = decodeBase64url(commitment);
67
+ let bits = 0n;
68
+ for (let index = 0; index < 7; index += 1)
69
+ bits = (bits << 8n) | BigInt(bytes[index]);
70
+ bits >>= 6n;
71
+ let code = "";
72
+ for (let index = 0; index < LINK_CODE_LENGTH; index += 1) {
73
+ code = CROCKFORD[Number(bits & 31n)] + code;
74
+ bits >>= 5n;
75
+ }
76
+ return code;
77
+ }
78
+ /** Upper case, no separators, the letters Crockford reads as digits mapped back. */
79
+ export function normalizeLinkCode(typed) {
80
+ return typed
81
+ .toUpperCase()
82
+ .replace(/[\s-]/g, "")
83
+ .replace(/O/g, "0")
84
+ .replace(/[IL]/g, "1");
85
+ }
86
+ export function formatLinkCode(code) {
87
+ return `${code.slice(0, 4)}-${code.slice(4, 8)}-${code.slice(8)}`;
88
+ }
89
+ export function vouchStatement(app, voucher, device, commitment, at) {
90
+ return {
91
+ v: 1,
92
+ kind: "vouch",
93
+ app: new URL(app).origin,
94
+ voucher,
95
+ device,
96
+ commitment,
97
+ at,
98
+ };
99
+ }
100
+ export function removeStatement(app, actor, device, at) {
101
+ return { v: 1, kind: "remove", app: new URL(app).origin, actor, device, at };
102
+ }
103
+ /** The fragment Parlor Host's loopback redirect carries back to the browser that approved `connect`. */
104
+ export const HOST_PAIRING_FRAGMENT = "parlor-host-pairing";
105
+ const pairingKey = (secret) => crypto.subtle.importKey("raw", new Uint8Array(decodeBase64url(secret)), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
106
+ const pairingBytes = (deviceId, signingKey) => canonicalBytes({
107
+ v: 1,
108
+ kind: "pairing",
109
+ deviceId,
110
+ signingKey: {
111
+ crv: signingKey.crv,
112
+ kty: signingKey.kty,
113
+ x: signingKey.x,
114
+ y: signingKey.y,
115
+ },
116
+ });
117
+ /**
118
+ * The browser that approved `connect` gets the host's pairing secret through the loopback
119
+ * redirect, never through the app; this binds it to one device's keys.
120
+ */
121
+ export async function pairingProof(secret, deviceId, signingKey) {
122
+ const mac = await crypto.subtle.sign("HMAC", await pairingKey(secret), new Uint8Array(pairingBytes(deviceId, signingKey)));
123
+ return encodeBase64url(new Uint8Array(mac));
124
+ }
125
+ export async function checkPairingProof(secret, proof, deviceId, signingKey) {
126
+ try {
127
+ return await crypto.subtle.verify("HMAC", await pairingKey(secret), new Uint8Array(decodeBase64url(proof)), new Uint8Array(pairingBytes(deviceId, signingKey)));
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ async function assertionHolds(statement, signature, presenceKey) {
134
+ if (presenceKey?.format !== "webauthn" || signature.presence !== "user")
135
+ return false;
136
+ if (signature.authenticatorData === undefined ||
137
+ signature.clientDataJSON === undefined) {
138
+ return false;
139
+ }
140
+ if (signature.keyId !== presenceKey.credentialId)
141
+ return false;
142
+ if (new URL(statement.app).hostname !== presenceKey.rpId)
143
+ return false;
144
+ const clientDataBytes = decodeBase64url(signature.clientDataJSON);
145
+ const clientData = JSON.parse(new TextDecoder().decode(clientDataBytes));
146
+ const challenge = await digestOf(statementBytes(statement));
147
+ if (clientData.type !== "webauthn.get" ||
148
+ clientData.challenge !== challenge ||
149
+ clientData.origin !== statement.app ||
150
+ clientData.crossOrigin === true) {
151
+ return false;
152
+ }
153
+ const authData = decodeBase64url(signature.authenticatorData);
154
+ const flags = authData[32];
155
+ if (authData.length < AUTH_DATA_MIN_BYTES || flags === undefined)
156
+ return false;
157
+ if ((flags & USER_PRESENT) === 0 || (flags & USER_VERIFIED) === 0)
158
+ return false;
159
+ const rpIdHash = await sha256(new TextEncoder().encode(presenceKey.rpId));
160
+ if (encodeBase64url(authData.slice(0, 32)) !== encodeBase64url(rpIdHash))
161
+ return false;
162
+ const raw = derToRaw(decodeBase64url(signature.signature));
163
+ if (!raw)
164
+ return false;
165
+ const signed = new Uint8Array([...authData, ...(await sha256(clientDataBytes))]);
166
+ return verifyEs256(presenceKey.publicKey, raw, signed);
167
+ }
168
+ async function signatureHolds(statement, signature, pinned) {
169
+ if (signature.format === "webauthn") {
170
+ return assertionHolds(statement, signature, pinned.presenceKey);
171
+ }
172
+ const bytes = statementBytes(statement);
173
+ const raw = decodeBase64url(signature.signature);
174
+ if (signature.presence === "none")
175
+ return verifyEs256(pinned.signingKey, raw, bytes);
176
+ const { presenceKey } = pinned;
177
+ return presenceKey?.format === "es256-raw"
178
+ ? verifyEs256(presenceKey.publicKey, raw, bytes)
179
+ : false;
180
+ }
181
+ export async function verifyStatement(signed, pinned, options) {
182
+ const { statement, signature } = signed;
183
+ if (signed.deviceId !== pinned.deviceId)
184
+ return "invalid";
185
+ if (options.now - statement.at > options.maxAgeMs)
186
+ return "stale";
187
+ if (statement.at - options.now > FUTURE_SKEW_MS)
188
+ return "invalid";
189
+ if (options.requirePresence && signature.presence !== "user") {
190
+ return "presence_required";
191
+ }
192
+ try {
193
+ return (await signatureHolds(statement, signature, pinned)) ? "valid" : "invalid";
194
+ }
195
+ catch {
196
+ return "invalid";
197
+ }
198
+ }
199
+ //# sourceMappingURL=statements.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statements.js","sourceRoot":"","sources":["../src/statements.ts"],"names":[],"mappings":"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","sourcesContent":["import type { DeviceSignature, PresenceSignature } from \"@parlor/capabilities\";\nimport {\n canonicalBytes,\n decodeBase64url,\n encodeBase64url,\n} from \"@parlor/capabilities/encoding\";\nimport type { AnswerToSign, LoggedItem, PromptToSign } from \"@parlor/chat\";\nimport type { DeviceId } from \"@parlor/core\";\nimport type { PresenceKey } from \"@parlor/server\";\n\nimport { derToRaw, verifyEs256 } from \"./es256.js\";\n\nexport interface PromptStatement {\n readonly v: 1;\n readonly kind: \"prompt\";\n /** The app's origin: a statement for one app is worthless to another. */\n readonly app: string;\n readonly threadId: string;\n readonly turnId: string;\n /** `<hostId>/<agentId>`: a prompt cannot be replayed to another agent. */\n readonly agent: string;\n /** base64url SHA-256 of the prompt text */\n readonly digest: string;\n readonly at: number;\n}\n\nexport interface AnswerStatement {\n readonly v: 1;\n readonly kind: \"answer\";\n readonly app: string;\n readonly threadId: string;\n readonly askId: string;\n /** An option id, `\"declined\"`, or the digest of a form's values. */\n readonly choice: string;\n readonly at: number;\n}\n\n/** A linked device's word that the device behind `commitment` is the user's. */\nexport interface VouchStatement {\n readonly v: 1;\n readonly kind: \"vouch\";\n readonly app: string;\n readonly voucher: string;\n readonly device: string;\n /** `deviceCommitment` of the new device's keys, as the voucher checked it. */\n readonly commitment: string;\n readonly at: number;\n}\n\n/** A linked device's word, with presence, that `device` should no longer use the user's agents. */\nexport interface RemoveStatement {\n readonly v: 1;\n readonly kind: \"remove\";\n readonly app: string;\n readonly actor: string;\n readonly device: string;\n readonly at: number;\n}\n\nexport type Statement =\n | PromptStatement\n | AnswerStatement\n | VouchStatement\n | RemoveStatement;\n\nexport interface Signed<S extends Statement> {\n readonly statement: S;\n readonly deviceId: DeviceId;\n readonly signature: S extends AnswerStatement\n ? DeviceSignature | PresenceSignature\n : S extends VouchStatement | RemoveStatement\n ? PresenceSignature\n : DeviceSignature;\n}\n\n/** A device key the verifier pinned itself, never one that travelled with the statement. */\nexport interface PinnedDevice {\n readonly deviceId: DeviceId;\n readonly signingKey: JsonWebKey;\n readonly presenceKey?: PresenceKey | undefined;\n}\n\nexport type VerifyOutcome = \"valid\" | \"invalid\" | \"stale\" | \"presence_required\";\n\nconst FUTURE_SKEW_MS = 5 * 60_000;\nconst USER_PRESENT = 0x01;\nconst USER_VERIFIED = 0x04;\nconst AUTH_DATA_MIN_BYTES = 37;\n\nexport interface VerifyOptions {\n readonly requirePresence: boolean;\n readonly now: number;\n readonly maxAgeMs: number;\n}\n\nconst sha256 = async (bytes: Uint8Array): Promise<Uint8Array> =>\n new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new Uint8Array(bytes)));\n\nconst digestOf = async (bytes: Uint8Array) => encodeBase64url(await sha256(bytes));\n\nexport async function promptStatement(\n app: string,\n prompt: PromptToSign,\n at: number,\n): Promise<PromptStatement> {\n return {\n v: 1,\n kind: \"prompt\",\n app,\n threadId: prompt.threadId,\n turnId: prompt.turnId,\n agent: `${prompt.agent.hostId}/${prompt.agent.agentId}`,\n digest: await digestOf(new TextEncoder().encode(prompt.text)),\n at,\n };\n}\n\nexport async function answerStatement(\n app: string,\n answer: AnswerToSign,\n at: number,\n): Promise<AnswerStatement> {\n return {\n v: 1,\n kind: \"answer\",\n app,\n threadId: answer.threadId,\n askId: answer.askId,\n choice:\n answer.values === undefined\n ? answer.choice\n : await digestOf(\n canonicalBytes({ choice: answer.choice, values: answer.values }),\n ),\n at,\n };\n}\n\nexport async function signedPrompt(\n app: string,\n item: LoggedItem<\"prompt\">,\n): Promise<Signed<PromptStatement> | undefined> {\n const { signed, agent, text } = item.body;\n if (!signed) return undefined;\n return {\n statement: await promptStatement(\n app,\n { threadId: item.threadId, turnId: item.turnId, agent, text },\n signed.at,\n ),\n deviceId: signed.deviceId,\n signature: signed.signature,\n };\n}\n\nexport function statementBytes(statement: Statement): Uint8Array {\n return canonicalBytes(statement);\n}\n\nconst CROCKFORD = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\nexport const LINK_CODE_LENGTH = 10;\n\n/**\n * What a new device shows to be linked: a hash of its keys, so whoever links it checks the\n * keys the backend hands over are the ones this device holds.\n */\nexport async function deviceCommitment(\n app: string,\n deviceId: string,\n signingKey: JsonWebKey,\n presenceKey: JsonWebKey | undefined,\n): Promise<string> {\n const point = (key: JsonWebKey) => ({ crv: key.crv, kty: key.kty, x: key.x, y: key.y });\n return digestOf(\n canonicalBytes({\n v: 1,\n kind: \"device\",\n app: new URL(app).origin,\n deviceId,\n signing: point(signingKey),\n presence: presenceKey ? point(presenceKey) : null,\n }),\n );\n}\n\n/** The commitment's first 50 bits in Crockford base32, short enough to type. */\nexport function linkCode(commitment: string): string {\n const bytes = decodeBase64url(commitment);\n let bits = 0n;\n for (let index = 0; index < 7; index += 1) bits = (bits << 8n) | BigInt(bytes[index]!);\n bits >>= 6n;\n let code = \"\";\n for (let index = 0; index < LINK_CODE_LENGTH; index += 1) {\n code = CROCKFORD[Number(bits & 31n)]! + code;\n bits >>= 5n;\n }\n return code;\n}\n\n/** Upper case, no separators, the letters Crockford reads as digits mapped back. */\nexport function normalizeLinkCode(typed: string): string {\n return typed\n .toUpperCase()\n .replace(/[\\s-]/g, \"\")\n .replace(/O/g, \"0\")\n .replace(/[IL]/g, \"1\");\n}\n\nexport function formatLinkCode(code: string): string {\n return `${code.slice(0, 4)}-${code.slice(4, 8)}-${code.slice(8)}`;\n}\n\nexport function vouchStatement(\n app: string,\n voucher: string,\n device: string,\n commitment: string,\n at: number,\n): VouchStatement {\n return {\n v: 1,\n kind: \"vouch\",\n app: new URL(app).origin,\n voucher,\n device,\n commitment,\n at,\n };\n}\n\nexport function removeStatement(\n app: string,\n actor: string,\n device: string,\n at: number,\n): RemoveStatement {\n return { v: 1, kind: \"remove\", app: new URL(app).origin, actor, device, at };\n}\n\n/** The fragment Parlor Host's loopback redirect carries back to the browser that approved `connect`. */\nexport const HOST_PAIRING_FRAGMENT = \"parlor-host-pairing\";\n\nconst pairingKey = (secret: string) =>\n crypto.subtle.importKey(\n \"raw\",\n new Uint8Array(decodeBase64url(secret)),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\", \"verify\"],\n );\n\nconst pairingBytes = (deviceId: string, signingKey: JsonWebKey) =>\n canonicalBytes({\n v: 1,\n kind: \"pairing\",\n deviceId,\n signingKey: {\n crv: signingKey.crv,\n kty: signingKey.kty,\n x: signingKey.x,\n y: signingKey.y,\n },\n });\n\n/**\n * The browser that approved `connect` gets the host's pairing secret through the loopback\n * redirect, never through the app; this binds it to one device's keys.\n */\nexport async function pairingProof(\n secret: string,\n deviceId: string,\n signingKey: JsonWebKey,\n): Promise<string> {\n const mac = await crypto.subtle.sign(\n \"HMAC\",\n await pairingKey(secret),\n new Uint8Array(pairingBytes(deviceId, signingKey)),\n );\n return encodeBase64url(new Uint8Array(mac));\n}\n\nexport async function checkPairingProof(\n secret: string,\n proof: string,\n deviceId: string,\n signingKey: JsonWebKey,\n): Promise<boolean> {\n try {\n return await crypto.subtle.verify(\n \"HMAC\",\n await pairingKey(secret),\n new Uint8Array(decodeBase64url(proof)),\n new Uint8Array(pairingBytes(deviceId, signingKey)),\n );\n } catch {\n return false;\n }\n}\n\ninterface ClientData {\n readonly type?: unknown;\n readonly challenge?: unknown;\n readonly origin?: unknown;\n readonly crossOrigin?: unknown;\n}\n\nasync function assertionHolds(\n statement: Statement,\n signature: DeviceSignature,\n presenceKey: PinnedDevice[\"presenceKey\"],\n): Promise<boolean> {\n if (presenceKey?.format !== \"webauthn\" || signature.presence !== \"user\") return false;\n if (\n signature.authenticatorData === undefined ||\n signature.clientDataJSON === undefined\n ) {\n return false;\n }\n if (signature.keyId !== presenceKey.credentialId) return false;\n if (new URL(statement.app).hostname !== presenceKey.rpId) return false;\n const clientDataBytes = decodeBase64url(signature.clientDataJSON);\n const clientData = JSON.parse(new TextDecoder().decode(clientDataBytes)) as ClientData;\n const challenge = await digestOf(statementBytes(statement));\n if (\n clientData.type !== \"webauthn.get\" ||\n clientData.challenge !== challenge ||\n clientData.origin !== statement.app ||\n clientData.crossOrigin === true\n ) {\n return false;\n }\n const authData = decodeBase64url(signature.authenticatorData);\n const flags = authData[32];\n if (authData.length < AUTH_DATA_MIN_BYTES || flags === undefined) return false;\n if ((flags & USER_PRESENT) === 0 || (flags & USER_VERIFIED) === 0) return false;\n const rpIdHash = await sha256(new TextEncoder().encode(presenceKey.rpId));\n if (encodeBase64url(authData.slice(0, 32)) !== encodeBase64url(rpIdHash)) return false;\n const raw = derToRaw(decodeBase64url(signature.signature));\n if (!raw) return false;\n const signed = new Uint8Array([...authData, ...(await sha256(clientDataBytes))]);\n return verifyEs256(presenceKey.publicKey, raw, signed);\n}\n\nasync function signatureHolds(\n statement: Statement,\n signature: DeviceSignature,\n pinned: PinnedDevice,\n): Promise<boolean> {\n if (signature.format === \"webauthn\") {\n return assertionHolds(statement, signature, pinned.presenceKey);\n }\n const bytes = statementBytes(statement);\n const raw = decodeBase64url(signature.signature);\n if (signature.presence === \"none\") return verifyEs256(pinned.signingKey, raw, bytes);\n const { presenceKey } = pinned;\n return presenceKey?.format === \"es256-raw\"\n ? verifyEs256(presenceKey.publicKey, raw, bytes)\n : false;\n}\n\nexport async function verifyStatement(\n signed: Signed<Statement>,\n pinned: PinnedDevice,\n options: VerifyOptions,\n): Promise<VerifyOutcome> {\n const { statement, signature } = signed;\n if (signed.deviceId !== pinned.deviceId) return \"invalid\";\n if (options.now - statement.at > options.maxAgeMs) return \"stale\";\n if (statement.at - options.now > FUTURE_SKEW_MS) return \"invalid\";\n if (options.requirePresence && signature.presence !== \"user\") {\n return \"presence_required\";\n }\n try {\n return (await signatureHolds(statement, signature, pinned)) ? \"valid\" : \"invalid\";\n } catch {\n return \"invalid\";\n }\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import { type AgentPrincipal, type HostPrincipal, type TurnBinding } from "@parlor/core";
2
+ import { type Delegation, type TurnDelegation } from "@parlor/server";
3
+ /** A host assertion's audience: signed by the computer's own key, per request. */
4
+ export declare const HOST_AUDIENCE = "parlor:host";
5
+ /** A turn token's audience: signed by the server, carried by the host's MCP proxy. */
6
+ export declare const TURN_AUDIENCE = "parlor:mcp";
7
+ export declare const HOST_ASSERTION_TYP = "parlor-host+jwt";
8
+ export declare const TURN_TOKEN_TYP = "parlor-turn+jwt";
9
+ /**
10
+ * A turn token never outlives the revocation horizon, or a revoked grant's token could
11
+ * verify after `RevocationSet` forgot the revocation.
12
+ */
13
+ export declare const MAX_TURN_TOKEN_TTL_MS: number;
14
+ export type TokenAudience = typeof HOST_AUDIENCE | typeof TURN_AUDIENCE;
15
+ declare const audience: unique symbol;
16
+ /** A compact JWS whose audience is known from the scheme it arrived under; never swapped. */
17
+ export type Jws<A extends TokenAudience> = string & {
18
+ readonly [audience]: A;
19
+ };
20
+ export type HostAssertion = Jws<typeof HOST_AUDIENCE>;
21
+ export type TurnToken = Jws<typeof TURN_AUDIENCE>;
22
+ /** What an `authorization` header carries, sorted by scheme and shape before any verification. */
23
+ export type Credential = {
24
+ readonly kind: "host";
25
+ readonly assertion: HostAssertion;
26
+ } | {
27
+ readonly kind: "turn";
28
+ readonly token: TurnToken;
29
+ } | {
30
+ readonly kind: "access";
31
+ readonly token: string;
32
+ } | {
33
+ readonly kind: "session";
34
+ readonly token: string;
35
+ };
36
+ export interface TurnTokenClaims {
37
+ readonly aud: typeof TURN_AUDIENCE;
38
+ readonly iss: string;
39
+ /** user id */
40
+ readonly sub: string;
41
+ /** grant id: revoking the grant kills the token */
42
+ readonly gid: string;
43
+ readonly hid: string;
44
+ readonly thr: string;
45
+ readonly trn: string;
46
+ /** the client the prompt came from; `ui_*` binds to it */
47
+ readonly cid: string;
48
+ readonly agt: string;
49
+ readonly iat: number;
50
+ /** as long as the turn's lease */
51
+ readonly exp: number;
52
+ }
53
+ export interface TurnTokenKeys {
54
+ readonly issuer: string;
55
+ readonly kid: string;
56
+ readonly privateKey: CryptoKey;
57
+ readonly publicKeys: ReadonlyMap<string, CryptoKey>;
58
+ }
59
+ /** `ParlorHost <jws>` is a host; a `Bearer` JWS whose header says `typ: parlor-turn+jwt` is a turn. */
60
+ export declare function credentialOf(authorization: string | null): Credential | undefined;
61
+ /** Mints a turn token for `host`'s grant; the provider's `delegate`. */
62
+ export declare function delegate(keys: TurnTokenKeys, host: HostPrincipal, turn: TurnDelegation, now: number): Promise<Delegation>;
63
+ /** Signature, `aud`, `iss` and `exp` only; the caller then asks `RevocationSet` about `gid`. */
64
+ export declare function verifyTurnToken(keys: TurnTokenKeys, token: TurnToken, now: number): Promise<TurnTokenClaims>;
65
+ export declare function agentPrincipalOf(claims: TurnTokenClaims): AgentPrincipal & {
66
+ readonly turn: TurnBinding;
67
+ };
68
+ export {};
69
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
package/dist/tokens.js ADDED
@@ -0,0 +1,134 @@
1
+ import { asId, } from "@parlor/core";
2
+ import { AuthFailure, REVOCATION_HORIZON_MS, } from "@parlor/server";
3
+ import { decodeProtectedHeader, errors, jwtVerify, SignJWT } from "jose";
4
+ import { z } from "zod";
5
+ /** A host assertion's audience: signed by the computer's own key, per request. */
6
+ export const HOST_AUDIENCE = "parlor:host";
7
+ /** A turn token's audience: signed by the server, carried by the host's MCP proxy. */
8
+ export const TURN_AUDIENCE = "parlor:mcp";
9
+ export const HOST_ASSERTION_TYP = "parlor-host+jwt";
10
+ export const TURN_TOKEN_TYP = "parlor-turn+jwt";
11
+ const CLOCK_SKEW_MS = 60_000;
12
+ /**
13
+ * A turn token never outlives the revocation horizon, or a revoked grant's token could
14
+ * verify after `RevocationSet` forgot the revocation.
15
+ */
16
+ export const MAX_TURN_TOKEN_TTL_MS = REVOCATION_HORIZON_MS - CLOCK_SKEW_MS;
17
+ const JWS_SHAPE = /^[\w-]+\.[\w-]+\.[\w-]+$/;
18
+ function brand(compact) {
19
+ return compact;
20
+ }
21
+ function typOf(jws) {
22
+ try {
23
+ return decodeProtectedHeader(jws).typ;
24
+ }
25
+ catch {
26
+ throw new AuthFailure("invalid", "The token is malformed.");
27
+ }
28
+ }
29
+ /** `ParlorHost <jws>` is a host; a `Bearer` JWS whose header says `typ: parlor-turn+jwt` is a turn. */
30
+ export function credentialOf(authorization) {
31
+ if (authorization === null)
32
+ return undefined;
33
+ const match = /^([A-Za-z]+)\s+(\S+)$/.exec(authorization);
34
+ const scheme = match?.[1]?.toLowerCase();
35
+ const value = match?.[2];
36
+ if (value === undefined || (scheme !== "bearer" && scheme !== "parlorhost")) {
37
+ throw new AuthFailure("invalid", "Use a Bearer token.");
38
+ }
39
+ if (scheme === "parlorhost") {
40
+ if (!JWS_SHAPE.test(value)) {
41
+ throw new AuthFailure("invalid", "The host assertion is malformed.");
42
+ }
43
+ return { kind: "host", assertion: brand(value) };
44
+ }
45
+ if (!JWS_SHAPE.test(value))
46
+ return { kind: "session", token: value };
47
+ return typOf(value) === TURN_TOKEN_TYP
48
+ ? { kind: "turn", token: brand(value) }
49
+ : { kind: "access", token: value };
50
+ }
51
+ /** Mints a turn token for `host`'s grant; the provider's `delegate`. */
52
+ export async function delegate(keys, host, turn, now) {
53
+ if (!(turn.ttlMs > 0))
54
+ throw new RangeError("A turn token needs a positive lifetime.");
55
+ const iat = Math.floor(now / 1000);
56
+ const exp = iat + Math.ceil(Math.min(turn.ttlMs, MAX_TURN_TOKEN_TTL_MS) / 1000);
57
+ const claims = {
58
+ aud: TURN_AUDIENCE,
59
+ iss: keys.issuer,
60
+ sub: host.userId,
61
+ gid: host.grantId,
62
+ hid: host.hostId,
63
+ thr: turn.threadId,
64
+ trn: turn.turnId,
65
+ cid: turn.clientId,
66
+ agt: turn.agent,
67
+ iat,
68
+ exp,
69
+ };
70
+ const token = await new SignJWT({ ...claims })
71
+ .setProtectedHeader({ alg: "ES256", typ: TURN_TOKEN_TYP, kid: keys.kid })
72
+ .sign(keys.privateKey);
73
+ return { token, expiresAt: exp * 1000 };
74
+ }
75
+ const claimsSchema = z.object({
76
+ aud: z.literal(TURN_AUDIENCE),
77
+ iss: z.string().min(1),
78
+ sub: z.string().min(1),
79
+ gid: z.string().min(1),
80
+ hid: z.string().min(1),
81
+ thr: z.string().min(1),
82
+ trn: z.string().min(1),
83
+ cid: z.string(),
84
+ agt: z.string().min(1),
85
+ iat: z.number().int(),
86
+ exp: z.number().int(),
87
+ });
88
+ /** Signature, `aud`, `iss` and `exp` only; the caller then asks `RevocationSet` about `gid`. */
89
+ export async function verifyTurnToken(keys, token, now) {
90
+ let payload;
91
+ try {
92
+ ({ payload } = await jwtVerify(token, (header) => {
93
+ const key = header.kid === undefined ? undefined : keys.publicKeys.get(header.kid);
94
+ if (!key)
95
+ throw new errors.JWKSNoMatchingKey();
96
+ return key;
97
+ }, {
98
+ algorithms: ["ES256"],
99
+ typ: TURN_TOKEN_TYP,
100
+ issuer: keys.issuer,
101
+ audience: TURN_AUDIENCE,
102
+ currentDate: new Date(now),
103
+ }));
104
+ }
105
+ catch (error) {
106
+ if (error instanceof errors.JWTExpired) {
107
+ throw new AuthFailure("expired", "The turn token expired.");
108
+ }
109
+ throw new AuthFailure("invalid", "The turn token is invalid.");
110
+ }
111
+ const claims = claimsSchema.safeParse(payload);
112
+ if (!claims.success)
113
+ throw new AuthFailure("invalid", "The turn token is malformed.");
114
+ const { iat, exp } = claims.data;
115
+ if (exp - iat > MAX_TURN_TOKEN_TTL_MS / 1000 || iat * 1000 > now + CLOCK_SKEW_MS) {
116
+ throw new AuthFailure("invalid", "The turn token has an impossible lifetime.");
117
+ }
118
+ return claims.data;
119
+ }
120
+ export function agentPrincipalOf(claims) {
121
+ return {
122
+ kind: "agent",
123
+ userId: asId("user", claims.sub),
124
+ grantId: asId("grant", claims.gid),
125
+ agent: { name: claims.agt, verified: true },
126
+ turn: {
127
+ threadId: asId("thread", claims.thr),
128
+ turnId: asId("turn", claims.trn),
129
+ hostId: asId("host", claims.hid),
130
+ clientId: claims.cid,
131
+ },
132
+ };
133
+ }
134
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type AgentPrincipal,\n asId,\n type HostPrincipal,\n type TurnBinding,\n} from \"@parlor/core\";\nimport {\n AuthFailure,\n type Delegation,\n REVOCATION_HORIZON_MS,\n type TurnDelegation,\n} from \"@parlor/server\";\nimport { decodeProtectedHeader, errors, jwtVerify, SignJWT } from \"jose\";\nimport { z } from \"zod\";\n\n/** A host assertion's audience: signed by the computer's own key, per request. */\nexport const HOST_AUDIENCE = \"parlor:host\";\n/** A turn token's audience: signed by the server, carried by the host's MCP proxy. */\nexport const TURN_AUDIENCE = \"parlor:mcp\";\n\nexport const HOST_ASSERTION_TYP = \"parlor-host+jwt\";\nexport const TURN_TOKEN_TYP = \"parlor-turn+jwt\";\n\nconst CLOCK_SKEW_MS = 60_000;\n\n/**\n * A turn token never outlives the revocation horizon, or a revoked grant's token could\n * verify after `RevocationSet` forgot the revocation.\n */\nexport const MAX_TURN_TOKEN_TTL_MS = REVOCATION_HORIZON_MS - CLOCK_SKEW_MS;\n\nexport type TokenAudience = typeof HOST_AUDIENCE | typeof TURN_AUDIENCE;\n\ndeclare const audience: unique symbol;\n\n/** A compact JWS whose audience is known from the scheme it arrived under; never swapped. */\nexport type Jws<A extends TokenAudience> = string & { readonly [audience]: A };\nexport type HostAssertion = Jws<typeof HOST_AUDIENCE>;\nexport type TurnToken = Jws<typeof TURN_AUDIENCE>;\n\n/** What an `authorization` header carries, sorted by scheme and shape before any verification. */\nexport type Credential =\n | { readonly kind: \"host\"; readonly assertion: HostAssertion }\n | { readonly kind: \"turn\"; readonly token: TurnToken }\n | { readonly kind: \"access\"; readonly token: string }\n | { readonly kind: \"session\"; readonly token: string };\n\nexport interface TurnTokenClaims {\n readonly aud: typeof TURN_AUDIENCE;\n readonly iss: string;\n /** user id */\n readonly sub: string;\n /** grant id: revoking the grant kills the token */\n readonly gid: string;\n readonly hid: string;\n readonly thr: string;\n readonly trn: string;\n /** the client the prompt came from; `ui_*` binds to it */\n readonly cid: string;\n readonly agt: string;\n readonly iat: number;\n /** as long as the turn's lease */\n readonly exp: number;\n}\n\nexport interface TurnTokenKeys {\n readonly issuer: string;\n readonly kid: string;\n readonly privateKey: CryptoKey;\n readonly publicKeys: ReadonlyMap<string, CryptoKey>;\n}\n\nconst JWS_SHAPE = /^[\\w-]+\\.[\\w-]+\\.[\\w-]+$/;\n\nfunction brand<A extends TokenAudience>(compact: string): Jws<A> {\n return compact as Jws<A>;\n}\n\nfunction typOf(jws: string): string | undefined {\n try {\n return decodeProtectedHeader(jws).typ;\n } catch {\n throw new AuthFailure(\"invalid\", \"The token is malformed.\");\n }\n}\n\n/** `ParlorHost <jws>` is a host; a `Bearer` JWS whose header says `typ: parlor-turn+jwt` is a turn. */\nexport function credentialOf(authorization: string | null): Credential | undefined {\n if (authorization === null) return undefined;\n const match = /^([A-Za-z]+)\\s+(\\S+)$/.exec(authorization);\n const scheme = match?.[1]?.toLowerCase();\n const value = match?.[2];\n if (value === undefined || (scheme !== \"bearer\" && scheme !== \"parlorhost\")) {\n throw new AuthFailure(\"invalid\", \"Use a Bearer token.\");\n }\n if (scheme === \"parlorhost\") {\n if (!JWS_SHAPE.test(value)) {\n throw new AuthFailure(\"invalid\", \"The host assertion is malformed.\");\n }\n return { kind: \"host\", assertion: brand<typeof HOST_AUDIENCE>(value) };\n }\n if (!JWS_SHAPE.test(value)) return { kind: \"session\", token: value };\n return typOf(value) === TURN_TOKEN_TYP\n ? { kind: \"turn\", token: brand<typeof TURN_AUDIENCE>(value) }\n : { kind: \"access\", token: value };\n}\n\n/** Mints a turn token for `host`'s grant; the provider's `delegate`. */\nexport async function delegate(\n keys: TurnTokenKeys,\n host: HostPrincipal,\n turn: TurnDelegation,\n now: number,\n): Promise<Delegation> {\n if (!(turn.ttlMs > 0)) throw new RangeError(\"A turn token needs a positive lifetime.\");\n const iat = Math.floor(now / 1000);\n const exp = iat + Math.ceil(Math.min(turn.ttlMs, MAX_TURN_TOKEN_TTL_MS) / 1000);\n const claims: TurnTokenClaims = {\n aud: TURN_AUDIENCE,\n iss: keys.issuer,\n sub: host.userId,\n gid: host.grantId,\n hid: host.hostId,\n thr: turn.threadId,\n trn: turn.turnId,\n cid: turn.clientId,\n agt: turn.agent,\n iat,\n exp,\n };\n const token = await new SignJWT({ ...claims })\n .setProtectedHeader({ alg: \"ES256\", typ: TURN_TOKEN_TYP, kid: keys.kid })\n .sign(keys.privateKey);\n return { token, expiresAt: exp * 1000 };\n}\n\nconst claimsSchema = z.object({\n aud: z.literal(TURN_AUDIENCE),\n iss: z.string().min(1),\n sub: z.string().min(1),\n gid: z.string().min(1),\n hid: z.string().min(1),\n thr: z.string().min(1),\n trn: z.string().min(1),\n cid: z.string(),\n agt: z.string().min(1),\n iat: z.number().int(),\n exp: z.number().int(),\n});\n\n/** Signature, `aud`, `iss` and `exp` only; the caller then asks `RevocationSet` about `gid`. */\nexport async function verifyTurnToken(\n keys: TurnTokenKeys,\n token: TurnToken,\n now: number,\n): Promise<TurnTokenClaims> {\n let payload: unknown;\n try {\n ({ payload } = await jwtVerify(\n token,\n (header) => {\n const key =\n header.kid === undefined ? undefined : keys.publicKeys.get(header.kid);\n if (!key) throw new errors.JWKSNoMatchingKey();\n return key;\n },\n {\n algorithms: [\"ES256\"],\n typ: TURN_TOKEN_TYP,\n issuer: keys.issuer,\n audience: TURN_AUDIENCE,\n currentDate: new Date(now),\n },\n ));\n } catch (error) {\n if (error instanceof errors.JWTExpired) {\n throw new AuthFailure(\"expired\", \"The turn token expired.\");\n }\n throw new AuthFailure(\"invalid\", \"The turn token is invalid.\");\n }\n const claims = claimsSchema.safeParse(payload);\n if (!claims.success) throw new AuthFailure(\"invalid\", \"The turn token is malformed.\");\n const { iat, exp } = claims.data;\n if (exp - iat > MAX_TURN_TOKEN_TTL_MS / 1000 || iat * 1000 > now + CLOCK_SKEW_MS) {\n throw new AuthFailure(\"invalid\", \"The turn token has an impossible lifetime.\");\n }\n return claims.data;\n}\n\nexport function agentPrincipalOf(\n claims: TurnTokenClaims,\n): AgentPrincipal & { readonly turn: TurnBinding } {\n return {\n kind: \"agent\",\n userId: asId(\"user\", claims.sub),\n grantId: asId(\"grant\", claims.gid),\n agent: { name: claims.agt, verified: true },\n turn: {\n threadId: asId(\"thread\", claims.thr),\n turnId: asId(\"turn\", claims.trn),\n hostId: asId(\"host\", claims.hid),\n clientId: claims.cid,\n },\n };\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { DataProvider } from "@parlor/core";
2
+ import type { Migration } from "@parlor/server";
3
+ import type { TurnTokenKeys } from "./tokens.js";
4
+ export declare const TURN_KEYS_MIGRATION: Migration;
5
+ export interface TurnKeyStore {
6
+ keys(): Promise<TurnTokenKeys>;
7
+ refresh(): Promise<TurnTokenKeys>;
8
+ }
9
+ export interface TurnKeyStoreOptions {
10
+ readonly database: DataProvider;
11
+ readonly issuer: string;
12
+ readonly secret: () => Promise<string>;
13
+ readonly now?: (() => number) | undefined;
14
+ }
15
+ export declare function createTurnKeyStore(options: TurnKeyStoreOptions): TurnKeyStore;
16
+ //# sourceMappingURL=turn-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-keys.d.ts","sourceRoot":"","sources":["../src/turn-keys.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAKhD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,eAAO,MAAM,mBAAmB,EAAE,SAWjC,CAAC;AAiCF,MAAM,WAAW,YAAY;IAC3B,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/B,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CAC3C;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CAiG7E"}
@@ -0,0 +1,108 @@
1
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from "node:crypto";
2
+ import { sql } from "drizzle-orm";
3
+ import { exportJWK, generateKeyPair } from "jose";
4
+ import { jwkOf, rowsOf } from "./database.js";
5
+ export const TURN_KEYS_MIGRATION = {
6
+ id: "auth/0005_turn_keys",
7
+ sql: `
8
+ create table parlor_auth_turn_keys (
9
+ kid text primary key,
10
+ public_key jsonb not null,
11
+ private_key text not null,
12
+ active boolean not null default true,
13
+ created_at timestamptz not null default now()
14
+ );
15
+ create unique index parlor_auth_turn_keys_active on parlor_auth_turn_keys (active) where active`,
16
+ };
17
+ const KEY_ALGORITHM = { name: "ECDSA", namedCurve: "P-256" };
18
+ const REFRESH_MIN_INTERVAL_MS = 1000;
19
+ function sealingKey(secret) {
20
+ return Buffer.from(hkdfSync("sha256", secret, "", "parlor-auth/turn-keys", 32));
21
+ }
22
+ function seal(secret, plaintext) {
23
+ const iv = randomBytes(12);
24
+ const cipher = createCipheriv("aes-256-gcm", sealingKey(secret), iv);
25
+ const body = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
26
+ return [iv, cipher.getAuthTag(), body]
27
+ .map((part) => part.toString("base64url"))
28
+ .join(".");
29
+ }
30
+ function unseal(secret, sealed) {
31
+ const [iv, tag, body] = sealed.split(".").map((part) => Buffer.from(part, "base64url"));
32
+ if (!iv || !tag || !body)
33
+ throw new Error("The sealed turn key is malformed.");
34
+ const decipher = createDecipheriv("aes-256-gcm", sealingKey(secret), iv);
35
+ decipher.setAuthTag(tag);
36
+ return Buffer.concat([decipher.update(body), decipher.final()]).toString("utf8");
37
+ }
38
+ export function createTurnKeyStore(options) {
39
+ const { database, issuer } = options;
40
+ const now = options.now ?? Date.now;
41
+ let loaded;
42
+ let refreshedAt = 0;
43
+ const read = async () => rowsOf(await database.read().execute(sql `select kid, public_key, private_key, active from parlor_auth_turn_keys`));
44
+ const replace = async (secret, retiring) => {
45
+ const pair = await generateKeyPair("ES256", { extractable: true });
46
+ const publicKey = await exportJWK(pair.publicKey);
47
+ const privateKey = await exportJWK(pair.privateKey);
48
+ const kid = crypto.randomUUID();
49
+ await database.transaction(async (tx) => {
50
+ const db = tx;
51
+ if (retiring !== undefined) {
52
+ await db.execute(sql `update parlor_auth_turn_keys set active = false where kid = ${retiring} and active`);
53
+ }
54
+ await db.execute(sql `
55
+ insert into parlor_auth_turn_keys (kid, public_key, private_key)
56
+ values (${kid}, ${JSON.stringify(publicKey)}::jsonb, ${seal(secret, JSON.stringify(privateKey))})
57
+ on conflict do nothing`);
58
+ });
59
+ };
60
+ const signingKeyOf = async (secret, row) => {
61
+ try {
62
+ return await crypto.subtle.importKey("jwk", JSON.parse(unseal(secret, row.private_key)), KEY_ALGORITHM, false, ["sign"]);
63
+ }
64
+ catch {
65
+ return undefined;
66
+ }
67
+ };
68
+ const build = async (mayReplace) => {
69
+ const secret = await options.secret();
70
+ let rows = await read();
71
+ let active = rows.find((row) => row.active);
72
+ let privateKey = active && (await signingKeyOf(secret, active));
73
+ if (!privateKey && !mayReplace)
74
+ throw new Error("The turn signing key cannot be opened.");
75
+ if (!privateKey) {
76
+ await replace(secret, active?.kid);
77
+ rows = await read();
78
+ active = rows.find((row) => row.active);
79
+ privateKey = active && (await signingKeyOf(secret, active));
80
+ }
81
+ if (!active || !privateKey)
82
+ throw new Error("The turn signing key could not be created.");
83
+ const verifyKeys = await Promise.all(rows.map(async (row) => {
84
+ const key = await crypto.subtle.importKey("jwk", jwkOf(row.public_key), KEY_ALGORITHM, false, ["verify"]);
85
+ return [row.kid, key];
86
+ }));
87
+ return { issuer, kid: active.kid, privateKey, publicKeys: new Map(verifyKeys) };
88
+ };
89
+ const load = () => {
90
+ loaded ??= build(true).catch((error) => {
91
+ loaded = undefined;
92
+ throw error;
93
+ });
94
+ return loaded;
95
+ };
96
+ return {
97
+ keys: load,
98
+ async refresh() {
99
+ const current = await load();
100
+ if (now() - refreshedAt < REFRESH_MIN_INTERVAL_MS)
101
+ return current;
102
+ refreshedAt = now();
103
+ loaded = build(false).catch(() => current);
104
+ return loaded;
105
+ },
106
+ };
107
+ }
108
+ //# sourceMappingURL=turn-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-keys.js","sourceRoot":"","sources":["../src/turn-keys.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from \"node:crypto\";\n\nimport type { DataProvider } from \"@parlor/core\";\nimport type { Migration } from \"@parlor/server\";\nimport { sql } from \"drizzle-orm\";\nimport { exportJWK, generateKeyPair } from \"jose\";\n\nimport { type Database, jwkOf, rowsOf } from \"./database.js\";\nimport type { TurnTokenKeys } from \"./tokens.js\";\n\nexport const TURN_KEYS_MIGRATION: Migration = {\n id: \"auth/0005_turn_keys\",\n sql: `\n create table parlor_auth_turn_keys (\n kid text primary key,\n public_key jsonb not null,\n private_key text not null,\n active boolean not null default true,\n created_at timestamptz not null default now()\n );\n create unique index parlor_auth_turn_keys_active on parlor_auth_turn_keys (active) where active`,\n};\n\nconst KEY_ALGORITHM = { name: \"ECDSA\", namedCurve: \"P-256\" } as const;\nconst REFRESH_MIN_INTERVAL_MS = 1000;\n\ninterface KeyRow {\n readonly kid: string;\n readonly public_key: JsonWebKey | string;\n readonly private_key: string;\n readonly active: boolean;\n}\n\nfunction sealingKey(secret: string): Buffer {\n return Buffer.from(hkdfSync(\"sha256\", secret, \"\", \"parlor-auth/turn-keys\", 32));\n}\n\nfunction seal(secret: string, plaintext: string): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", sealingKey(secret), iv);\n const body = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n return [iv, cipher.getAuthTag(), body]\n .map((part) => part.toString(\"base64url\"))\n .join(\".\");\n}\n\nfunction unseal(secret: string, sealed: string): string {\n const [iv, tag, body] = sealed.split(\".\").map((part) => Buffer.from(part, \"base64url\"));\n if (!iv || !tag || !body) throw new Error(\"The sealed turn key is malformed.\");\n const decipher = createDecipheriv(\"aes-256-gcm\", sealingKey(secret), iv);\n decipher.setAuthTag(tag);\n return Buffer.concat([decipher.update(body), decipher.final()]).toString(\"utf8\");\n}\n\nexport interface TurnKeyStore {\n keys(): Promise<TurnTokenKeys>;\n refresh(): Promise<TurnTokenKeys>;\n}\n\nexport interface TurnKeyStoreOptions {\n readonly database: DataProvider;\n readonly issuer: string;\n readonly secret: () => Promise<string>;\n readonly now?: (() => number) | undefined;\n}\n\nexport function createTurnKeyStore(options: TurnKeyStoreOptions): TurnKeyStore {\n const { database, issuer } = options;\n const now = options.now ?? Date.now;\n let loaded: Promise<TurnTokenKeys> | undefined;\n let refreshedAt = 0;\n\n const read = async (): Promise<KeyRow[]> =>\n rowsOf<KeyRow>(\n await (database.read() as Database).execute(\n sql`select kid, public_key, private_key, active from parlor_auth_turn_keys`,\n ),\n );\n\n const replace = async (secret: string, retiring: string | undefined): Promise<void> => {\n const pair = await generateKeyPair(\"ES256\", { extractable: true });\n const publicKey = await exportJWK(pair.publicKey);\n const privateKey = await exportJWK(pair.privateKey);\n const kid = crypto.randomUUID();\n await database.transaction(async (tx) => {\n const db = tx as Database;\n if (retiring !== undefined) {\n await db.execute(\n sql`update parlor_auth_turn_keys set active = false where kid = ${retiring} and active`,\n );\n }\n await db.execute(sql`\n insert into parlor_auth_turn_keys (kid, public_key, private_key)\n values (${kid}, ${JSON.stringify(publicKey)}::jsonb, ${seal(secret, JSON.stringify(privateKey))})\n on conflict do nothing`);\n });\n };\n\n const signingKeyOf = async (\n secret: string,\n row: KeyRow,\n ): Promise<CryptoKey | undefined> => {\n try {\n return await crypto.subtle.importKey(\n \"jwk\",\n JSON.parse(unseal(secret, row.private_key)) as JsonWebKey,\n KEY_ALGORITHM,\n false,\n [\"sign\"],\n );\n } catch {\n return undefined;\n }\n };\n\n const build = async (mayReplace: boolean): Promise<TurnTokenKeys> => {\n const secret = await options.secret();\n let rows = await read();\n let active = rows.find((row) => row.active);\n let privateKey = active && (await signingKeyOf(secret, active));\n if (!privateKey && !mayReplace)\n throw new Error(\"The turn signing key cannot be opened.\");\n if (!privateKey) {\n await replace(secret, active?.kid);\n rows = await read();\n active = rows.find((row) => row.active);\n privateKey = active && (await signingKeyOf(secret, active));\n }\n if (!active || !privateKey)\n throw new Error(\"The turn signing key could not be created.\");\n const verifyKeys = await Promise.all(\n rows.map(async (row) => {\n const key = await crypto.subtle.importKey(\n \"jwk\",\n jwkOf(row.public_key),\n KEY_ALGORITHM,\n false,\n [\"verify\"],\n );\n return [row.kid, key] as const;\n }),\n );\n return { issuer, kid: active.kid, privateKey, publicKeys: new Map(verifyKeys) };\n };\n\n const load = (): Promise<TurnTokenKeys> => {\n loaded ??= build(true).catch((error: unknown) => {\n loaded = undefined;\n throw error;\n });\n return loaded;\n };\n\n return {\n keys: load,\n async refresh() {\n const current = await load();\n if (now() - refreshedAt < REFRESH_MIN_INTERVAL_MS) return current;\n refreshedAt = now();\n loaded = build(false).catch(() => current);\n return loaded;\n },\n };\n}\n"]}