@oaath/server 0.1.0 → 0.3.1

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 (50) hide show
  1. package/README.md +239 -5
  2. package/dist/apns.d.ts +2 -2
  3. package/dist/apns.js +2 -2
  4. package/dist/{challenge-AGTmUfj2.js → challenge-DmpGSogj.js} +2 -2
  5. package/dist/{challenge-AGTmUfj2.js.map → challenge-DmpGSogj.js.map} +1 -1
  6. package/dist/inbox-B80EUVht.js +249 -0
  7. package/dist/inbox-B80EUVht.js.map +1 -0
  8. package/dist/index.d.ts +9 -255
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +631 -48
  11. package/dist/index.js.map +1 -1
  12. package/dist/interface-DHpwP0wr.d.ts +245 -0
  13. package/dist/interface-DHpwP0wr.d.ts.map +1 -0
  14. package/dist/kernel.d.ts +42 -0
  15. package/dist/kernel.d.ts.map +1 -0
  16. package/dist/kernel.js +80 -0
  17. package/dist/kernel.js.map +1 -0
  18. package/dist/kms-BWkywdJM.d.ts +18 -0
  19. package/dist/kms-BWkywdJM.d.ts.map +1 -0
  20. package/dist/native.d.ts +3 -30
  21. package/dist/native.js +115 -2
  22. package/dist/native.js.map +1 -0
  23. package/dist/postgres.d.ts +38 -3
  24. package/dist/postgres.d.ts.map +1 -1
  25. package/dist/postgres.js +301 -14
  26. package/dist/postgres.js.map +1 -1
  27. package/dist/projection-B7IB9irs.d.ts +228 -0
  28. package/dist/projection-B7IB9irs.d.ts.map +1 -0
  29. package/dist/{records-Bgr-9PUh.js → records-BQNDxbPg.js} +32 -8
  30. package/dist/records-BQNDxbPg.js.map +1 -0
  31. package/dist/records-BUvqmp0D.js +70 -0
  32. package/dist/records-BUvqmp0D.js.map +1 -0
  33. package/dist/service-Cf4f-9KP.js +706 -0
  34. package/dist/service-Cf4f-9KP.js.map +1 -0
  35. package/dist/service-J51bOVe6.d.ts +483 -0
  36. package/dist/service-J51bOVe6.d.ts.map +1 -0
  37. package/package.json +39 -8
  38. package/LICENSE +0 -201
  39. package/dist/authentication-HUYWk5Y2.d.ts +0 -46
  40. package/dist/authentication-HUYWk5Y2.d.ts.map +0 -1
  41. package/dist/decision-dEe9n108.d.ts +0 -53
  42. package/dist/decision-dEe9n108.d.ts.map +0 -1
  43. package/dist/interface-WwvwWs8e.d.ts +0 -114
  44. package/dist/interface-WwvwWs8e.d.ts.map +0 -1
  45. package/dist/native.d.ts.map +0 -1
  46. package/dist/projection-dGIkE0rI.js +0 -516
  47. package/dist/projection-dGIkE0rI.js.map +0 -1
  48. package/dist/projection-g6k_CCUK.d.ts +0 -136
  49. package/dist/projection-g6k_CCUK.d.ts.map +0 -1
  50. package/dist/records-Bgr-9PUh.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
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";
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-BQNDxbPg.js";
2
+ import { a as relayNow, i as verifyPkceS256, r as sha256Base64Url, t as isCodeChallengeS256 } from "./challenge-DmpGSogj.js";
3
+ import { S as withRelayTransaction, a as submitOwnerPhoneRevocationDecision, b as openArtifact, c as projectOwnerPhonePermissionSigning, d as classifyStoredAuthorizationScope, f as createAuthorizationRequest, i as requestOwnerPhoneRevocation, l as projectOwnerPhoneRequest, m as readAuthorizationState, n as fetchOwnerPhoneRevocation, p as fetchAuthorizationRequest, t as fetchClientPhoneRevocation, x as sealArtifact, y as readApprovedPermission } from "./service-Cf4f-9KP.js";
4
+ import { i as submitAuthorizationDecision, n as listOwnerPhoneRequests, r as submitOwnerPhoneDecision } from "./inbox-B80EUVht.js";
5
+ import { a as parseRevocationRequestRecord, i as parseRevocationDecisionRecord, n as OAATH_REVOCATION_REQUEST_RECORD_VERSION, t as OAATH_REVOCATION_DECISION_RECORD_VERSION } from "./records-BUvqmp0D.js";
6
+ import { OAATH_GRANT_REFERENCE_APPROVED_REVISION, OAATH_GRANT_REFERENCE_VERSION, OAATH_OPERATOR_CREDENTIAL_PROFILE_VERSION, OAATH_SERVICE_BOOTSTRAP_VERSION, OAATH_WORKSPACE_ACCOUNT_CONTEXT_VERSION, captureDenseArray, captureRecord, captureServiceAccount, exactCapturedRecord, hashGrantPolicy, hashGrantPolicyCalls, isKernelExistingAccountProfile, parseAccountId, parseClientId, parseServiceBootstrap, parseVerifyGrantRevisionInput, readRpcBundlerRejection, readValidationGasDiagnostic } from "@oaath/protocol";
5
7
  import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
6
8
  import { secp256k1 } from "@noble/curves/secp256k1.js";
7
9
  import { keccak_256 } from "@noble/hashes/sha3.js";
@@ -109,6 +111,284 @@ function resumeAuthorization(input) {
109
111
  return withRelayTransaction(input.store, (transaction) => readAuthorizationState(transaction, input.requestId, now, (request) => request.clientId === input.caller.clientId));
110
112
  }
111
113
  //#endregion
114
+ //#region src/directory/records.ts
115
+ const OAATH_SERVICE_DIRECTORY_VERSION = "oaath.service-directory/v1";
116
+ /** Captures one configuration or durable-storage boundary, including reference meaning. */
117
+ function parseServiceDirectory(value, code) {
118
+ const context = /* @__PURE__ */ new WeakSet();
119
+ const fail = (message) => relayFailure(code, message);
120
+ const record = (entry, keys) => exactCapturedRecord(captureRecord(entry, "service directory record", context, fail), keys, "service directory record", fail);
121
+ const id = (entry) => parseClientId(entry, fail);
122
+ const subject = (entry) => boundedText(entry, 256, "directory subject", code);
123
+ function rows(entries, parse, key) {
124
+ const seen = /* @__PURE__ */ new Set();
125
+ return Object.freeze(captureDenseArray(entries, "directory records", context, fail).map((entry) => {
126
+ const parsed = parse(entry);
127
+ const identity = JSON.stringify(key(parsed));
128
+ if (seen.has(identity)) return fail("directory records repeat an identity");
129
+ seen.add(identity);
130
+ return Object.freeze(parsed);
131
+ }));
132
+ }
133
+ try {
134
+ const root = record(value, [
135
+ "version",
136
+ "workspaces",
137
+ "applications",
138
+ "memberships",
139
+ "ownerDevices",
140
+ "accounts",
141
+ "selections"
142
+ ]);
143
+ if (root.version !== "oaath.service-directory/v1") return fail("service directory version is unsupported");
144
+ const workspaces = rows(root.workspaces, (value) => {
145
+ const entry = record(value, ["workspaceId", "kind"]);
146
+ if (entry.kind !== "personal" && entry.kind !== "team") return fail("workspace kind is unsupported");
147
+ return {
148
+ workspaceId: id(entry.workspaceId),
149
+ kind: entry.kind
150
+ };
151
+ }, (entry) => [entry.workspaceId]);
152
+ const applications = rows(root.applications, (value) => {
153
+ const entry = record(value, [
154
+ "clientId",
155
+ "applicationId",
156
+ "applicationName"
157
+ ]);
158
+ return {
159
+ clientId: id(entry.clientId),
160
+ applicationId: id(entry.applicationId),
161
+ applicationName: boundedText(entry.applicationName, 256, "application name", code)
162
+ };
163
+ }, (entry) => [entry.clientId]);
164
+ const memberships = rows(root.memberships, (value) => {
165
+ const entry = record(value, [
166
+ "workspaceId",
167
+ "clientId",
168
+ "subject"
169
+ ]);
170
+ return {
171
+ workspaceId: id(entry.workspaceId),
172
+ clientId: id(entry.clientId),
173
+ subject: subject(entry.subject)
174
+ };
175
+ }, (entry) => [
176
+ entry.workspaceId,
177
+ entry.clientId,
178
+ entry.subject
179
+ ]);
180
+ const ownerDevices = rows(root.ownerDevices, (value) => {
181
+ const entry = record(value, [
182
+ "workspaceId",
183
+ "ownerDeviceId",
184
+ "subject"
185
+ ]);
186
+ return {
187
+ workspaceId: id(entry.workspaceId),
188
+ ownerDeviceId: id(entry.ownerDeviceId),
189
+ subject: subject(entry.subject)
190
+ };
191
+ }, (entry) => [entry.workspaceId, entry.ownerDeviceId]);
192
+ const accounts = rows(root.accounts, (value) => {
193
+ const entry = record(value, [
194
+ "workspaceId",
195
+ "accountId",
196
+ "ownerDeviceId",
197
+ "account",
198
+ "ownerValidator",
199
+ "chainIds"
200
+ ]);
201
+ const chainIds = captureDenseArray(entry.chainIds, "account chains", context, fail).map((chainId) => {
202
+ if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId < 1) return fail("account chain must be a positive integer");
203
+ return chainId;
204
+ });
205
+ if (chainIds.length < 1 || chainIds.length > 32 || new Set(chainIds).size !== chainIds.length) return fail("account chains must contain 1 to 32 distinct chains");
206
+ return {
207
+ workspaceId: id(entry.workspaceId),
208
+ accountId: parseAccountId(entry.accountId, fail),
209
+ ownerDeviceId: id(entry.ownerDeviceId),
210
+ ...captureServiceAccount(entry.account, entry.ownerValidator, context, fail),
211
+ chainIds: Object.freeze(chainIds)
212
+ };
213
+ }, (entry) => [entry.workspaceId, entry.accountId]);
214
+ const selections = rows(root.selections, (value) => {
215
+ const entry = record(value, [
216
+ "clientId",
217
+ "subject",
218
+ "workspaceId",
219
+ "accountId"
220
+ ]);
221
+ return {
222
+ clientId: id(entry.clientId),
223
+ subject: subject(entry.subject),
224
+ workspaceId: id(entry.workspaceId),
225
+ accountId: parseAccountId(entry.accountId, fail)
226
+ };
227
+ }, (entry) => [entry.clientId, entry.subject]);
228
+ const workspaceIds = new Set(workspaces.map((entry) => entry.workspaceId));
229
+ const clientIds = new Set(applications.map((entry) => entry.clientId));
230
+ for (const entry of [
231
+ ...memberships,
232
+ ...ownerDevices,
233
+ ...accounts
234
+ ]) if (!workspaceIds.has(entry.workspaceId)) return fail("directory reference names an absent workspace");
235
+ for (const entry of memberships) if (!clientIds.has(entry.clientId)) return fail("membership names an absent application");
236
+ for (const account of accounts) if (!ownerDevices.some((owner) => owner.workspaceId === account.workspaceId && owner.ownerDeviceId === account.ownerDeviceId)) return fail("account names an absent owner device");
237
+ return Object.freeze({
238
+ version: OAATH_SERVICE_DIRECTORY_VERSION,
239
+ workspaces,
240
+ applications,
241
+ memberships,
242
+ ownerDevices,
243
+ accounts,
244
+ selections
245
+ });
246
+ } catch {
247
+ return fail("service directory is invalid");
248
+ }
249
+ }
250
+ //#endregion
251
+ //#region src/directory/service.ts
252
+ /**
253
+ * Owns membership/account selection and atomic phone enrollment over one
254
+ * versioned directory document. Enrollment moves absent device/accounts to
255
+ * jointly registered identities; it cannot overwrite them. Its single CAS is
256
+ * the persisted evidence and cleanup owner: a lost response causes no retry,
257
+ * and a recreated reader sees the whole enrollment or none of it.
258
+ * Writes use compare-and-swap; bootstrap/admission reads never reserve resources.
259
+ * A reload reads durable state afresh. Member removal blocks new admission but
260
+ * does not revoke grants or delete operation evidence. The deployment owns store
261
+ * resources and cleanup; this service owns no connections and retries no writes.
262
+ */
263
+ function createServiceDirectory(store) {
264
+ async function read() {
265
+ const raw = await store.read();
266
+ if (raw === null) return null;
267
+ try {
268
+ const fail = (message) => relayFailure("relay_record_unreadable", message);
269
+ const snapshot = exactCapturedRecord(captureRecord(raw, "directory snapshot", /* @__PURE__ */ new WeakSet(), fail), ["revision", "directory"], "directory snapshot", fail);
270
+ if (typeof snapshot.revision !== "number" || !Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1) return fail("directory revision is invalid");
271
+ return Object.freeze({
272
+ revision: snapshot.revision,
273
+ directory: parseServiceDirectory(snapshot.directory, "relay_record_unreadable")
274
+ });
275
+ } catch {
276
+ return relayFailure("relay_record_unreadable", "directory snapshot is unreadable");
277
+ }
278
+ }
279
+ function assigned(directory, caller, workspaceId) {
280
+ return caller.role === "client" && directory.memberships.some((member) => member.workspaceId === workspaceId && member.clientId === caller.clientId && member.subject === caller.subject);
281
+ }
282
+ async function resolveOwner(caller, input, chainId) {
283
+ const scope = classifyStoredAuthorizationScope(input.requestedScope, input.requestId);
284
+ if (scope.kind !== "permission-request") return null;
285
+ const request = scope.request;
286
+ const snapshot = await read();
287
+ if (snapshot === null || !assigned(snapshot.directory, caller, request.context.workspaceId)) return null;
288
+ const directory = snapshot.directory;
289
+ const workspace = directory.workspaces.find((entry) => entry.workspaceId === request.context.workspaceId);
290
+ const application = directory.applications.find((entry) => entry.clientId === caller.clientId);
291
+ const account = directory.accounts.find((entry) => entry.workspaceId === request.context.workspaceId && entry.accountId === request.context.accountId);
292
+ if (!workspace || !application || !account || chainId !== void 0 && !account.chainIds.includes(chainId) || workspace.kind !== request.context.workspaceKind || application.applicationId !== request.application.applicationId || caller.clientId !== request.application.clientId || JSON.stringify(account.account) !== JSON.stringify(request.logicalAccount)) return null;
293
+ const device = directory.ownerDevices.find((entry) => entry.workspaceId === workspace.workspaceId && entry.ownerDeviceId === account.ownerDeviceId);
294
+ if (!device) return null;
295
+ return Object.freeze({
296
+ ownerDeviceId: device.ownerDeviceId,
297
+ ownerSubject: device.subject
298
+ });
299
+ }
300
+ return Object.freeze({
301
+ read,
302
+ resolveOwner,
303
+ resolveRevocationOwner(caller, input) {
304
+ return resolveOwner(caller, input, input.chainId);
305
+ },
306
+ async replace(input) {
307
+ if (input.expectedRevision !== null && (!Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 1 || input.expectedRevision === Number.MAX_SAFE_INTEGER)) return relayFailure("relay_request_invalid", "expected directory revision is invalid");
308
+ return store.compareAndSwap(input.expectedRevision, parseServiceDirectory(input.directory, "relay_request_invalid"));
309
+ },
310
+ async enrollOwnerDevice(input) {
311
+ const enrollment = structuredClone(input);
312
+ if (!Number.isSafeInteger(enrollment.expectedRevision) || enrollment.expectedRevision < 1 || enrollment.expectedRevision === Number.MAX_SAFE_INTEGER) return relayFailure("relay_request_invalid", "expected directory revision is invalid");
313
+ const snapshot = await read();
314
+ if (snapshot === null) return relayFailure("relay_not_found", "service directory is not initialized");
315
+ if (snapshot.revision !== enrollment.expectedRevision) return false;
316
+ const proposed = parseServiceDirectory({
317
+ ...snapshot.directory,
318
+ ownerDevices: [...snapshot.directory.ownerDevices, enrollment.device],
319
+ accounts: [...snapshot.directory.accounts, ...enrollment.accounts]
320
+ }, "relay_request_invalid");
321
+ const device = proposed.ownerDevices.at(-1);
322
+ const accounts = proposed.accounts.slice(snapshot.directory.accounts.length);
323
+ const owner = accounts[0]?.account.ownerCredential;
324
+ if (!device || owner?.kind !== "p256" || accounts.some((account) => account.workspaceId !== device.workspaceId || account.ownerDeviceId !== device.ownerDeviceId || isKernelExistingAccountProfile(account.account) || account.account.factoryRoute !== "kernel_factory" || account.account.ownerCredential.kind !== "p256" || account.account.ownerCredential.publicKey !== owner.publicKey)) return relayFailure("relay_request_invalid", "phone accounts must bind one workspace, device, and P-256 owner");
325
+ return store.compareAndSwap(enrollment.expectedRevision, proposed);
326
+ },
327
+ async selectAccount(caller, selection) {
328
+ const fail = (message) => relayFailure("relay_request_invalid", message);
329
+ const workspaceId = parseClientId(selection.workspaceId, fail);
330
+ const accountId = parseAccountId(selection.accountId, fail);
331
+ const snapshot = await read();
332
+ if (snapshot === null || !assigned(snapshot.directory, caller, workspaceId)) return relayFailure("relay_forbidden", "caller is not a member of this workspace");
333
+ if (!snapshot.directory.accounts.some((account) => account.workspaceId === workspaceId && account.accountId === accountId)) return relayFailure("relay_not_found", "account is not registered in this workspace");
334
+ const selections = snapshot.directory.selections.filter((entry) => entry.clientId !== caller.clientId || entry.subject !== caller.subject);
335
+ selections.push(Object.freeze({
336
+ clientId: caller.clientId,
337
+ subject: caller.subject,
338
+ workspaceId,
339
+ accountId
340
+ }));
341
+ return store.compareAndSwap(snapshot.revision, Object.freeze({
342
+ ...snapshot.directory,
343
+ selections: Object.freeze(selections)
344
+ }));
345
+ },
346
+ async resolve(caller) {
347
+ const snapshot = await read();
348
+ if (snapshot === null) return null;
349
+ const directory = snapshot.directory;
350
+ const selection = directory.selections.find((entry) => entry.clientId === caller.clientId && entry.subject === caller.subject);
351
+ if (!selection || !assigned(directory, caller, selection.workspaceId)) return null;
352
+ const workspace = directory.workspaces.find((entry) => entry.workspaceId === selection.workspaceId);
353
+ const application = directory.applications.find((entry) => entry.clientId === caller.clientId);
354
+ const account = directory.accounts.find((entry) => entry.workspaceId === selection.workspaceId && entry.accountId === selection.accountId);
355
+ if (!workspace || !application || !account) return null;
356
+ return Object.freeze({
357
+ application: Object.freeze({
358
+ applicationId: application.applicationId,
359
+ applicationName: application.applicationName
360
+ }),
361
+ context: Object.freeze({
362
+ version: OAATH_WORKSPACE_ACCOUNT_CONTEXT_VERSION,
363
+ workspaceId: workspace.workspaceId,
364
+ workspaceKind: workspace.kind,
365
+ accountId: account.accountId
366
+ }),
367
+ account: account.account,
368
+ ownerValidator: account.ownerValidator,
369
+ chainIds: account.chainIds
370
+ });
371
+ }
372
+ });
373
+ }
374
+ /** Ephemeral deployment/test store; the service captures every read and write boundary. */
375
+ function createMemoryServiceDirectoryStore() {
376
+ let snapshot = null;
377
+ return {
378
+ async read() {
379
+ return structuredClone(snapshot);
380
+ },
381
+ async compareAndSwap(expectedRevision, directory) {
382
+ if ((snapshot?.revision ?? null) !== expectedRevision) return false;
383
+ snapshot = {
384
+ revision: (expectedRevision ?? 0) + 1,
385
+ directory: structuredClone(directory)
386
+ };
387
+ return true;
388
+ }
389
+ };
390
+ }
391
+ //#endregion
112
392
  //#region src/authorization/pickup.ts
113
393
  /**
114
394
  * Authenticated client pickup of a released authorization code.
@@ -155,6 +435,106 @@ async function fetchAuthorizationCode(input) {
155
435
  });
156
436
  }
157
437
  //#endregion
438
+ //#region src/authorization/verify.ts
439
+ /**
440
+ * Grant reference verification: one read-only projection of the relay's
441
+ * durable authorization evidence into the `@oaath/protocol` grant-reference
442
+ * contract.
443
+ *
444
+ * ```text
445
+ * state and owner no new state; the request, decision, and capability
446
+ * invalidation records already own every fact projected
447
+ * persisted evidence the immutable request (subject, clientId, audience,
448
+ * requestedScope), terminal decision, sealed approval
449
+ * artifact and one-shot invalidation
450
+ * resource occupied? nothing; verification is a pure read
451
+ * retry positively safe? yes: no write exists, so a replay answers the same
452
+ * stored facts and never mutates the Grant
453
+ * transitions none; this owner performs no transition
454
+ * crash/reload nothing to recover; the projected records recover
455
+ * through their own owners
456
+ * cleanup owner none
457
+ * ```
458
+ *
459
+ * Decision semantics, fail closed and in deterministic order:
460
+ *
461
+ * 1. absent grant, another client's grant, or unreadable/contradictory stored
462
+ * scope → `unknown` (absence is reported for another client's grant so the
463
+ * endpoint is not an existence oracle);
464
+ * 2. subject, client, or audience assertion mismatch → `denied` before any
465
+ * lifecycle fact is revealed;
466
+ * 3. pending, rejected, revoked, expired → `denied`;
467
+ * 4. revision other than the single approved authority revision → `denied`;
468
+ * 5. reviewed call set digest differing from the permitted call set →
469
+ * `denied`;
470
+ * 6. otherwise → `authorized` with the immutable reference evidence.
471
+ *
472
+ * The sealed permission decision owns the approved policy. The requested
473
+ * policy is its upper bound, checked through applyPermissionDecision. A missing
474
+ * or unreadable artifact never falls back to outcome=approved. This reads
475
+ * service-approved authority; Kernel capability verification and onchain
476
+ * installation remain the runtime's separate domain.
477
+ * `expiresAt`/`validUntil` speak protocol seconds; the relay clock speaks
478
+ * milliseconds and is floored into the protocol domain before comparison.
479
+ *
480
+ * @author taek <leekt216@gmail.com>
481
+ */
482
+ function unknown(code) {
483
+ return Object.freeze({
484
+ state: "unknown",
485
+ code
486
+ });
487
+ }
488
+ function denied(code) {
489
+ return Object.freeze({
490
+ state: "denied",
491
+ code
492
+ });
493
+ }
494
+ async function verifyGrantReference(input) {
495
+ let assertion;
496
+ try {
497
+ assertion = parseVerifyGrantRevisionInput(input.assertion);
498
+ } catch {
499
+ return relayFailure("relay_request_invalid", "grant verification assertion is malformed");
500
+ }
501
+ const now = relayNow(input.clock);
502
+ return withRelayTransaction(input.store, async (transaction) => {
503
+ const request = await transaction.lockAuthorizationRequest(assertion.grantId);
504
+ if (!request || request.clientId !== input.caller.clientId) return unknown("grant_unknown");
505
+ const scope = classifyStoredAuthorizationScope(request.requestedScope, request.requestId);
506
+ if (scope.kind !== "permission-request") return unknown("grant_unreadable");
507
+ if (scope.request.application.clientId !== request.clientId) return unknown("grant_unreadable");
508
+ if (assertion.subject !== request.subject) return denied("grant_subject_mismatch");
509
+ if (assertion.clientId !== request.clientId) return denied("grant_client_mismatch");
510
+ if (request.organizationAudience === null || assertion.organizationAudience !== request.organizationAudience) return denied("grant_audience_mismatch");
511
+ const decision = await transaction.lockAuthorizationDecision(assertion.grantId);
512
+ if (!decision) return denied("grant_pending");
513
+ if (decision.outcome === "rejected") return denied("grant_rejected");
514
+ if (await transaction.lockCapabilityInvalidation(assertion.grantId)) return denied("grant_revoked");
515
+ const approval = (await readApprovedPermission(transaction, input.kms, request, decision, scope.request))?.permission ?? null;
516
+ if (approval === null) return unknown("grant_unreadable");
517
+ const validUntil = approval.approvedPolicy.validUntil;
518
+ if (validUntil === null || Math.floor(now / 1e3) > validUntil) return denied("grant_expired");
519
+ if (assertion.revision !== OAATH_GRANT_REFERENCE_APPROVED_REVISION) return denied("grant_revision_mismatch");
520
+ if (assertion.requiredCallsDigest !== hashGrantPolicyCalls(approval.approvedPolicy.calls)) return denied("grant_calls_mismatch");
521
+ const ref = Object.freeze({
522
+ version: OAATH_GRANT_REFERENCE_VERSION,
523
+ grantId: request.requestId,
524
+ revision: OAATH_GRANT_REFERENCE_APPROVED_REVISION,
525
+ subject: request.subject,
526
+ clientId: request.clientId,
527
+ organizationAudience: request.organizationAudience,
528
+ state: "active",
529
+ policyDigest: hashGrantPolicy(approval.approvedPolicy)
530
+ });
531
+ return Object.freeze({
532
+ state: "authorized",
533
+ ref
534
+ });
535
+ });
536
+ }
537
+ //#endregion
158
538
  //#region src/security/authentication.ts
159
539
  /**
160
540
  * Deployment-owned client/device authentication port.
@@ -169,12 +549,20 @@ const MAX_REDIRECT_URIS = 8;
169
549
  function captureCaller(value) {
170
550
  const context = /* @__PURE__ */ new WeakSet();
171
551
  const fail = (message) => relayFailure("relay_internal", message);
172
- const record = exactRecord(value, [
552
+ const captured = captureRecord(value, "authenticated caller", context, fail);
553
+ const declaresAudience = Object.hasOwn(captured, "organizationAudience");
554
+ const record = exactCapturedRecord(captured, declaresAudience ? [
555
+ "role",
556
+ "clientId",
557
+ "subject",
558
+ "redirectUris",
559
+ "organizationAudience"
560
+ ] : [
173
561
  "role",
174
562
  "clientId",
175
563
  "subject",
176
564
  "redirectUris"
177
- ], "authenticated caller", context, fail);
565
+ ], "authenticated caller", fail);
178
566
  if (record.role !== "client" && record.role !== "owner") return relayFailure("relay_internal", "authenticated caller role is unsupported");
179
567
  const redirectUris = captureDenseArray(record.redirectUris, "caller redirectUris", context, fail);
180
568
  if (redirectUris.length > MAX_REDIRECT_URIS) return relayFailure("relay_internal", "caller redirectUris is unbounded");
@@ -182,7 +570,8 @@ function captureCaller(value) {
182
570
  role: record.role,
183
571
  clientId: canonicalIdentifier(record.clientId, "caller clientId", "relay_internal"),
184
572
  subject: canonicalIdentifier(record.subject, "caller subject", "relay_internal"),
185
- redirectUris: Object.freeze(redirectUris.map((uri) => boundedText(uri, RELAY_LIMITS.redirectUri, "caller redirectUri", "relay_internal")))
573
+ redirectUris: Object.freeze(redirectUris.map((uri) => boundedText(uri, RELAY_LIMITS.redirectUri, "caller redirectUri", "relay_internal"))),
574
+ organizationAudience: declaresAudience && record.organizationAudience !== null ? canonicalIdentifier(record.organizationAudience, "caller organizationAudience", "relay_internal") : null
186
575
  });
187
576
  }
188
577
  async function authenticateCaller(authentication, request, required) {
@@ -240,6 +629,9 @@ async function assertWithinRateLimit(limiter, input) {
240
629
  * POST /authorization/resume client fresh auth + recovery read
241
630
  * GET /bootstrap client URL-only service context
242
631
  * POST /invalidations client capability invalidation
632
+ * POST /grants/verify client grant reference verification
633
+ * POST /grants/{grantId}/revocations/{chainId} client request/recover phone custody
634
+ * GET /grants/{grantId}/revocations/{chainId} client current custody status
243
635
  * POST /chains/{chainId}/{port} client chain execution relay,
244
636
  * port in reads |
245
637
  * observation | bundler |
@@ -249,13 +641,14 @@ async function assertWithinRateLimit(limiter, input) {
249
641
  * ```
250
642
  *
251
643
  * 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
644
+ * means: no stability guarantee and no production qualification. Consent and
645
+ * decision shapes are pinned field-for-field by the strict Swift decoders in
254
646
  * `native/ios/Sources/OwnerPhone/{Projection,Decision}.swift`.
255
647
  *
256
648
  * ```text
257
649
  * GET /native/projections/{operationId} owner consent projection
258
650
  * POST /native/decisions/{operationId} owner approve or reject saga
651
+ * POST /native/revocation-decisions/{operationId} owner revocation custody decision
259
652
  * ```
260
653
  *
261
654
  * @author taek <leekt216@gmail.com>
@@ -264,14 +657,17 @@ const DEFAULT_REQUEST_TTL_MS = 3e5;
264
657
  const DEFAULT_CODE_TTL_MS = 6e4;
265
658
  const DEFAULT_MAX_BODY_BYTES = 65536;
266
659
  const MAX_TTL_MS = 864e5;
267
- /** RFC 6749 ceiling, matching `MAX_AUTHORIZATION_CODE_LIFETIME` in @oaath/protocol. */
660
+ /** The relay owns the authorization-code lifetime ceiling, in milliseconds. */
268
661
  const MAX_CODE_TTL_MS = 6e5;
269
662
  const MAX_PAYMASTER_REQUEST_TIMEOUT_MS = 3e4;
270
663
  const HASH$1 = /^0x[0-9a-f]{64}$/u;
271
664
  const INVALID = "relay_request_invalid";
272
665
  const OPTION_KEYS = [
666
+ "revocations",
667
+ "permissionApprovals",
273
668
  "store",
274
669
  "authentication",
670
+ "ownerRouting",
275
671
  "kms",
276
672
  "clock",
277
673
  "rateLimit",
@@ -333,7 +729,25 @@ function captureChainPorts(value) {
333
729
  if (port.usage !== null && typeof port.usage !== "function") return relayFailure("relay_internal", "chain port usage must be a function or null");
334
730
  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
731
  if (chains.has(port.chainId)) return relayFailure("relay_internal", "chain ports repeat a chainId");
336
- chains.set(port.chainId, port);
732
+ let gas;
733
+ const configuredGas = port.gas;
734
+ if (configuredGas !== void 0) {
735
+ const policy = exactCapturedRecord(captureRecord(configuredGas, "chain gas policy", /* @__PURE__ */ new WeakSet(), () => relayFailure("relay_internal", "chain gas policy is invalid")), ["enableVerificationGasFloor"], "chain gas policy", () => relayFailure("relay_internal", "chain gas policy is invalid"));
736
+ if (typeof policy.enableVerificationGasFloor !== "bigint" || policy.enableVerificationGasFloor < 0n || policy.enableVerificationGasFloor >= 1n << 120n) return relayFailure("relay_internal", "chain gas floor is invalid");
737
+ gas = Object.freeze({ enableVerificationGasFloor: policy.enableVerificationGasFloor });
738
+ }
739
+ chains.set(port.chainId, Object.freeze({
740
+ chainId: port.chainId,
741
+ reads: port.reads,
742
+ observation: port.observation,
743
+ bundler: port.bundler,
744
+ quote: port.quote,
745
+ submission: port.submission,
746
+ usage: port.usage,
747
+ feePayer: port.feePayer,
748
+ staticPaymasterConfigurationHash: port.staticPaymasterConfigurationHash,
749
+ ...gas === void 0 ? {} : { gas }
750
+ }));
337
751
  }
338
752
  return chains;
339
753
  }
@@ -352,6 +766,7 @@ function capturePaymasterServices(value, context, chains) {
352
766
  if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId < 1) relayFailure("relay_internal", "paymaster service chainId must be a positive integer");
353
767
  if (!chains.has(chainId)) relayFailure("relay_internal", "paymaster service chain is not configured");
354
768
  if (services.has(chainId)) relayFailure("relay_internal", "paymaster services repeat a chainId");
769
+ if (chains.get(chainId)?.staticPaymasterConfigurationHash !== null) relayFailure("relay_internal", "a chain cannot combine a paymaster service and a static paymaster");
355
770
  if (service.requestTimeoutMs === void 0) relayFailure("relay_internal", "paymaster requestTimeoutMs is required");
356
771
  services.set(chainId, Object.freeze({
357
772
  chainId,
@@ -388,39 +803,20 @@ function captureOptions(value) {
388
803
  let bootstrap = null;
389
804
  if (record.bootstrap !== void 0) {
390
805
  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
- }
806
+ bootstrap = requirePort(record.bootstrap, ["resolve"], "bootstrap");
420
807
  }
421
808
  return Object.freeze({
422
809
  store: requirePort(record.store, ["begin", "close"], "store"),
810
+ revocations: record.revocations === void 0 ? null : (() => {
811
+ const configuration = requirePort(record.revocations, ["prepare"], "revocations");
812
+ return Object.freeze({
813
+ prepare: configuration.prepare.bind(configuration),
814
+ directory: requirePort(configuration.directory, ["resolveRevocationOwner"], "revocation directory")
815
+ });
816
+ })(),
817
+ permissionApprovals: record.permissionApprovals === void 0 ? void 0 : requirePort(record.permissionApprovals, ["prepare"], "permissionApprovals"),
423
818
  authentication: requirePort(record.authentication, ["authenticate"], "authentication"),
819
+ ownerRouting: requirePort(record.ownerRouting, ["resolveOwner"], "ownerRouting"),
424
820
  kms: requirePort(record.kms, ["encrypt", "decrypt"], "kms"),
425
821
  clock: requirePort(record.clock, ["now"], "clock"),
426
822
  rateLimit,
@@ -539,11 +935,60 @@ function createRelayHandler(options) {
539
935
  const route = async (request) => {
540
936
  const segments = new URL(request.url).pathname.split("/").filter((part) => part.length > 0);
541
937
  const [head, group, third, fourth] = segments;
938
+ if (head === "grants" && third === "revocations" && segments.length === 4) {
939
+ if (request.method !== "GET" && request.method !== "POST") return relayFailure("relay_method_not_allowed", "route does not accept this method");
940
+ const caller = await authenticate(request, "client", "grants.revocation");
941
+ const grantId = canonicalIdentifier(group, "grantId", INVALID);
942
+ if (typeof fourth !== "string" || !/^[1-9][0-9]*$/u.test(fourth) || !Number.isSafeInteger(Number(fourth))) return relayFailure(INVALID, "revocation chain is invalid");
943
+ const input = {
944
+ store: captured.store,
945
+ clock: captured.clock,
946
+ caller,
947
+ grantId,
948
+ chainId: Number(fourth)
949
+ };
950
+ if (request.method === "GET") return jsonResponse(200, await fetchClientPhoneRevocation(input));
951
+ exactBody(await bodyRecord(request, captured.maxBodyBytes), []);
952
+ if (!captured.revocations) return relayFailure("relay_not_found", "revocation preparation is not configured");
953
+ const { created, ...status } = await requestOwnerPhoneRevocation({
954
+ ...input,
955
+ kms: captured.kms,
956
+ requestTtlMs: captured.requestTtlMs,
957
+ ...captured.revocations
958
+ });
959
+ return jsonResponse(created ? 201 : 200, status);
960
+ }
542
961
  if (head === "native") {
962
+ if (segments.length === 2 && group === "inbox") {
963
+ requireMethod(request, "GET");
964
+ const caller = await authenticate(request, "owner", "native.inbox");
965
+ return jsonResponse(200, await listOwnerPhoneRequests({
966
+ store: captured.store,
967
+ clock: captured.clock,
968
+ caller
969
+ }));
970
+ }
971
+ if (segments.length === 3 && group === "permission-signing") {
972
+ requireMethod(request, "GET");
973
+ const caller = await authenticate(request, "owner", "native.permissionSigning");
974
+ return jsonResponse(200, await projectOwnerPhonePermissionSigning({
975
+ store: captured.store,
976
+ clock: captured.clock,
977
+ caller,
978
+ requestId: canonicalIdentifier(third, "operationId", INVALID),
979
+ ...captured.permissionApprovals ? { permissionApprovals: captured.permissionApprovals } : {}
980
+ }));
981
+ }
543
982
  if (segments.length === 3 && group === "projections") {
544
983
  requireMethod(request, "GET");
545
984
  const caller = await authenticate(request, "owner", "native.project");
546
- const projection = await projectOwnerPhoneRequest({
985
+ const operationId = canonicalIdentifier(third, "operationId", INVALID);
986
+ const projection = operationId.startsWith("revocation-") ? await fetchOwnerPhoneRevocation({
987
+ store: captured.store,
988
+ clock: captured.clock,
989
+ caller,
990
+ operationId
991
+ }) : await projectOwnerPhoneRequest({
547
992
  store: captured.store,
548
993
  clock: captured.clock,
549
994
  caller,
@@ -551,11 +996,25 @@ function createRelayHandler(options) {
551
996
  });
552
997
  return jsonResponse(200, projection);
553
998
  }
999
+ if (segments.length === 3 && group === "revocation-decisions") {
1000
+ requireMethod(request, "POST");
1001
+ const caller = await authenticate(request, "owner", "native.revoke");
1002
+ const command = phoneDecisionCommand(await bodyRecord(request, captured.maxBodyBytes));
1003
+ return jsonResponse(200, await submitOwnerPhoneRevocationDecision({
1004
+ store: captured.store,
1005
+ clock: captured.clock,
1006
+ kms: captured.kms,
1007
+ caller,
1008
+ operationId: canonicalIdentifier(third, "operationId", INVALID),
1009
+ command
1010
+ }));
1011
+ }
554
1012
  if (segments.length === 3 && group === "decisions") {
555
1013
  requireMethod(request, "POST");
556
1014
  const caller = await authenticate(request, "owner", "native.decide");
557
1015
  const command = phoneDecisionCommand(await bodyRecord(request, captured.maxBodyBytes));
558
1016
  const decided = await submitOwnerPhoneDecision({
1017
+ ...captured.permissionApprovals ? { permissionApprovals: captured.permissionApprovals } : {},
559
1018
  store: captured.store,
560
1019
  clock: captured.clock,
561
1020
  kms: captured.kms,
@@ -570,9 +1029,59 @@ function createRelayHandler(options) {
570
1029
  }
571
1030
  if (head === "bootstrap" && segments.length === 1) {
572
1031
  requireMethod(request, "GET");
573
- await authenticate(request, "client", "bootstrap.fetch");
1032
+ const caller = await authenticate(request, "client", "bootstrap.fetch");
574
1033
  if (captured.bootstrap === null) return relayFailure("relay_not_found", "route does not exist");
575
- return jsonResponse(200, captured.bootstrap);
1034
+ const selection = await captured.bootstrap.resolve(caller);
1035
+ if (selection === null) return relayFailure("relay_not_found", "no assigned account");
1036
+ let document;
1037
+ try {
1038
+ const context = /* @__PURE__ */ new WeakSet();
1039
+ const fail = (message) => relayFailure("relay_internal", message);
1040
+ const selected = exactCapturedRecord(captureRecord(selection, "bootstrap selection", context, fail), [
1041
+ "application",
1042
+ "context",
1043
+ "account",
1044
+ "ownerValidator",
1045
+ "chainIds"
1046
+ ], "bootstrap selection", fail);
1047
+ const application = exactCapturedRecord(captureRecord(selected.application, "bootstrap application", context, fail), ["applicationId", "applicationName"], "bootstrap application", fail);
1048
+ const chainIds = captureDenseArray(selected.chainIds, "selected chains", context, fail);
1049
+ document = parseServiceBootstrap({
1050
+ version: OAATH_SERVICE_BOOTSTRAP_VERSION,
1051
+ application: {
1052
+ ...application,
1053
+ clientId: caller.clientId,
1054
+ redirectUris: caller.redirectUris
1055
+ },
1056
+ userHandle: caller.subject,
1057
+ context: selected.context,
1058
+ account: selected.account,
1059
+ ownerValidator: selected.ownerValidator,
1060
+ chains: chainIds.map((chainId) => {
1061
+ const port = captured.chains.get(chainId);
1062
+ if (port === void 0) return fail("selected chain is not configured");
1063
+ const paymasterService = captured.paymasterServices.get(port.chainId);
1064
+ return {
1065
+ chainId: port.chainId,
1066
+ ...port.gas === void 0 ? {} : { gas: { enableVerificationGasFloor: port.gas.enableVerificationGasFloor.toString() } },
1067
+ usage: port.usage !== null,
1068
+ feePayer: port.feePayer,
1069
+ paymasterService: paymasterService === void 0 ? null : { providerId: paymasterService.providerId },
1070
+ staticPaymasterConfigurationHash: port.staticPaymasterConfigurationHash
1071
+ };
1072
+ }),
1073
+ sessionSigner: captured.sessionSigner === null ? {
1074
+ mode: "frontend",
1075
+ providerId: null
1076
+ } : {
1077
+ mode: captured.sessionSigner.mode,
1078
+ providerId: captured.sessionSigner.providerId
1079
+ }
1080
+ });
1081
+ } catch {
1082
+ return relayFailure("relay_internal", "resolved bootstrap is invalid");
1083
+ }
1084
+ return jsonResponse(200, document);
576
1085
  }
577
1086
  if (head === "session-signers") {
578
1087
  const custody = captured.sessionSigner;
@@ -615,6 +1124,18 @@ function createRelayHandler(options) {
615
1124
  }
616
1125
  return relayFailure("relay_not_found", "route does not exist");
617
1126
  }
1127
+ if (head === "grants" && segments.length === 2 && group === "verify") {
1128
+ requireMethod(request, "POST");
1129
+ const caller = await authenticate(request, "client", "grants.verify");
1130
+ const result = await verifyGrantReference({
1131
+ store: captured.store,
1132
+ clock: captured.clock,
1133
+ kms: captured.kms,
1134
+ caller,
1135
+ assertion: await bodyRecord(request, captured.maxBodyBytes)
1136
+ });
1137
+ return jsonResponse(200, result);
1138
+ }
618
1139
  if (head === "invalidations" && segments.length === 1) {
619
1140
  requireMethod(request, "POST");
620
1141
  const caller = await authenticate(request, "client", "invalidations.create");
@@ -676,8 +1197,8 @@ function createRelayHandler(options) {
676
1197
  let result;
677
1198
  try {
678
1199
  result = await capability(body.request);
679
- } catch {
680
- return relayFailure("relay_chain_unavailable", "chain port did not answer");
1200
+ } catch (error) {
1201
+ return relayFailure("relay_chain_unavailable", "chain port did not answer", readValidationGasDiagnostic(error), name === "submission" ? readRpcBundlerRejection(error) : null);
681
1202
  }
682
1203
  return jsonResponse(200, result === void 0 ? {
683
1204
  present: false,
@@ -698,6 +1219,7 @@ function createRelayHandler(options) {
698
1219
  ]);
699
1220
  const created = await createAuthorizationRequest({
700
1221
  store: captured.store,
1222
+ ownerRouting: captured.ownerRouting,
701
1223
  clock: captured.clock,
702
1224
  caller,
703
1225
  redirectUri: boundedText(body.redirectUri, RELAY_LIMITS.redirectUri, "redirectUri", INVALID),
@@ -793,7 +1315,7 @@ function createRelayHandler(options) {
793
1315
  try {
794
1316
  return await route(request);
795
1317
  } catch (error) {
796
- return relayErrorResponse(relayErrorCode(error));
1318
+ return relayErrorResponse(relayErrorCode(error), readValidationGasDiagnostic(error), error instanceof OaathRelayError ? error.bundlerRejection : null);
797
1319
  }
798
1320
  };
799
1321
  }
@@ -1010,6 +1532,8 @@ function createKmsSessionSignerProvider(input) {
1010
1532
  */
1011
1533
  function emptyTables() {
1012
1534
  return {
1535
+ revocationRequests: /* @__PURE__ */ new Map(),
1536
+ revocationDecisions: /* @__PURE__ */ new Map(),
1013
1537
  requests: /* @__PURE__ */ new Map(),
1014
1538
  decisions: /* @__PURE__ */ new Map(),
1015
1539
  codes: /* @__PURE__ */ new Map(),
@@ -1019,6 +1543,8 @@ function emptyTables() {
1019
1543
  }
1020
1544
  function copyTables(tables) {
1021
1545
  return {
1546
+ revocationRequests: new Map(tables.revocationRequests),
1547
+ revocationDecisions: new Map(tables.revocationDecisions),
1022
1548
  requests: new Map(tables.requests),
1023
1549
  decisions: new Map(tables.decisions),
1024
1550
  codes: new Map(tables.codes),
@@ -1035,6 +1561,16 @@ function read(table, key, parse) {
1035
1561
  const stored = table.get(key);
1036
1562
  return stored === void 0 ? void 0 : parse(stored);
1037
1563
  }
1564
+ function artifactByRequest(tables, requestId) {
1565
+ let match;
1566
+ for (const value of tables.artifacts.values()) {
1567
+ const record = parseEncryptedArtifactRecord(value);
1568
+ if (record.requestId !== requestId) continue;
1569
+ if (match) return relayFailure("relay_record_unreadable", "request has multiple artifacts");
1570
+ match = record;
1571
+ }
1572
+ return match;
1573
+ }
1038
1574
  function createMemoryRelayStore() {
1039
1575
  let committed = emptyTables();
1040
1576
  let closed = false;
@@ -1065,6 +1601,48 @@ function createMemoryRelayStore() {
1065
1601
  release();
1066
1602
  };
1067
1603
  return {
1604
+ async listPendingOwnerRequests(ownerSubject, now, limit) {
1605
+ const tables = open();
1606
+ const requests = [];
1607
+ for (const value of tables.requests.values()) {
1608
+ const record = parseAuthorizationRequestRecord(value);
1609
+ if (record.ownerSubject === ownerSubject && record.expiresAt > now && !tables.decisions.has(record.requestId)) requests.push({
1610
+ operationId: record.requestId,
1611
+ ownerSubject,
1612
+ expiresAt: record.expiresAt
1613
+ });
1614
+ }
1615
+ for (const value of tables.revocationRequests.values()) {
1616
+ const record = parseRevocationRequestRecord(value);
1617
+ if (record.ownerSubject === ownerSubject && record.expiresAt > now && !tables.revocationDecisions.has(record.operationId)) requests.push({
1618
+ operationId: record.operationId,
1619
+ ownerSubject,
1620
+ expiresAt: record.expiresAt
1621
+ });
1622
+ }
1623
+ return requests.sort((a, b) => a.expiresAt - b.expiresAt || (a.operationId < b.operationId ? -1 : a.operationId > b.operationId ? 1 : 0)).slice(0, limit);
1624
+ },
1625
+ async lockLatestRevocationRequest(grantId, chainId) {
1626
+ let latest;
1627
+ for (const stored of open().revocationRequests.values()) {
1628
+ const record = parseRevocationRequestRecord(stored);
1629
+ if (record.signingRequest.permissionRequest.requestId !== grantId || record.signingRequest.chainId !== chainId) continue;
1630
+ if (!latest || record.createdAt > latest.createdAt) latest = record;
1631
+ }
1632
+ return latest;
1633
+ },
1634
+ async lockRevocationRequest(operationId) {
1635
+ return read(open().revocationRequests, operationId, parseRevocationRequestRecord);
1636
+ },
1637
+ async insertRevocationRequest(record) {
1638
+ return insert(open().revocationRequests, record.operationId, record);
1639
+ },
1640
+ async lockRevocationDecision(operationId) {
1641
+ return read(open().revocationDecisions, operationId, parseRevocationDecisionRecord);
1642
+ },
1643
+ async insertRevocationDecision(record) {
1644
+ return insert(open().revocationDecisions, record.operationId, record);
1645
+ },
1068
1646
  async lockAuthorizationRequest(requestId) {
1069
1647
  return read(open().requests, requestId, parseAuthorizationRequestRecord);
1070
1648
  },
@@ -1102,8 +1680,13 @@ function createMemoryRelayStore() {
1102
1680
  async lockEncryptedArtifact(artifactId) {
1103
1681
  return read(open().artifacts, artifactId, parseEncryptedArtifactRecord);
1104
1682
  },
1683
+ async lockEncryptedArtifactByRequestId(requestId) {
1684
+ return artifactByRequest(open(), requestId);
1685
+ },
1105
1686
  async insertEncryptedArtifact(record) {
1106
- return insert(open().artifacts, record.artifactId, record);
1687
+ const tables = open();
1688
+ if (artifactByRequest(tables, record.requestId)) return false;
1689
+ return insert(tables.artifacts, record.artifactId, record);
1107
1690
  },
1108
1691
  async claimEncryptedArtifact(artifactId, claimedAt) {
1109
1692
  const artifacts = open().artifacts;
@@ -1130,6 +1713,6 @@ function createMemoryRelayStore() {
1130
1713
  };
1131
1714
  }
1132
1715
  //#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 };
1716
+ export { OAATH_AUTHORIZATION_CODE_RECORD_VERSION, OAATH_AUTHORIZATION_DECISION_RECORD_VERSION, OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION, OAATH_ENCRYPTED_ARTIFACT_RECORD_VERSION, OAATH_REVOCATION_DECISION_RECORD_VERSION, OAATH_REVOCATION_REQUEST_RECORD_VERSION, OAATH_SERVICE_DIRECTORY_VERSION, OaathRelayError, REDACTED, RELAY_ERROR_STATUS, claimEncryptedArtifact, consumeAuthorizationCode, createAuthorizationRequest, createKmsSessionSignerProvider, createMemoryRelayStore, createMemoryServiceDirectoryStore, createRelayHandler, createServiceDirectory, fetchAuthorizationRequest, isCodeChallengeS256, openArtifact, redactForLog, redactUrl, resumeAuthorization, sealArtifact, submitAuthorizationDecision, verifyPkceS256 };
1134
1717
 
1135
1718
  //# sourceMappingURL=index.js.map