@oaath/server 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1135 @@
1
+ import { _ as jsonResponse, a as OAATH_ENCRYPTED_ARTIFACT_RECORD_VERSION, b as relayFailure, c as canonicalIdentifier, d as parseAuthorizationRequestRecord, f as parseCapabilityInvalidationRecord, g as RELAY_ERROR_STATUS, h as OaathRelayError, i as OAATH_CAPABILITY_INVALIDATION_RECORD_VERSION, l as parseAuthorizationCodeRecord, m as timestamp, n as OAATH_AUTHORIZATION_DECISION_RECORD_VERSION, o as RELAY_LIMITS, p as parseEncryptedArtifactRecord, r as OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION, s as boundedText, t as OAATH_AUTHORIZATION_CODE_RECORD_VERSION, u as parseAuthorizationDecisionRecord, v as relayErrorCode, y as relayErrorResponse } from "./records-Bgr-9PUh.js";
2
+ import { a as relayNow, i as verifyPkceS256, r as sha256Base64Url, t as isCodeChallengeS256 } from "./challenge-AGTmUfj2.js";
3
+ import { a as submitAuthorizationDecision, c as readAuthorizationState, d as withRelayTransaction, i as submitOwnerPhoneDecision, l as openArtifact, o as createAuthorizationRequest, r as projectOwnerPhoneRequest, s as fetchAuthorizationRequest, u as sealArtifact } from "./projection-dGIkE0rI.js";
4
+ import { OAATH_OPERATOR_CREDENTIAL_PROFILE_VERSION, OAATH_SERVICE_BOOTSTRAP_VERSION, captureDenseArray, captureRecord, exactCapturedRecord, exactRecord, parseServiceBootstrap } from "@oaath/protocol";
5
+ import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
6
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
7
+ import { keccak_256 } from "@noble/hashes/sha3.js";
8
+ //#region src/artifact/claim.ts
9
+ /**
10
+ * One-time encrypted artifact release.
11
+ *
12
+ * ```text
13
+ * state and owner the artifact record's `claimedAt` owns "released"
14
+ * transitions sealed -> claimed, exactly once
15
+ * terminal claimed; a second claim fails closed
16
+ * retry positively safe? no. The claim commits before the KMS is asked to
17
+ * decrypt, so a decrypt or transport failure burns the
18
+ * artifact instead of risking a second release
19
+ * crash/reload a crash before commit leaves the artifact claimable
20
+ * ```
21
+ *
22
+ * @author taek <leekt216@gmail.com>
23
+ */
24
+ async function claimEncryptedArtifact(input) {
25
+ const claimedAt = relayNow(input.clock);
26
+ const claimed = await withRelayTransaction(input.store, async (transaction) => {
27
+ const record = await transaction.lockEncryptedArtifact(input.artifactId);
28
+ if (!record || record.clientId !== input.caller.clientId) return relayFailure("relay_not_found", "encrypted artifact does not exist");
29
+ if (record.claimedAt !== null) relayFailure("relay_artifact_already_claimed", "encrypted artifact was already claimed");
30
+ if (!await transaction.claimEncryptedArtifact(input.artifactId, claimedAt)) relayFailure("relay_state_ambiguous", "one-time claim did not apply under its row lock");
31
+ return record;
32
+ });
33
+ return Object.freeze({
34
+ requestId: claimed.requestId,
35
+ artifact: await openArtifact(input.kms, claimed.ciphertextRef)
36
+ });
37
+ }
38
+ //#endregion
39
+ //#region src/authorization/code.ts
40
+ /**
41
+ * One-time authorization code consume.
42
+ *
43
+ * ```text
44
+ * state and owner the code record's `consumedAt` owns "used"
45
+ * transitions issued -> consumed, exactly once
46
+ * terminal consumed; a second consume fails closed
47
+ * retry positively safe? no, in either direction: a released code is never
48
+ * re-released, and an ambiguous commit is never retried
49
+ * crash/reload the guarded update and the release decide together in one
50
+ * transaction under the code's row lock
51
+ * ```
52
+ *
53
+ * A failed PKCE or redirect binding still burns the code, so a stolen code
54
+ * cannot be brute-forced against the stored challenge.
55
+ *
56
+ * The endpoint is not an oracle for whether a guessed code was correct: an
57
+ * unknown code, a code bound to another client, a wrong redirect URI, and a
58
+ * wrong verifier all leave as the single `relay_code_invalid` code and status.
59
+ *
60
+ * @author taek <leekt216@gmail.com>
61
+ */
62
+ async function consumeAuthorizationCode(input) {
63
+ const consumedAt = relayNow(input.clock);
64
+ const codeHash = await sha256Base64Url(input.code);
65
+ const transaction = await input.store.begin();
66
+ let outcome;
67
+ try {
68
+ const record = await transaction.lockAuthorizationCode(codeHash);
69
+ if (!record || record.clientId !== input.caller.clientId) relayFailure("relay_code_invalid", "authorization code redemption failed");
70
+ if (record.consumedAt !== null) relayFailure("relay_code_already_consumed", "authorization code was already consumed");
71
+ if (consumedAt >= record.expiresAt) relayFailure("relay_expired", "authorization code expired");
72
+ const bound = record.redirectUri === input.redirectUri && await verifyPkceS256(input.codeVerifier, record.codeChallenge);
73
+ if (!await transaction.consumeAuthorizationCode(codeHash, consumedAt)) relayFailure("relay_state_ambiguous", "one-time consume did not apply under its row lock");
74
+ if (!bound) {
75
+ await transaction.lockEncryptedArtifact(record.artifactId);
76
+ await transaction.claimEncryptedArtifact(record.artifactId, consumedAt);
77
+ }
78
+ outcome = bound ? Object.freeze({
79
+ kind: "released",
80
+ released: Object.freeze({
81
+ requestId: record.requestId,
82
+ artifactId: record.artifactId
83
+ })
84
+ }) : Object.freeze({
85
+ kind: "burned",
86
+ code: "relay_code_invalid"
87
+ });
88
+ } catch (error) {
89
+ await transaction.rollback();
90
+ throw error;
91
+ }
92
+ await transaction.commit();
93
+ if (outcome.kind === "burned") relayFailure(outcome.code, "authorization code redemption failed and the code was burned");
94
+ return outcome.released;
95
+ }
96
+ //#endregion
97
+ //#region src/authorization/resume.ts
98
+ /**
99
+ * Resume: fresh authentication plus a grant recovery read.
100
+ *
101
+ * Resume never transitions anything and never re-releases a code or an artifact.
102
+ * The caller re-authenticates on every call, and receives only enough state to
103
+ * decide whether its own grant work is still valid.
104
+ *
105
+ * @author taek <leekt216@gmail.com>
106
+ */
107
+ function resumeAuthorization(input) {
108
+ const now = relayNow(input.clock);
109
+ return withRelayTransaction(input.store, (transaction) => readAuthorizationState(transaction, input.requestId, now, (request) => request.clientId === input.caller.clientId));
110
+ }
111
+ //#endregion
112
+ //#region src/authorization/pickup.ts
113
+ /**
114
+ * Authenticated client pickup of a released authorization code.
115
+ *
116
+ * ```text
117
+ * state and owner reads the request and decision records; writes nothing
118
+ * transitions none — pickup is idempotent
119
+ * terminal follows the decision record
120
+ * retry positively safe? yes: the code stays one-shot at consumption, where the
121
+ * PKCE verifier and the code hash lock decide
122
+ * ```
123
+ *
124
+ * The relay mints the code, so releasing the sealed copy to the authenticated
125
+ * creating client adds no authority: consumption still requires the verifier
126
+ * only that client holds. The request's stored clientId owns who may pick up;
127
+ * any other caller reads absence, never existence.
128
+ *
129
+ * @author taek <leekt216@gmail.com>
130
+ */
131
+ async function fetchAuthorizationCode(input) {
132
+ const now = relayNow(input.clock);
133
+ const decided = await withRelayTransaction(input.store, async (transaction) => {
134
+ const request = await transaction.lockAuthorizationRequest(input.requestId);
135
+ if (!request || request.clientId !== input.caller.clientId) return relayFailure("relay_not_found", "authorization request does not exist");
136
+ const decision = await transaction.lockAuthorizationDecision(input.requestId);
137
+ if (!decision) {
138
+ if (now >= request.expiresAt) return relayFailure("relay_expired", "authorization request expired");
139
+ return null;
140
+ }
141
+ return decision;
142
+ });
143
+ if (decided === null) return Object.freeze({ outcome: "pending" });
144
+ if (decided.outcome === "rejected" || decided.codeRef === null) return Object.freeze({
145
+ outcome: "rejected",
146
+ decidedAt: decided.decidedAt
147
+ });
148
+ const codeExpiresAt = decided.codeExpiresAt ?? 0;
149
+ if (now >= codeExpiresAt) return relayFailure("relay_expired", "authorization code expired");
150
+ return Object.freeze({
151
+ outcome: "approved",
152
+ decidedAt: decided.decidedAt,
153
+ code: await openArtifact(input.kms, decided.codeRef),
154
+ codeExpiresAt
155
+ });
156
+ }
157
+ //#endregion
158
+ //#region src/security/authentication.ts
159
+ /**
160
+ * Deployment-owned client/device authentication port.
161
+ *
162
+ * The relay never parses credentials. It hands the request to the deployment,
163
+ * which returns the authenticated bindings or nothing. `clientId` and `subject`
164
+ * come only from here: wire input may never declare either.
165
+ *
166
+ * @author taek <leekt216@gmail.com>
167
+ */
168
+ const MAX_REDIRECT_URIS = 8;
169
+ function captureCaller(value) {
170
+ const context = /* @__PURE__ */ new WeakSet();
171
+ const fail = (message) => relayFailure("relay_internal", message);
172
+ const record = exactRecord(value, [
173
+ "role",
174
+ "clientId",
175
+ "subject",
176
+ "redirectUris"
177
+ ], "authenticated caller", context, fail);
178
+ if (record.role !== "client" && record.role !== "owner") return relayFailure("relay_internal", "authenticated caller role is unsupported");
179
+ const redirectUris = captureDenseArray(record.redirectUris, "caller redirectUris", context, fail);
180
+ if (redirectUris.length > MAX_REDIRECT_URIS) return relayFailure("relay_internal", "caller redirectUris is unbounded");
181
+ return Object.freeze({
182
+ role: record.role,
183
+ clientId: canonicalIdentifier(record.clientId, "caller clientId", "relay_internal"),
184
+ subject: canonicalIdentifier(record.subject, "caller subject", "relay_internal"),
185
+ redirectUris: Object.freeze(redirectUris.map((uri) => boundedText(uri, RELAY_LIMITS.redirectUri, "caller redirectUri", "relay_internal")))
186
+ });
187
+ }
188
+ async function authenticateCaller(authentication, request, required) {
189
+ let authenticated;
190
+ try {
191
+ authenticated = await authentication.authenticate(request);
192
+ } catch (error) {
193
+ if (error instanceof OaathRelayError) throw error;
194
+ return relayFailure("relay_unauthenticated", "authentication port failed");
195
+ }
196
+ if (authenticated === null || authenticated === void 0) return relayFailure("relay_unauthenticated", "caller is not authenticated");
197
+ const caller = captureCaller(authenticated);
198
+ if (!(typeof required === "string" ? [required] : required).includes(caller.role)) return relayFailure("relay_forbidden", "caller may not act in the required role");
199
+ return caller;
200
+ }
201
+ //#endregion
202
+ //#region src/security/rate-limit.ts
203
+ /**
204
+ * Optional deployment-owned limiter port.
205
+ *
206
+ * There is no default limiter: an unset port means no limiting, and the relay
207
+ * never invents a policy the deployment did not choose. A limiter that fails is
208
+ * treated as `limited`, because an unreadable limiter is not permission.
209
+ *
210
+ * @author taek <leekt216@gmail.com>
211
+ */
212
+ async function assertWithinRateLimit(limiter, input) {
213
+ if (!limiter) return;
214
+ let verdict;
215
+ try {
216
+ verdict = await limiter.check(input);
217
+ } catch {
218
+ return relayFailure("relay_rate_limited", "rate limiter is unreadable");
219
+ }
220
+ if (verdict !== "allowed") return relayFailure("relay_rate_limited", "rate limiter rejected the call");
221
+ }
222
+ //#endregion
223
+ //#region src/relay/handler.ts
224
+ /**
225
+ * Fetch-standard relay handler.
226
+ *
227
+ * Every wire input is exact-captured once here and handed to a use case as typed
228
+ * data. Every failure leaves as a structured code projected to a status; no
229
+ * message text, driver output, or internal detail reaches a response body.
230
+ *
231
+ * Endpoints:
232
+ *
233
+ * ```text
234
+ * POST /authorization/requests client create request
235
+ * GET /authorization/requests/{requestId} owner fetch request
236
+ * POST /authorization/requests/{requestId}/decision owner approve or reject
237
+ * GET /authorization/requests/{requestId}/code client released-code pickup
238
+ * POST /authorization/codes/consume client one-time code consume
239
+ * POST /authorization/artifacts/{artifactId}/claim client one-time artifact claim
240
+ * POST /authorization/resume client fresh auth + recovery read
241
+ * GET /bootstrap client URL-only service context
242
+ * POST /invalidations client capability invalidation
243
+ * POST /chains/{chainId}/{port} client chain execution relay,
244
+ * port in reads |
245
+ * observation | bundler |
246
+ * quote | submissions | usage
247
+ * POST /chains/{chainId}/paymaster/{method} client registered paymaster proxy,
248
+ * method in stub-data | data
249
+ * ```
250
+ *
251
+ * EXPERIMENTAL PREVIEW routes for the owner-phone approval surface. Preview
252
+ * means: no stability guarantee and no production qualification. Their wire
253
+ * shapes are pinned field-for-field by the strict Swift decoders in
254
+ * `native/ios/Sources/OwnerPhone/{Projection,Decision}.swift`.
255
+ *
256
+ * ```text
257
+ * GET /native/projections/{operationId} owner consent projection
258
+ * POST /native/decisions/{operationId} owner approve or reject saga
259
+ * ```
260
+ *
261
+ * @author taek <leekt216@gmail.com>
262
+ */
263
+ const DEFAULT_REQUEST_TTL_MS = 3e5;
264
+ const DEFAULT_CODE_TTL_MS = 6e4;
265
+ const DEFAULT_MAX_BODY_BYTES = 65536;
266
+ const MAX_TTL_MS = 864e5;
267
+ /** RFC 6749 ceiling, matching `MAX_AUTHORIZATION_CODE_LIFETIME` in @oaath/protocol. */
268
+ const MAX_CODE_TTL_MS = 6e5;
269
+ const MAX_PAYMASTER_REQUEST_TIMEOUT_MS = 3e4;
270
+ const HASH$1 = /^0x[0-9a-f]{64}$/u;
271
+ const INVALID = "relay_request_invalid";
272
+ const OPTION_KEYS = [
273
+ "store",
274
+ "authentication",
275
+ "kms",
276
+ "clock",
277
+ "rateLimit",
278
+ "requestTtlMs",
279
+ "codeTtlMs",
280
+ "maxBodyBytes",
281
+ "bootstrap",
282
+ "chains",
283
+ "sessionSigner",
284
+ "paymasterServices"
285
+ ];
286
+ const CHAIN_PORT_NAMES = [
287
+ "reads",
288
+ "observation",
289
+ "bundler",
290
+ "quote",
291
+ "submission"
292
+ ];
293
+ /**
294
+ * The Grant a chain port request acts for, where the port's contract names
295
+ * one: a submission's prepared operation and a usage read both carry it.
296
+ * Reading it here is bounded extraction for the invalidation gate, not
297
+ * interpretation — the port's own owner still captures the payload exactly.
298
+ */
299
+ function chainRequestGrantId(name, request) {
300
+ try {
301
+ if (name === "usage") {
302
+ const grantId = request?.grantId;
303
+ return typeof grantId === "string" && grantId.length > 0 ? grantId : null;
304
+ }
305
+ if (name === "submission") {
306
+ const grantId = (request?.prepared)?.grantId;
307
+ return typeof grantId === "string" && grantId.length > 0 ? grantId : null;
308
+ }
309
+ } catch {}
310
+ return null;
311
+ }
312
+ /**
313
+ * Ports may be class instances, so only their required capabilities are checked.
314
+ * Narrowing follows the runtime check.
315
+ */
316
+ function requirePort(value, methods, label) {
317
+ if (!value || typeof value !== "object") return relayFailure("relay_internal", `${label} must be an object`);
318
+ for (const method of methods) if (typeof value[method] !== "function") return relayFailure("relay_internal", `${label} must implement ${method}()`);
319
+ return value;
320
+ }
321
+ function duration(value, fallback, label, maximum = MAX_TTL_MS) {
322
+ if (value === void 0) return fallback;
323
+ const milliseconds = timestamp(value, label, "relay_internal");
324
+ if (milliseconds < 1 || milliseconds > maximum) return relayFailure("relay_internal", `${label} must be a bounded positive duration`);
325
+ return milliseconds;
326
+ }
327
+ function captureChainPorts(value) {
328
+ if (!Array.isArray(value)) return relayFailure("relay_internal", "chains must be an array of chain ports");
329
+ const chains = /* @__PURE__ */ new Map();
330
+ for (const entry of value) {
331
+ const port = requirePort(entry, [...CHAIN_PORT_NAMES], "chain port");
332
+ if (typeof port.chainId !== "number" || !Number.isSafeInteger(port.chainId) || port.chainId < 1) return relayFailure("relay_internal", "chain port chainId must be a positive integer");
333
+ if (port.usage !== null && typeof port.usage !== "function") return relayFailure("relay_internal", "chain port usage must be a function or null");
334
+ if (port.staticPaymasterConfigurationHash !== null && (typeof port.staticPaymasterConfigurationHash !== "string" || !HASH$1.test(port.staticPaymasterConfigurationHash))) return relayFailure("relay_internal", "chain port static paymaster commitment must be a lowercase hash or null");
335
+ if (chains.has(port.chainId)) return relayFailure("relay_internal", "chain ports repeat a chainId");
336
+ chains.set(port.chainId, port);
337
+ }
338
+ return chains;
339
+ }
340
+ function capturePaymasterServices(value, context, chains) {
341
+ if (value === void 0) return /* @__PURE__ */ new Map();
342
+ const entries = captureDenseArray(value, "paymaster services", context, (message) => relayFailure("relay_internal", message));
343
+ const services = /* @__PURE__ */ new Map();
344
+ for (const entry of entries) {
345
+ const service = exactCapturedRecord(captureRecord(entry, "paymaster service", context, (message) => relayFailure("relay_internal", message)), [
346
+ "chainId",
347
+ "providerId",
348
+ "requestTimeoutMs",
349
+ "provider"
350
+ ], "paymaster service", (message) => relayFailure("relay_internal", message));
351
+ const chainId = service.chainId;
352
+ if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId < 1) relayFailure("relay_internal", "paymaster service chainId must be a positive integer");
353
+ if (!chains.has(chainId)) relayFailure("relay_internal", "paymaster service chain is not configured");
354
+ if (services.has(chainId)) relayFailure("relay_internal", "paymaster services repeat a chainId");
355
+ if (service.requestTimeoutMs === void 0) relayFailure("relay_internal", "paymaster requestTimeoutMs is required");
356
+ services.set(chainId, Object.freeze({
357
+ chainId,
358
+ providerId: canonicalIdentifier(service.providerId, "paymaster providerId", "relay_internal"),
359
+ requestTimeoutMs: duration(service.requestTimeoutMs, 0, "paymaster requestTimeoutMs", MAX_PAYMASTER_REQUEST_TIMEOUT_MS),
360
+ provider: requirePort(service.provider, ["getPaymasterStubData", "getPaymasterData"], "paymaster provider")
361
+ }));
362
+ }
363
+ return services;
364
+ }
365
+ function captureOptions(value) {
366
+ const context = /* @__PURE__ */ new WeakSet();
367
+ const record = captureRecord(value, "relay handler options", context, (message) => relayFailure("relay_internal", message));
368
+ for (const key of Object.keys(record)) if (!OPTION_KEYS.includes(key)) relayFailure("relay_internal", "relay handler options contain an unknown field");
369
+ const chains = record.chains === void 0 ? /* @__PURE__ */ new Map() : captureChainPorts(record.chains);
370
+ const rateLimit = record.rateLimit === void 0 ? void 0 : requirePort(record.rateLimit, ["check"], "rateLimit");
371
+ const paymasterServices = capturePaymasterServices(record.paymasterServices, context, chains);
372
+ if (paymasterServices.size > 0) {
373
+ if (record.bootstrap === void 0) relayFailure("relay_internal", "a paymaster service requires a bootstrap surface");
374
+ if (rateLimit === void 0) relayFailure("relay_internal", "a paymaster service requires a rate limiter");
375
+ }
376
+ let sessionSigner = null;
377
+ if (record.sessionSigner !== void 0) {
378
+ if (record.bootstrap === void 0) relayFailure("relay_internal", "a session signer requires a bootstrap surface");
379
+ const custody = captureRecord(record.sessionSigner, "relay session signer configuration", context, (message) => relayFailure("relay_internal", message));
380
+ if (custody.mode !== "application_backend" && custody.mode !== "oaath_hosted") relayFailure("relay_internal", "session signer mode is unsupported");
381
+ if (typeof custody.providerId !== "string" || custody.providerId.length < 1) relayFailure("relay_internal", "session signer providerId is invalid");
382
+ sessionSigner = Object.freeze({
383
+ mode: custody.mode,
384
+ providerId: custody.providerId,
385
+ provider: requirePort(custody.provider, ["credential", "sign"], "session signer provider")
386
+ });
387
+ }
388
+ let bootstrap = null;
389
+ if (record.bootstrap !== void 0) {
390
+ if (chains.size === 0) relayFailure("relay_internal", "a bootstrap surface requires chain ports");
391
+ const identity = captureRecord(record.bootstrap, "relay bootstrap configuration", context, (message) => relayFailure("relay_internal", message));
392
+ try {
393
+ bootstrap = parseServiceBootstrap({
394
+ version: OAATH_SERVICE_BOOTSTRAP_VERSION,
395
+ application: identity.application,
396
+ userHandle: identity.userHandle,
397
+ account: identity.account,
398
+ ownerValidator: identity.ownerValidator,
399
+ chains: [...chains.values()].map((port) => {
400
+ const paymasterService = paymasterServices.get(port.chainId);
401
+ return {
402
+ chainId: port.chainId,
403
+ usage: port.usage !== null,
404
+ feePayer: port.feePayer,
405
+ paymasterService: paymasterService === void 0 ? null : { providerId: paymasterService.providerId },
406
+ staticPaymasterConfigurationHash: port.staticPaymasterConfigurationHash
407
+ };
408
+ }),
409
+ sessionSigner: sessionSigner === null ? {
410
+ mode: "frontend",
411
+ providerId: null
412
+ } : {
413
+ mode: sessionSigner.mode,
414
+ providerId: sessionSigner.providerId
415
+ }
416
+ });
417
+ } catch {
418
+ relayFailure("relay_internal", "relay bootstrap configuration is invalid");
419
+ }
420
+ }
421
+ return Object.freeze({
422
+ store: requirePort(record.store, ["begin", "close"], "store"),
423
+ authentication: requirePort(record.authentication, ["authenticate"], "authentication"),
424
+ kms: requirePort(record.kms, ["encrypt", "decrypt"], "kms"),
425
+ clock: requirePort(record.clock, ["now"], "clock"),
426
+ rateLimit,
427
+ requestTtlMs: duration(record.requestTtlMs, DEFAULT_REQUEST_TTL_MS, "requestTtlMs"),
428
+ codeTtlMs: duration(record.codeTtlMs, DEFAULT_CODE_TTL_MS, "codeTtlMs", MAX_CODE_TTL_MS),
429
+ maxBodyBytes: duration(record.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes"),
430
+ bootstrap,
431
+ sessionSigner,
432
+ chains,
433
+ paymasterServices
434
+ });
435
+ }
436
+ function requireMethod(request, expected) {
437
+ if (request.method !== expected) relayFailure("relay_method_not_allowed", "route does not accept this method");
438
+ }
439
+ async function bodyRecord(request, maxBodyBytes) {
440
+ if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) relayFailure(INVALID, "request body must be application/json");
441
+ let text;
442
+ try {
443
+ text = await request.text();
444
+ } catch {
445
+ return relayFailure(INVALID, "request body is unreadable");
446
+ }
447
+ if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return relayFailure(INVALID, "request body exceeds its bound");
448
+ let parsed;
449
+ try {
450
+ parsed = JSON.parse(text);
451
+ } catch {
452
+ return relayFailure(INVALID, "request body is not JSON");
453
+ }
454
+ return captureRecord(parsed, "request body", /* @__PURE__ */ new WeakSet(), (message) => relayFailure(INVALID, message));
455
+ }
456
+ function exactBody(record, keys) {
457
+ return exactCapturedRecord(record, keys, "request body", (message) => relayFailure(INVALID, message));
458
+ }
459
+ async function invokePaymasterService(service, method, caller, params) {
460
+ const controller = new AbortController();
461
+ let timeout;
462
+ try {
463
+ const deadline = new Promise((_resolve, reject) => {
464
+ timeout = setTimeout(() => {
465
+ controller.abort();
466
+ reject(/* @__PURE__ */ new Error("paymaster request deadline elapsed"));
467
+ }, service.requestTimeoutMs);
468
+ });
469
+ const capability = method === "stub-data" ? service.provider.getPaymasterStubData : service.provider.getPaymasterData;
470
+ const request = Object.freeze({
471
+ caller: Object.freeze({
472
+ clientId: caller.clientId,
473
+ subject: caller.subject
474
+ }),
475
+ params,
476
+ signal: controller.signal
477
+ });
478
+ return await Promise.race([Promise.resolve(Reflect.apply(capability, void 0, [request])), deadline]);
479
+ } catch {
480
+ return relayFailure("relay_chain_unavailable", "paymaster provider did not answer");
481
+ } finally {
482
+ if (timeout !== void 0) clearTimeout(timeout);
483
+ }
484
+ }
485
+ function decisionCommand(record) {
486
+ if (record.outcome === "approved") {
487
+ const exact = exactBody(record, ["outcome", "artifact"]);
488
+ return Object.freeze({
489
+ outcome: "approved",
490
+ artifact: boundedText(exact.artifact, RELAY_LIMITS.artifactPlaintext, "artifact", INVALID)
491
+ });
492
+ }
493
+ if (record.outcome === "rejected") {
494
+ exactBody(record, ["outcome"]);
495
+ return Object.freeze({ outcome: "rejected" });
496
+ }
497
+ return relayFailure(INVALID, "decision outcome is unsupported");
498
+ }
499
+ /**
500
+ * EXPERIMENTAL PREVIEW — the phone decision body: `{command, artifact?}`.
501
+ * `approve` hands over the artifact the client will claim once; `reject`
502
+ * carries nothing else. The saga answers the stored outcome on a replay.
503
+ */
504
+ function phoneDecisionCommand(record) {
505
+ if (record.command === "approve") {
506
+ const exact = exactBody(record, ["command", "artifact"]);
507
+ return Object.freeze({
508
+ outcome: "approved",
509
+ artifact: boundedText(exact.artifact, RELAY_LIMITS.artifactPlaintext, "artifact", INVALID)
510
+ });
511
+ }
512
+ if (record.command === "reject") {
513
+ exactBody(record, ["command"]);
514
+ return Object.freeze({ outcome: "rejected" });
515
+ }
516
+ return relayFailure(INVALID, "decision command is unsupported");
517
+ }
518
+ function stateBody(state) {
519
+ return {
520
+ requestId: state.requestId,
521
+ clientId: state.clientId,
522
+ redirectUri: state.redirectUri,
523
+ requestedScope: state.requestedScope,
524
+ expiresAt: state.expiresAt,
525
+ expired: state.expired,
526
+ decision: state.decision
527
+ };
528
+ }
529
+ function createRelayHandler(options) {
530
+ const captured = captureOptions(options);
531
+ const authenticate = async (request, role, route) => {
532
+ const caller = await authenticateCaller(captured.authentication, request, role);
533
+ await assertWithinRateLimit(captured.rateLimit, {
534
+ route,
535
+ clientId: caller.clientId
536
+ });
537
+ return caller;
538
+ };
539
+ const route = async (request) => {
540
+ const segments = new URL(request.url).pathname.split("/").filter((part) => part.length > 0);
541
+ const [head, group, third, fourth] = segments;
542
+ if (head === "native") {
543
+ if (segments.length === 3 && group === "projections") {
544
+ requireMethod(request, "GET");
545
+ const caller = await authenticate(request, "owner", "native.project");
546
+ const projection = await projectOwnerPhoneRequest({
547
+ store: captured.store,
548
+ clock: captured.clock,
549
+ caller,
550
+ requestId: canonicalIdentifier(third, "operationId", INVALID)
551
+ });
552
+ return jsonResponse(200, projection);
553
+ }
554
+ if (segments.length === 3 && group === "decisions") {
555
+ requireMethod(request, "POST");
556
+ const caller = await authenticate(request, "owner", "native.decide");
557
+ const command = phoneDecisionCommand(await bodyRecord(request, captured.maxBodyBytes));
558
+ const decided = await submitOwnerPhoneDecision({
559
+ store: captured.store,
560
+ clock: captured.clock,
561
+ kms: captured.kms,
562
+ caller,
563
+ operationId: canonicalIdentifier(third, "operationId", INVALID),
564
+ command,
565
+ codeTtlMs: captured.codeTtlMs
566
+ });
567
+ return jsonResponse(200, decided);
568
+ }
569
+ return relayFailure("relay_not_found", "route does not exist");
570
+ }
571
+ if (head === "bootstrap" && segments.length === 1) {
572
+ requireMethod(request, "GET");
573
+ await authenticate(request, "client", "bootstrap.fetch");
574
+ if (captured.bootstrap === null) return relayFailure("relay_not_found", "route does not exist");
575
+ return jsonResponse(200, captured.bootstrap);
576
+ }
577
+ if (head === "session-signers") {
578
+ const custody = captured.sessionSigner;
579
+ if (custody === null) return relayFailure("relay_not_found", "route does not exist");
580
+ if (segments.length === 1) {
581
+ requireMethod(request, "POST");
582
+ const caller = await authenticate(request, "client", "session-signers.credential");
583
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["deviceId"]);
584
+ const deviceId = boundedText(body.deviceId, 256, "session signer deviceId", INVALID);
585
+ let credential;
586
+ try {
587
+ credential = await custody.provider.credential({
588
+ clientId: caller.clientId,
589
+ subject: caller.subject,
590
+ deviceId
591
+ });
592
+ } catch {
593
+ return relayFailure("relay_internal", "session signer provider did not answer");
594
+ }
595
+ return jsonResponse(200, { operatorCredential: credential });
596
+ }
597
+ if (segments.length === 2 && group === "signatures") {
598
+ requireMethod(request, "POST");
599
+ const caller = await authenticate(request, "client", "session-signers.sign");
600
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["deviceId", "hash"]);
601
+ const deviceId = boundedText(body.deviceId, 256, "session signer deviceId", INVALID);
602
+ if (typeof body.hash !== "string" || !/^0x[0-9a-f]{64}$/u.test(body.hash)) return relayFailure(INVALID, "session signer hash must be one exact 32-byte hash");
603
+ let signature;
604
+ try {
605
+ signature = await custody.provider.sign({
606
+ clientId: caller.clientId,
607
+ subject: caller.subject,
608
+ deviceId,
609
+ hash: body.hash
610
+ });
611
+ } catch {
612
+ return relayFailure("relay_internal", "session signer provider did not answer");
613
+ }
614
+ return jsonResponse(200, { signature });
615
+ }
616
+ return relayFailure("relay_not_found", "route does not exist");
617
+ }
618
+ if (head === "invalidations" && segments.length === 1) {
619
+ requireMethod(request, "POST");
620
+ const caller = await authenticate(request, "client", "invalidations.create");
621
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["grantId", "capabilityHash"]);
622
+ const grantId = canonicalIdentifier(body.grantId, "grantId", INVALID);
623
+ const capabilityHash = boundedText(body.capabilityHash, 66, "capabilityHash", INVALID);
624
+ if (!/^0x[0-9a-f]{64}$/u.test(capabilityHash)) return relayFailure(INVALID, "capabilityHash must be a lowercase 32-byte hash");
625
+ const record = await withRelayTransaction(captured.store, async (transaction) => {
626
+ const existing = await transaction.lockCapabilityInvalidation(grantId);
627
+ if (existing) {
628
+ if (existing.clientId !== caller.clientId) return relayFailure("relay_not_found", "grant does not exist");
629
+ return existing;
630
+ }
631
+ const created = Object.freeze({
632
+ version: OAATH_CAPABILITY_INVALIDATION_RECORD_VERSION,
633
+ grantId,
634
+ clientId: caller.clientId,
635
+ capabilityHash,
636
+ invalidatedAt: relayNow(captured.clock)
637
+ });
638
+ if (!await transaction.insertCapabilityInvalidation(created)) return relayFailure("relay_internal", "capability invalidation did not commit");
639
+ return created;
640
+ });
641
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`oaath-relay-invalidation:v1:${record.clientId}:${record.grantId}:${record.capabilityHash}:${record.invalidatedAt}`));
642
+ const evidenceHash = `0x${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
643
+ return jsonResponse(200, {
644
+ evidenceHash,
645
+ invalidatedAt: Math.floor(record.invalidatedAt / 1e3)
646
+ });
647
+ }
648
+ if (head === "chains" && segments.length === 4 && third === "paymaster") {
649
+ requireMethod(request, "POST");
650
+ const service = captured.paymasterServices.get(Number(group));
651
+ const method = fourth === "stub-data" || fourth === "data" ? fourth : null;
652
+ if (service === void 0 || method === null) return relayFailure("relay_not_found", "route does not exist");
653
+ const result = await invokePaymasterService(service, method, await authenticate(request, "client", `chains.paymaster.${method}`), exactBody(await bodyRecord(request, captured.maxBodyBytes), ["params"]).params);
654
+ return jsonResponse(200, result === void 0 ? {
655
+ present: false,
656
+ result: null
657
+ } : {
658
+ present: true,
659
+ result
660
+ });
661
+ }
662
+ if (head === "chains") {
663
+ if (segments.length !== 3) return relayFailure("relay_not_found", "route does not exist");
664
+ requireMethod(request, "POST");
665
+ const port = captured.chains.get(Number(group));
666
+ const name = third === "submissions" ? "submission" : third === "reads" || third === "observation" || third === "bundler" || third === "quote" || third === "usage" ? third : null;
667
+ if (!port || name === null) return relayFailure("relay_not_found", "route does not exist");
668
+ const caller = await authenticate(request, name === "submission" ? ["client", "owner"] : "client", `chains.${name}`);
669
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["request"]);
670
+ const capability = name === "usage" ? port.usage : port[name];
671
+ if (capability === null) return relayFailure("relay_not_found", "route does not exist");
672
+ const grantId = caller.role === "owner" ? null : chainRequestGrantId(name, body.request);
673
+ if (grantId !== null) {
674
+ if (await withRelayTransaction(captured.store, (transaction) => transaction.lockCapabilityInvalidation(grantId)) !== void 0) return relayFailure("relay_capability_invalidated", "the Grant's capability is invalidated");
675
+ }
676
+ let result;
677
+ try {
678
+ result = await capability(body.request);
679
+ } catch {
680
+ return relayFailure("relay_chain_unavailable", "chain port did not answer");
681
+ }
682
+ return jsonResponse(200, result === void 0 ? {
683
+ present: false,
684
+ result: null
685
+ } : {
686
+ present: true,
687
+ result
688
+ });
689
+ }
690
+ if (head !== "authorization") relayFailure("relay_not_found", "route does not exist");
691
+ if (segments.length === 2 && group === "requests") {
692
+ requireMethod(request, "POST");
693
+ const caller = await authenticate(request, "client", "authorization.create");
694
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), [
695
+ "redirectUri",
696
+ "codeChallenge",
697
+ "requestedScope"
698
+ ]);
699
+ const created = await createAuthorizationRequest({
700
+ store: captured.store,
701
+ clock: captured.clock,
702
+ caller,
703
+ redirectUri: boundedText(body.redirectUri, RELAY_LIMITS.redirectUri, "redirectUri", INVALID),
704
+ codeChallenge: boundedText(body.codeChallenge, RELAY_LIMITS.codeChallenge, "codeChallenge", INVALID),
705
+ requestedScope: boundedText(body.requestedScope, RELAY_LIMITS.requestedScope, "requestedScope", INVALID),
706
+ requestTtlMs: captured.requestTtlMs
707
+ });
708
+ return jsonResponse(201, created);
709
+ }
710
+ if (segments.length === 3 && group === "requests") {
711
+ requireMethod(request, "GET");
712
+ const caller = await authenticate(request, "owner", "authorization.fetch");
713
+ const state = await fetchAuthorizationRequest({
714
+ store: captured.store,
715
+ clock: captured.clock,
716
+ caller,
717
+ requestId: canonicalIdentifier(third, "requestId", INVALID)
718
+ });
719
+ return jsonResponse(200, stateBody(state));
720
+ }
721
+ if (segments.length === 4 && group === "requests" && fourth === "decision") {
722
+ requireMethod(request, "POST");
723
+ const caller = await authenticate(request, "owner", "authorization.decide");
724
+ const command = decisionCommand(await bodyRecord(request, captured.maxBodyBytes));
725
+ const decided = await submitAuthorizationDecision({
726
+ store: captured.store,
727
+ clock: captured.clock,
728
+ kms: captured.kms,
729
+ caller,
730
+ requestId: canonicalIdentifier(third, "requestId", INVALID),
731
+ command,
732
+ codeTtlMs: captured.codeTtlMs
733
+ });
734
+ return jsonResponse(200, decided);
735
+ }
736
+ if (segments.length === 4 && group === "requests" && fourth === "code") {
737
+ requireMethod(request, "GET");
738
+ const caller = await authenticate(request, "client", "authorization.code");
739
+ const fetched = await fetchAuthorizationCode({
740
+ store: captured.store,
741
+ clock: captured.clock,
742
+ kms: captured.kms,
743
+ caller,
744
+ requestId: canonicalIdentifier(third, "requestId", INVALID)
745
+ });
746
+ return jsonResponse(200, fetched);
747
+ }
748
+ if (segments.length === 3 && group === "codes" && third === "consume") {
749
+ requireMethod(request, "POST");
750
+ const caller = await authenticate(request, "client", "authorization.consume");
751
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), [
752
+ "code",
753
+ "codeVerifier",
754
+ "redirectUri"
755
+ ]);
756
+ const consumed = await consumeAuthorizationCode({
757
+ store: captured.store,
758
+ clock: captured.clock,
759
+ caller,
760
+ code: canonicalIdentifier(body.code, "code", INVALID),
761
+ codeVerifier: boundedText(body.codeVerifier, RELAY_LIMITS.codeVerifier, "codeVerifier", INVALID),
762
+ redirectUri: boundedText(body.redirectUri, RELAY_LIMITS.redirectUri, "redirectUri", INVALID)
763
+ });
764
+ return jsonResponse(200, consumed);
765
+ }
766
+ if (segments.length === 4 && group === "artifacts" && fourth === "claim") {
767
+ requireMethod(request, "POST");
768
+ const caller = await authenticate(request, "client", "authorization.claim");
769
+ const claimed = await claimEncryptedArtifact({
770
+ store: captured.store,
771
+ clock: captured.clock,
772
+ kms: captured.kms,
773
+ caller,
774
+ artifactId: canonicalIdentifier(third, "artifactId", INVALID)
775
+ });
776
+ return jsonResponse(200, claimed);
777
+ }
778
+ if (segments.length === 2 && group === "resume") {
779
+ requireMethod(request, "POST");
780
+ const caller = await authenticate(request, "client", "authorization.resume");
781
+ const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["requestId"]);
782
+ const state = await resumeAuthorization({
783
+ store: captured.store,
784
+ clock: captured.clock,
785
+ caller,
786
+ requestId: canonicalIdentifier(body.requestId, "requestId", INVALID)
787
+ });
788
+ return jsonResponse(200, stateBody(state));
789
+ }
790
+ return relayFailure("relay_not_found", "route does not exist");
791
+ };
792
+ return async (request) => {
793
+ try {
794
+ return await route(request);
795
+ } catch (error) {
796
+ return relayErrorResponse(relayErrorCode(error));
797
+ }
798
+ };
799
+ }
800
+ //#endregion
801
+ //#region src/security/redact.ts
802
+ /**
803
+ * Bounded credential and URL redaction for diagnostics only.
804
+ *
805
+ * Nothing here shapes a response or a machine decision. Its only job is to make
806
+ * an adopter's log line safe and finite: no authorization code, verifier, token,
807
+ * artifact, ciphertext, credential-bearing URL, or unbounded provider blob.
808
+ *
809
+ * @author taek <leekt216@gmail.com>
810
+ */
811
+ const REDACTED = "[redacted]";
812
+ const UNREADABLE_URL = "[unreadable-url]";
813
+ const TRUNCATED = "[truncated]";
814
+ const MAX_TEXT = 128;
815
+ const MAX_DEPTH = 3;
816
+ const MAX_ENTRIES = 16;
817
+ /** Key names whose values are never safe to log, matched after normalization. */
818
+ const SENSITIVE_KEYS = /* @__PURE__ */ new Set([
819
+ "apikey",
820
+ "artifact",
821
+ "authorization",
822
+ "bearer",
823
+ "ciphertext",
824
+ "ciphertextref",
825
+ "clientsecret",
826
+ "code",
827
+ "codechallenge",
828
+ "codeverifier",
829
+ "cookie",
830
+ "credential",
831
+ "idtoken",
832
+ "key",
833
+ "password",
834
+ "plaintext",
835
+ "privatekey",
836
+ "secret",
837
+ "session",
838
+ "sessionkey",
839
+ "setcookie",
840
+ "signature",
841
+ "token",
842
+ "verifier"
843
+ ]);
844
+ function normalizeKey(key) {
845
+ return key.toLowerCase().replaceAll(/[^a-z0-9]/gu, "");
846
+ }
847
+ function isUrlKey(normalized) {
848
+ return normalized.endsWith("url") || normalized.endsWith("uri");
849
+ }
850
+ function boundText(value) {
851
+ return value.length <= MAX_TEXT ? value : `${value.slice(0, MAX_TEXT)}${TRUNCATED}`;
852
+ }
853
+ /** Keeps scheme, host, and path. Drops userinfo, query, and fragment. */
854
+ function redactUrl(value) {
855
+ if (typeof value !== "string") return UNREADABLE_URL;
856
+ let url;
857
+ try {
858
+ url = new URL(value);
859
+ } catch {
860
+ return UNREADABLE_URL;
861
+ }
862
+ const port = url.port === "" ? "" : `:${url.port}`;
863
+ return boundText(`${url.protocol}//${url.hostname}${port}${url.pathname}`);
864
+ }
865
+ function redactValue(value, depth) {
866
+ if (value === null) return null;
867
+ switch (typeof value) {
868
+ case "string": return boundText(value);
869
+ case "number": return Number.isFinite(value) ? value : String(value);
870
+ case "boolean": return value;
871
+ case "bigint": return boundText(value.toString());
872
+ case "object": break;
873
+ default: return REDACTED;
874
+ }
875
+ if (depth >= MAX_DEPTH) return REDACTED;
876
+ if (Array.isArray(value)) {
877
+ const items = value.slice(0, MAX_ENTRIES).map((item) => redactValue(item, depth + 1));
878
+ return value.length > MAX_ENTRIES ? [...items, TRUNCATED] : items;
879
+ }
880
+ if (value instanceof Error) return {
881
+ name: boundText(value.name),
882
+ message: REDACTED
883
+ };
884
+ return redactRecord(value, depth);
885
+ }
886
+ function redactRecord(value, depth) {
887
+ const redacted = {};
888
+ let entries = 0;
889
+ for (const key of Object.keys(value)) {
890
+ if (entries >= MAX_ENTRIES) {
891
+ redacted[TRUNCATED] = true;
892
+ break;
893
+ }
894
+ entries += 1;
895
+ const normalized = normalizeKey(key);
896
+ if (SENSITIVE_KEYS.has(normalized)) redacted[key] = REDACTED;
897
+ else if (isUrlKey(normalized)) redacted[key] = redactUrl(value[key]);
898
+ else redacted[key] = redactValue(value[key], depth + 1);
899
+ }
900
+ return redacted;
901
+ }
902
+ /** Returns a bounded, credential-free projection safe to hand to a logger. */
903
+ function redactForLog(value) {
904
+ return redactValue(value, 0);
905
+ }
906
+ //#endregion
907
+ //#region src/session-signer/kms-provider.ts
908
+ /**
909
+ * Reference hosted session-signer provider: one KMS-sealed secp256k1 operator
910
+ * key per authenticated `(clientId, subject, deviceId)` identity, signing one
911
+ * exact 32-byte hash at a time.
912
+ *
913
+ * Custody boundary: the private scalar exists in plaintext only inside this
914
+ * process during key generation and signing; at rest it is exactly one
915
+ * KMS-sealed string. No route, log, or return value ever carries key material
916
+ * — `credential` answers the public operator profile and `sign` answers one
917
+ * 65-byte ECDSA signature.
918
+ *
919
+ * Rotation invariant: one identity maps to one credential for this provider's
920
+ * lifetime. There is no rotation path here — a new key requires a new
921
+ * identity, and therefore a newly approved Grant; nothing can silently swap
922
+ * the public key under an existing approval.
923
+ *
924
+ * Like `createMemoryRelayStore`, the in-memory registry makes this a
925
+ * reference implementation, not a production durability claim: a deployment
926
+ * that must survive restarts persists the sealed entries in its own store.
927
+ *
928
+ * @author taek <leekt216@gmail.com>
929
+ */
930
+ const HASH = /^0x[0-9a-f]{64}$/u;
931
+ const MAX_IDENTITY_PART = 256;
932
+ function identityKey(request) {
933
+ for (const part of [
934
+ request.clientId,
935
+ request.subject,
936
+ request.deviceId
937
+ ]) if (typeof part !== "string" || part.length < 1 || part.length > MAX_IDENTITY_PART) throw new Error("session signer identity is invalid");
938
+ return JSON.stringify([
939
+ request.clientId,
940
+ request.subject,
941
+ request.deviceId
942
+ ]);
943
+ }
944
+ function addressOf(privateKey) {
945
+ const publicKey = secp256k1.getPublicKey(privateKey, false);
946
+ return `0x${bytesToHex(keccak_256(publicKey.slice(1)).slice(12))}`;
947
+ }
948
+ function createKmsSessionSignerProvider(input) {
949
+ const kms = input.kms;
950
+ /** identity -> KMS-sealed private scalar; the only at-rest key form. */
951
+ const sealed = /* @__PURE__ */ new Map();
952
+ /** Serializes first-use creation so one identity never mints two keys. */
953
+ const creating = /* @__PURE__ */ new Map();
954
+ async function sealedKeyFor(identity) {
955
+ const existing = sealed.get(identity);
956
+ if (existing !== void 0) return existing;
957
+ const pending = creating.get(identity) ?? (async () => {
958
+ const scalar = secp256k1.utils.randomPrivateKey();
959
+ const reference = String(await kms.encrypt(bytesToHex(scalar)));
960
+ scalar.fill(0);
961
+ sealed.set(identity, reference);
962
+ return reference;
963
+ })();
964
+ creating.set(identity, pending);
965
+ pending.finally(() => creating.delete(identity));
966
+ return pending;
967
+ }
968
+ return Object.freeze({
969
+ async credential(request) {
970
+ const identity = identityKey(request);
971
+ const privateKey = hexToBytes(String(await kms.decrypt(await sealedKeyFor(identity))));
972
+ try {
973
+ return Object.freeze({
974
+ version: OAATH_OPERATOR_CREDENTIAL_PROFILE_VERSION,
975
+ kind: "ecdsa",
976
+ address: addressOf(privateKey)
977
+ });
978
+ } finally {
979
+ privateKey.fill(0);
980
+ }
981
+ },
982
+ async sign(request) {
983
+ const identity = identityKey(request);
984
+ if (typeof request.hash !== "string" || !HASH.test(request.hash)) throw new Error("session signer hash must be one exact 32-byte hash");
985
+ const reference = sealed.get(identity);
986
+ if (reference === void 0) throw new Error("session signer identity holds no credential");
987
+ const privateKey = hexToBytes(String(await kms.decrypt(reference)));
988
+ try {
989
+ const signature = secp256k1.sign(hexToBytes(request.hash.slice(2)), privateKey, { lowS: true });
990
+ if (signature.recovery !== 0 && signature.recovery !== 1) throw new Error("session signer produced an unusable signature");
991
+ return `0x${bytesToHex(signature.toCompactRawBytes())}${(27 + signature.recovery).toString(16)}`;
992
+ } finally {
993
+ privateKey.fill(0);
994
+ }
995
+ }
996
+ });
997
+ }
998
+ //#endregion
999
+ //#region src/store/memory.ts
1000
+ /**
1001
+ * In-memory relay store implementing the same transaction contract as
1002
+ * PostgreSQL. It is durable only for the life of the process: use it for local
1003
+ * development and tests, never for a deployment that must survive a restart.
1004
+ *
1005
+ * ponytail: one process-wide writer lock stands in for row locks. It is strictly
1006
+ * stronger than `SELECT ... FOR UPDATE`, so one-shot semantics are identical;
1007
+ * swap in per-row locks only if a single-process relay ever needs throughput.
1008
+ *
1009
+ * @author taek <leekt216@gmail.com>
1010
+ */
1011
+ function emptyTables() {
1012
+ return {
1013
+ requests: /* @__PURE__ */ new Map(),
1014
+ decisions: /* @__PURE__ */ new Map(),
1015
+ codes: /* @__PURE__ */ new Map(),
1016
+ artifacts: /* @__PURE__ */ new Map(),
1017
+ invalidations: /* @__PURE__ */ new Map()
1018
+ };
1019
+ }
1020
+ function copyTables(tables) {
1021
+ return {
1022
+ requests: new Map(tables.requests),
1023
+ decisions: new Map(tables.decisions),
1024
+ codes: new Map(tables.codes),
1025
+ artifacts: new Map(tables.artifacts),
1026
+ invalidations: new Map(tables.invalidations)
1027
+ };
1028
+ }
1029
+ function insert(table, key, record) {
1030
+ if (table.has(key)) return false;
1031
+ table.set(key, record);
1032
+ return true;
1033
+ }
1034
+ function read(table, key, parse) {
1035
+ const stored = table.get(key);
1036
+ return stored === void 0 ? void 0 : parse(stored);
1037
+ }
1038
+ function createMemoryRelayStore() {
1039
+ let committed = emptyTables();
1040
+ let closed = false;
1041
+ let tail = Promise.resolve();
1042
+ async function acquire() {
1043
+ const prior = tail;
1044
+ let release = () => {};
1045
+ tail = new Promise((resolve) => {
1046
+ release = resolve;
1047
+ });
1048
+ await prior;
1049
+ return release;
1050
+ }
1051
+ return {
1052
+ async begin() {
1053
+ if (closed) relayFailure("relay_store_unavailable", "memory relay store is closed");
1054
+ const release = await acquire();
1055
+ const staged = copyTables(committed);
1056
+ let settled = false;
1057
+ const open = () => {
1058
+ if (settled) relayFailure("relay_internal", "relay transaction already settled");
1059
+ return staged;
1060
+ };
1061
+ const settle = (apply) => {
1062
+ if (settled) relayFailure("relay_internal", "relay transaction already settled");
1063
+ settled = true;
1064
+ if (apply) committed = staged;
1065
+ release();
1066
+ };
1067
+ return {
1068
+ async lockAuthorizationRequest(requestId) {
1069
+ return read(open().requests, requestId, parseAuthorizationRequestRecord);
1070
+ },
1071
+ async insertAuthorizationRequest(record) {
1072
+ return insert(open().requests, record.requestId, record);
1073
+ },
1074
+ async lockAuthorizationDecision(requestId) {
1075
+ return read(open().decisions, requestId, parseAuthorizationDecisionRecord);
1076
+ },
1077
+ async insertAuthorizationDecision(record) {
1078
+ return insert(open().decisions, record.requestId, record);
1079
+ },
1080
+ async lockAuthorizationCode(codeHash) {
1081
+ return read(open().codes, codeHash, parseAuthorizationCodeRecord);
1082
+ },
1083
+ async insertAuthorizationCode(record) {
1084
+ return insert(open().codes, record.codeHash, record);
1085
+ },
1086
+ async consumeAuthorizationCode(codeHash, consumedAt) {
1087
+ const codes = open().codes;
1088
+ const record = read(codes, codeHash, parseAuthorizationCodeRecord);
1089
+ if (!record || record.consumedAt !== null) return false;
1090
+ codes.set(codeHash, Object.freeze({
1091
+ ...record,
1092
+ consumedAt
1093
+ }));
1094
+ return true;
1095
+ },
1096
+ async lockCapabilityInvalidation(grantId) {
1097
+ return read(open().invalidations, grantId, parseCapabilityInvalidationRecord);
1098
+ },
1099
+ async insertCapabilityInvalidation(record) {
1100
+ return insert(open().invalidations, record.grantId, record);
1101
+ },
1102
+ async lockEncryptedArtifact(artifactId) {
1103
+ return read(open().artifacts, artifactId, parseEncryptedArtifactRecord);
1104
+ },
1105
+ async insertEncryptedArtifact(record) {
1106
+ return insert(open().artifacts, record.artifactId, record);
1107
+ },
1108
+ async claimEncryptedArtifact(artifactId, claimedAt) {
1109
+ const artifacts = open().artifacts;
1110
+ const record = read(artifacts, artifactId, parseEncryptedArtifactRecord);
1111
+ if (!record || record.claimedAt !== null) return false;
1112
+ artifacts.set(artifactId, Object.freeze({
1113
+ ...record,
1114
+ claimedAt
1115
+ }));
1116
+ return true;
1117
+ },
1118
+ async commit() {
1119
+ settle(true);
1120
+ },
1121
+ async rollback() {
1122
+ if (!settled) settle(false);
1123
+ }
1124
+ };
1125
+ },
1126
+ async close() {
1127
+ closed = true;
1128
+ committed = emptyTables();
1129
+ }
1130
+ };
1131
+ }
1132
+ //#endregion
1133
+ export { OAATH_AUTHORIZATION_CODE_RECORD_VERSION, OAATH_AUTHORIZATION_DECISION_RECORD_VERSION, OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION, OAATH_ENCRYPTED_ARTIFACT_RECORD_VERSION, OaathRelayError, REDACTED, RELAY_ERROR_STATUS, claimEncryptedArtifact, consumeAuthorizationCode, createAuthorizationRequest, createKmsSessionSignerProvider, createMemoryRelayStore, createRelayHandler, fetchAuthorizationRequest, isCodeChallengeS256, openArtifact, redactForLog, redactUrl, resumeAuthorization, sealArtifact, submitAuthorizationDecision, verifyPkceS256 };
1134
+
1135
+ //# sourceMappingURL=index.js.map