@oaath/server 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -0,0 +1,706 @@
1
+ import { b as relayFailure, c as canonicalIdentifier, m as timestamp, o as RELAY_LIMITS, r as OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION, s as boundedText } from "./records-BQNDxbPg.js";
2
+ import { a as relayNow, n as randomIdentifier, r as sha256Base64Url, t as isCodeChallengeS256 } from "./challenge-DmpGSogj.js";
3
+ import { n as OAATH_REVOCATION_REQUEST_RECORD_VERSION, r as REVOCATION_OPERATION_PREFIX, t as OAATH_REVOCATION_DECISION_RECORD_VERSION } from "./records-BUvqmp0D.js";
4
+ import { applyPermissionDecision, captureRecord, createGrantFromPermissionRequest, exactRecord, hashKernelRevocationSigningRequest, hashOwnerSigningRequest, hashPermissionRequest, isKernelExistingAccountProfile, parseKernelReplayableInstallOwnerSigningRequest, parseKernelRevocationSigningRequest, parseOwnerSigningArtifact, parseOwnerSigningRequest, parsePermissionDecision, parsePermissionRequest, sameOwnerCredentialProfile, serializeOwnerSigningArtifact } from "@oaath/protocol";
5
+ import { p256 } from "@noble/curves/nist.js";
6
+ import { hexToBytes } from "@noble/hashes/utils.js";
7
+ //#region src/store/interface.ts
8
+ /**
9
+ * Runs one transition inside one transaction. The body commits, or the
10
+ * transaction is rolled back and the canonical failure is preserved.
11
+ */
12
+ async function withRelayTransaction(store, run) {
13
+ const transaction = await store.begin();
14
+ let result;
15
+ try {
16
+ result = await run(transaction);
17
+ } catch (error) {
18
+ await transaction.rollback();
19
+ throw error;
20
+ }
21
+ await transaction.commit();
22
+ return result;
23
+ }
24
+ //#endregion
25
+ //#region src/artifact/encrypt.ts
26
+ /**
27
+ * KMS sealing boundary. Plaintext never reaches the store.
28
+ *
29
+ * @author taek <leekt216@gmail.com>
30
+ */
31
+ const KMS_UNAVAILABLE = "relay_kms_unavailable";
32
+ /**
33
+ * Encrypts an approval artifact and returns the opaque reference to store.
34
+ * A port that echoes plaintext is rejected: the store must never hold it.
35
+ */
36
+ async function sealArtifact(kms, plaintext) {
37
+ let sealed;
38
+ try {
39
+ sealed = await kms.encrypt(plaintext);
40
+ } catch {
41
+ return relayFailure(KMS_UNAVAILABLE, "KMS encrypt failed");
42
+ }
43
+ const ciphertextRef = boundedText(sealed, RELAY_LIMITS.ciphertextRef, "KMS ciphertext reference", KMS_UNAVAILABLE);
44
+ if (ciphertextRef.includes(plaintext)) return relayFailure(KMS_UNAVAILABLE, "KMS reference contains plaintext");
45
+ return ciphertextRef;
46
+ }
47
+ /**
48
+ * Opens retained ciphertext for an authorized internal reader. Client release
49
+ * calls this only after claim commits; authority verification releases no bytes.
50
+ */
51
+ async function openArtifact(kms, ciphertextRef) {
52
+ let opened;
53
+ try {
54
+ opened = await kms.decrypt(ciphertextRef);
55
+ } catch {
56
+ return relayFailure(KMS_UNAVAILABLE, "KMS decrypt failed");
57
+ }
58
+ return boundedText(opened, RELAY_LIMITS.artifactPlaintext, "KMS plaintext", KMS_UNAVAILABLE);
59
+ }
60
+ //#endregion
61
+ //#region src/authorization/approved-permission.ts
62
+ /** Retained service-approved policy, independent of OAuth claim or execution expiry. */
63
+ /**
64
+ * The same protocol approval meaning at admission and after durable recovery.
65
+ * Kernel capability verification belongs to the SDK; this owner checks the
66
+ * decision's request binding, policy attenuation and reported decision time.
67
+ * Evaluate at that time so a retained approval remains readable for revocation
68
+ * after its execution or OAuth claim window has closed.
69
+ */
70
+ function parseApprovedPermission(plaintext, permissionRequest, relayDecidedAt) {
71
+ const value = JSON.parse(plaintext);
72
+ const decisionValue = value !== null && typeof value === "object" && "installApproval" in value ? (({ installApproval: _install, ...permission }) => permission)(value) : value;
73
+ const permission = parsePermissionDecision(decisionValue);
74
+ if (permission.kind !== "approve" || permission.decidedAt > Math.floor(relayDecidedAt / 1e3)) throw new Error("permission artifact is not an approved decision at the relay decision time");
75
+ const applied = applyPermissionDecision({
76
+ request: permissionRequest,
77
+ grant: createGrantFromPermissionRequest(permissionRequest),
78
+ observation: {
79
+ status: "available",
80
+ decision: permission
81
+ },
82
+ evaluatedAt: permission.decidedAt
83
+ });
84
+ if (applied.status !== "applied" || applied.grant.state !== "approved") throw new Error("permission artifact does not approve this request");
85
+ return {
86
+ permission,
87
+ plaintext
88
+ };
89
+ }
90
+ /** Kernel capability verification remains the SDK preparation owner's responsibility. */
91
+ async function readApprovedPermission(transaction, kms, request, decision, permissionRequest) {
92
+ try {
93
+ const artifact = await transaction.lockEncryptedArtifactByRequestId(request.requestId);
94
+ if (!artifact || artifact.requestId !== request.requestId || artifact.clientId !== request.clientId || artifact.createdAt !== decision.decidedAt) return null;
95
+ return parseApprovedPermission(await openArtifact(kms, artifact.ciphertextRef), permissionRequest, decision.decidedAt);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ //#endregion
101
+ //#region src/authorization/owner-signing.ts
102
+ /**
103
+ * Pure authorization of one returned Kernel owner-signing artifact.
104
+ *
105
+ * The protocol owns both exact wire shapes. This server owner adds only the
106
+ * release policy: the request must use the first-runtime P-256 credential, the
107
+ * artifact must be its one canonical JSON representation, its request hash must
108
+ * bind the exact request, and its compact low-S signature must verify the exact
109
+ * Kernel digest. It allocates and persists nothing.
110
+ *
111
+ * @author taek <leekt216@gmail.com>
112
+ */
113
+ const INVALID = "relay_request_invalid";
114
+ /**
115
+ * Returns the canonical artifact plaintext only after every request, key,
116
+ * request-hash, and signature binding agrees.
117
+ */
118
+ function verifyKernelV4ReplayableInstallOwnerSigningArtifact(requestValue, artifactPlaintext) {
119
+ try {
120
+ const request = parseKernelReplayableInstallOwnerSigningRequest(requestValue);
121
+ if (request.signer.ownerCredential.kind !== "p256") throw new TypeError();
122
+ return verifyArtifact(hashOwnerSigningRequest(request), request.expectedDigest, request.signer.ownerCredential.publicKey, artifactPlaintext);
123
+ } catch {
124
+ return relayFailure(INVALID, "Kernel owner signing artifact is invalid");
125
+ }
126
+ }
127
+ /** Stored signing request has already crossed the durable record boundary. */
128
+ function verifyKernelV4RevocationOwnerSigningArtifact(request, artifactPlaintext) {
129
+ try {
130
+ if (request.install.signer.ownerCredential.kind !== "p256") throw new TypeError();
131
+ return verifyArtifact(hashKernelRevocationSigningRequest(request), request.expectedDigest, request.install.signer.ownerCredential.publicKey, artifactPlaintext);
132
+ } catch {
133
+ return relayFailure(INVALID, "Kernel owner signing artifact is invalid");
134
+ }
135
+ }
136
+ function verifyArtifact(requestHash, digest, publicKey, artifactPlaintext) {
137
+ const received = boundedText(artifactPlaintext, RELAY_LIMITS.artifactPlaintext, "owner signing artifact", INVALID);
138
+ const artifact = parseOwnerSigningArtifact(JSON.parse(received));
139
+ const canonical = serializeOwnerSigningArtifact(artifact);
140
+ if (received !== canonical) throw new TypeError();
141
+ if (artifact.requestHash !== requestHash) throw new TypeError();
142
+ if (!p256.verify(hexToBytes(artifact.signature.slice(2)), hexToBytes(digest.slice(2)), hexToBytes(publicKey.slice(2)), {
143
+ format: "compact",
144
+ lowS: true,
145
+ prehash: false
146
+ })) throw new TypeError();
147
+ return canonical;
148
+ }
149
+ //#endregion
150
+ //#region src/native/display.ts
151
+ /** Bounded base64url match code length. 48 bits is plenty to compare by eye. */
152
+ const NATIVE_DISPLAY_PAYLOAD_LENGTH = 8;
153
+ const NATIVE_DISPLAY_DOMAIN = "oaath.native-display/v1:";
154
+ /** One non-secret comparison code for the requesting app, phone, inbox, and push. */
155
+ async function ownerPhoneDisplayPayload(ownerSubject, operationId) {
156
+ return (await sha256Base64Url(`${NATIVE_DISPLAY_DOMAIN}${ownerSubject}:${operationId}`)).slice(0, 8);
157
+ }
158
+ //#endregion
159
+ //#region src/authorization/request.ts
160
+ /**
161
+ * Create and read an authorization request.
162
+ *
163
+ * The request record snapshots the resolved approving device separately from
164
+ * the requesting member. It is immutable once created; reads and decisions never
165
+ * re-resolve the owner. A refused route creates no row. After a crash, the durable
166
+ * route remains authoritative. Creation does not automatically retry writes.
167
+ * Its decision is a separate terminal record, so "was this decided?" has one owner.
168
+ *
169
+ * @author taek <leekt216@gmail.com>
170
+ */
171
+ async function createAuthorizationRequest(input) {
172
+ if (!isCodeChallengeS256(input.codeChallenge)) return relayFailure("relay_request_invalid", "codeChallenge must be an S256 challenge");
173
+ if (!input.caller.redirectUris.includes(input.redirectUri)) return relayFailure("relay_forbidden", "redirectUri is not registered for this client");
174
+ const requestId = randomIdentifier();
175
+ const resolved = await input.ownerRouting.resolveOwner(input.caller, Object.freeze({
176
+ requestId,
177
+ requestedScope: input.requestedScope
178
+ }));
179
+ if (resolved === null) return relayFailure("relay_forbidden", "no approving device is assigned");
180
+ const owner = exactRecord(resolved, ["ownerDeviceId", "ownerSubject"], "owner route", /* @__PURE__ */ new WeakSet(), (message) => relayFailure("relay_internal", message));
181
+ const ownerDeviceId = canonicalIdentifier(owner.ownerDeviceId, "ownerDeviceId", "relay_internal");
182
+ const ownerSubject = canonicalIdentifier(owner.ownerSubject, "ownerSubject", "relay_internal");
183
+ const createdAt = relayNow(input.clock);
184
+ const record = Object.freeze({
185
+ version: OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION,
186
+ requestId,
187
+ clientId: input.caller.clientId,
188
+ subject: input.caller.subject,
189
+ ownerDeviceId,
190
+ ownerSubject,
191
+ organizationAudience: input.caller.organizationAudience,
192
+ redirectUri: input.redirectUri,
193
+ codeChallenge: input.codeChallenge,
194
+ requestedScope: input.requestedScope,
195
+ createdAt,
196
+ expiresAt: createdAt + input.requestTtlMs
197
+ });
198
+ const matchCode = await ownerPhoneDisplayPayload(ownerSubject, requestId);
199
+ await withRelayTransaction(input.store, async (transaction) => {
200
+ if (!await transaction.insertAuthorizationRequest(record)) return relayFailure("relay_internal", "authorization request identifier is not unique");
201
+ });
202
+ return Object.freeze({
203
+ requestId: record.requestId,
204
+ expiresAt: record.expiresAt,
205
+ matchCode
206
+ });
207
+ }
208
+ /**
209
+ * Row-locked read of the request and its decision. Read paths take the same
210
+ * exclusive lock as transitions.
211
+ *
212
+ * ponytail: one lock class for reads and writes; add a shared read mode only if
213
+ * approval-screen traffic measurably contends.
214
+ */
215
+ async function readAuthorizationState(transaction, requestId, now, match) {
216
+ const request = await transaction.lockAuthorizationRequest(requestId);
217
+ if (!request || !match(request)) return relayFailure("relay_not_found", "authorization request does not exist");
218
+ const decision = await transaction.lockAuthorizationDecision(requestId);
219
+ return Object.freeze({
220
+ requestId: request.requestId,
221
+ clientId: request.clientId,
222
+ redirectUri: request.redirectUri,
223
+ requestedScope: request.requestedScope,
224
+ expiresAt: request.expiresAt,
225
+ expired: now >= request.expiresAt,
226
+ decision: decision ? Object.freeze({
227
+ outcome: decision.outcome,
228
+ decidedAt: decision.decidedAt
229
+ }) : null
230
+ });
231
+ }
232
+ /** Read for the approving owner. */
233
+ function fetchAuthorizationRequest(input) {
234
+ const now = relayNow(input.clock);
235
+ return withRelayTransaction(input.store, (transaction) => readAuthorizationState(transaction, input.requestId, now, (request) => request.ownerSubject === input.caller.subject));
236
+ }
237
+ //#endregion
238
+ //#region src/authorization/scope.ts
239
+ /**
240
+ * Authoritative classification of one immutable stored authorization scope.
241
+ *
242
+ * The current server may approve an exact protocol PermissionRequest or the
243
+ * one exact Kernel replayable-install request backed by a P-256 owner. Other
244
+ * closed owner-signing requests are captured for projection and hashing but
245
+ * remain reject-only. Unknown, malformed, and legacy scopes stay readable and
246
+ * rejectable, but never authorize artifact creation.
247
+ *
248
+ * @author taek <leekt216@gmail.com>
249
+ */
250
+ function classifyStoredAuthorizationScope(requestedScope, requestId) {
251
+ try {
252
+ const parsed = JSON.parse(requestedScope);
253
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return Object.freeze({
254
+ kind: "unverified",
255
+ decision: "reject-only"
256
+ });
257
+ try {
258
+ return Object.freeze({
259
+ kind: "permission-request",
260
+ decision: "approve-or-reject",
261
+ request: parsePermissionRequest({
262
+ ...parsed,
263
+ requestId
264
+ })
265
+ });
266
+ } catch {
267
+ const request = parseOwnerSigningRequest(parsed);
268
+ try {
269
+ const kernelRequest = parseKernelReplayableInstallOwnerSigningRequest(request);
270
+ if (kernelRequest.signer.ownerCredential.kind === "p256") return Object.freeze({
271
+ kind: "kernel-owner-signing-request",
272
+ decision: "approve-or-reject",
273
+ request: kernelRequest
274
+ });
275
+ } catch {}
276
+ return Object.freeze({
277
+ kind: "owner-signing-request",
278
+ decision: "reject-only",
279
+ request
280
+ });
281
+ }
282
+ } catch {
283
+ return Object.freeze({
284
+ kind: "unverified",
285
+ decision: "reject-only"
286
+ });
287
+ }
288
+ }
289
+ //#endregion
290
+ //#region src/native/permission-approval.ts
291
+ /** Canonical permission preparation supplied by the deployment's Kernel SDK integration. */
292
+ async function prepareOwnerPhonePermissionApproval(provider, request) {
293
+ if (!provider) return relayFailure("relay_request_invalid", "phone permission signing is not configured");
294
+ const invalid = () => relayFailure("relay_request_invalid", "phone permission preparation is invalid");
295
+ const result = captureRecord(await provider.prepare(request), "phone permission preparation", /* @__PURE__ */ new WeakSet(), invalid);
296
+ const signingRequest = parseKernelReplayableInstallOwnerSigningRequest(result.signingRequest);
297
+ if (signingRequest.signer.ownerCredential.kind !== "p256" || !sameOwnerCredentialProfile(signingRequest.signer.ownerCredential, request.logicalAccount.ownerCredential) || typeof result.complete !== "function") return invalid();
298
+ const complete = result.complete;
299
+ const requestHash = hashOwnerSigningRequest(signingRequest);
300
+ return Object.freeze({
301
+ signingRequest,
302
+ async complete(artifact, decidedAt) {
303
+ if (artifact.requestHash !== requestHash) return invalid();
304
+ return boundedText(await complete(artifact, decidedAt), RELAY_LIMITS.artifactPlaintext, "permission artifact", "relay_request_invalid");
305
+ }
306
+ });
307
+ }
308
+ //#endregion
309
+ //#region src/native/projection.ts
310
+ /**
311
+ * EXPERIMENTAL PREVIEW — authenticated owner-phone consent projection.
312
+ *
313
+ * This is a preview surface reached through `@oaath/server/native` and the
314
+ * relay's preview HTTP routes (`GET /native/projections/{operationId}`). It has
315
+ * no stability guarantee and no production qualification: real iOS, Apple
316
+ * provisioning, Secure Enclave, and hosted operations are later work.
317
+ *
318
+ * What it owns: turning one stored authorization request into the projection
319
+ * the owner's phone renders before deciding. It owns nothing about deciding;
320
+ * the relay's decision use case stays the only decision owner.
321
+ *
322
+ * Two very different surfaces share this file, and only one of them is opaque:
323
+ *
324
+ * - The **push** projection (`operationId`, `displayPayload`, `expiresAt`)
325
+ * transits Apple inside an APNs payload, so it stays opaque by design: no
326
+ * permission, scope, client, chain, or account detail may ride it. The APNs
327
+ * sender enforces that subset exactly.
328
+ * - The **consent** projection returned here travels only the authenticated
329
+ * relay → owner channel, so it deliberately carries the full authorization
330
+ * request: the requesting client identity and the requested scope. The owner
331
+ * must see exactly the authority they grant before approving.
332
+ *
333
+ * `displayPayload` is not a secret and not authority: it is a deterministic
334
+ * digest of the request and its bound subject, so the owner can check that the
335
+ * code on the phone is the code the browser shows. Holding it authorizes
336
+ * nothing — deciding still requires the authenticated bound owner.
337
+ *
338
+ * @author taek <leekt216@gmail.com>
339
+ */
340
+ /** Versioned consent envelope; the Swift decoder pins this exact value. */
341
+ const OAATH_NATIVE_PROJECTION_VERSION = "oaath.native-projection/v7";
342
+ /**
343
+ * The stored scope is the requesting client's own JSON permission scope (the
344
+ * SDK serializes a permission request without its `requestId`; the relay's
345
+ * request id is the binding). An unparseable scope is not a failure: the owner
346
+ * still gets the raw text to review, never a silent omission.
347
+ */
348
+ function projectCredential(credential) {
349
+ if (credential.kind === "ecdsa") return Object.freeze({
350
+ kind: "ecdsa",
351
+ address: credential.address ?? ""
352
+ });
353
+ if (credential.kind === "p256") return Object.freeze({
354
+ kind: "p256",
355
+ publicKey: credential.publicKey ?? ""
356
+ });
357
+ return Object.freeze({
358
+ kind: "webauthn",
359
+ publicKey: credential.publicKey ?? "",
360
+ authenticatorIdHash: credential.authenticatorIdHash ?? ""
361
+ });
362
+ }
363
+ /** One owner for permission display facts in both grant and removal consent. */
364
+ async function projectPermissionConsent(request) {
365
+ if (isKernelExistingAccountProfile(request.logicalAccount)) return relayFailure("relay_request_invalid", "phone consent requires a Kernel v4 account");
366
+ return Object.freeze({
367
+ kind: "permission-request",
368
+ decision: "approve-or-reject",
369
+ context: request.context,
370
+ application: Object.freeze({
371
+ applicationId: request.application.applicationId,
372
+ clientId: request.application.clientId,
373
+ origin: request.application.origin,
374
+ deviceFingerprint: (await sha256Base64Url(`${NATIVE_DISPLAY_DOMAIN}device:${request.application.deviceId}`)).slice(0, 8)
375
+ }),
376
+ account: Object.freeze({
377
+ accountIndex: request.logicalAccount.accountIndex,
378
+ kernelVersion: request.logicalAccount.kernelVersion,
379
+ factoryRoute: request.logicalAccount.factoryRoute,
380
+ entryPointVersion: request.logicalAccount.entryPoint.version,
381
+ ownerCredential: projectCredential(request.logicalAccount.ownerCredential)
382
+ }),
383
+ operatorCredential: projectCredential(request.operatorCredential),
384
+ sessionSigner: request.sessionSigner === null ? null : Object.freeze({
385
+ mode: request.sessionSigner.mode,
386
+ providerId: request.sessionSigner.providerId
387
+ }),
388
+ chainScope: request.chainScope,
389
+ calls: Object.freeze(request.policy.calls.map((call) => Object.freeze({
390
+ target: call.target,
391
+ selector: call.selector,
392
+ valueLimit: call.valueLimit,
393
+ argumentEquals: Object.freeze(call.argumentEquals.map((rule) => Object.freeze({
394
+ index: rule.index,
395
+ value: rule.value
396
+ })))
397
+ }))),
398
+ requestedAt: request.requestedAt,
399
+ expiresAt: request.expiresAt,
400
+ policyValidAfter: request.policy.validAfter,
401
+ policyValidUntil: request.policy.validUntil,
402
+ perChainOperationLimit: request.policy.perChainOperationLimit.count,
403
+ perChainOperationIntervalSeconds: request.policy.perChainOperationLimit.intervalSeconds
404
+ });
405
+ }
406
+ async function projectOwnerPhoneScope(requestedScope, operationId) {
407
+ try {
408
+ const classified = classifyStoredAuthorizationScope(requestedScope, operationId);
409
+ if (classified.kind === "permission-request") return projectPermissionConsent(classified.request);
410
+ if (classified.kind === "kernel-owner-signing-request") return Object.freeze({
411
+ kind: "owner-signing-request",
412
+ decision: "approve-or-reject",
413
+ requestHash: hashOwnerSigningRequest(classified.request),
414
+ request: classified.request
415
+ });
416
+ if (classified.kind === "owner-signing-request") return Object.freeze({
417
+ kind: "owner-signing-request",
418
+ decision: "reject-only",
419
+ requestHash: hashOwnerSigningRequest(classified.request),
420
+ request: classified.request
421
+ });
422
+ return Object.freeze({
423
+ kind: "raw",
424
+ decision: "reject-only",
425
+ text: requestedScope
426
+ });
427
+ } catch {
428
+ return Object.freeze({
429
+ kind: "raw",
430
+ decision: "reject-only",
431
+ text: requestedScope
432
+ });
433
+ }
434
+ }
435
+ /**
436
+ * Projects a pending request for its bound owner. An expired or already decided
437
+ * request is not projectable: there is nothing left to approve, so nothing is
438
+ * ever pushed or rendered for it.
439
+ */
440
+ async function pendingOwnerRequest(input) {
441
+ if (input.caller.role !== "owner") return relayFailure("relay_forbidden", "caller may not act in the required role");
442
+ const state = await fetchAuthorizationRequest({
443
+ store: input.store,
444
+ clock: input.clock,
445
+ caller: input.caller,
446
+ requestId: input.requestId
447
+ });
448
+ if (state.expired) return relayFailure("relay_expired", "authorization request expired");
449
+ if (state.decision !== null) return relayFailure("relay_already_decided", "authorization request is already decided");
450
+ return state;
451
+ }
452
+ async function projectionFromState(state, caller, scope) {
453
+ return Object.freeze({
454
+ version: OAATH_NATIVE_PROJECTION_VERSION,
455
+ operationId: state.requestId,
456
+ displayPayload: await ownerPhoneDisplayPayload(caller.subject, state.requestId),
457
+ expiresAt: state.expiresAt,
458
+ client: Object.freeze({
459
+ clientId: state.clientId,
460
+ redirectUri: state.redirectUri
461
+ }),
462
+ scope
463
+ });
464
+ }
465
+ async function projectOwnerPhoneRequest(input) {
466
+ const state = await pendingOwnerRequest(input);
467
+ return projectionFromState(state, input.caller, await projectOwnerPhoneScope(state.requestedScope, state.requestId));
468
+ }
469
+ /** The signing packet for the same consent, using the existing projection wire shape. */
470
+ async function projectOwnerPhonePermissionSigning(input) {
471
+ const state = await pendingOwnerRequest(input);
472
+ const scope = classifyStoredAuthorizationScope(state.requestedScope, state.requestId);
473
+ if (scope.kind !== "permission-request") return relayFailure("relay_request_invalid", "request is not a permission");
474
+ const prepared = await prepareOwnerPhonePermissionApproval(input.permissionApprovals, scope.request);
475
+ return projectionFromState(state, input.caller, Object.freeze({
476
+ kind: "owner-signing-request",
477
+ decision: "approve-or-reject",
478
+ requestHash: hashOwnerSigningRequest(prepared.signingRequest),
479
+ request: prepared.signingRequest
480
+ }));
481
+ }
482
+ //#endregion
483
+ //#region src/native/revocation.ts
484
+ /** Consent projection for an immutable owner revocation request; no admission or decision state. */
485
+ /**
486
+ * Projects a stored request for its authenticated owner. The calling queue owns
487
+ * admission, current ownership, expiry and terminal-decision checks; this helper
488
+ * owns only consent bytes. It performs no signing, submission or observation.
489
+ */
490
+ async function projectOwnerPhoneRevocation(input) {
491
+ if (!/^[A-Za-z0-9._~-]{1,64}$/u.test(input.operationId) || !input.ownerSubject || !Number.isSafeInteger(input.expiresAt) || input.expiresAt < 0) return relayFailure("relay_request_invalid", "revocation projection metadata is invalid");
492
+ const request = parseKernelRevocationSigningRequest(input.request);
493
+ return Object.freeze({
494
+ version: OAATH_NATIVE_PROJECTION_VERSION,
495
+ operationId: input.operationId,
496
+ displayPayload: await ownerPhoneDisplayPayload(input.ownerSubject, input.operationId),
497
+ expiresAt: input.expiresAt,
498
+ client: Object.freeze({
499
+ clientId: request.permissionRequest.application.clientId,
500
+ redirectUri: null
501
+ }),
502
+ scope: Object.freeze({
503
+ kind: "kernel-revocation",
504
+ decision: "approve-or-reject",
505
+ requestHash: hashKernelRevocationSigningRequest(request),
506
+ permission: await projectPermissionConsent(request.permissionRequest),
507
+ install: Object.freeze({
508
+ kind: "owner-signing-request",
509
+ decision: "approve-or-reject",
510
+ requestHash: hashOwnerSigningRequest(request.install),
511
+ request: request.install
512
+ }),
513
+ effect: request.effect,
514
+ chainId: request.chainId,
515
+ entryPoint: request.entryPoint,
516
+ operation: request.operation,
517
+ expectedDigest: request.expectedDigest
518
+ })
519
+ });
520
+ }
521
+ //#endregion
522
+ //#region src/revocation/service.ts
523
+ /**
524
+ * Owner revocation custody: absent -> pending -> approved | rejected once.
525
+ * The immutable request and terminal decision are the only persisted evidence.
526
+ * Approval occupies this operation ID; it proves custody, never submission or
527
+ * onchain completion. No execution lane is acquired here. A decision retry reads
528
+ * the stored outcome before expiry or artifact handling. An ambiguous enqueue
529
+ * response is recovered by looking up the original grant/chain; it never
530
+ * authorizes a blind insertion. Only expired undecided custody may be replaced.
531
+ * Enqueue recovery needs only the store; signing custody needs KMS. There
532
+ * is no in-memory preparation to recover. Transactions own rollback and release.
533
+ */
534
+ async function readClientState(transaction, input) {
535
+ if (input.caller.role !== "client") return relayFailure("relay_forbidden", "only the originating client may request revocation");
536
+ const source = await transaction.lockAuthorizationRequest(input.grantId);
537
+ if (!source || source.clientId !== input.caller.clientId || source.subject !== input.caller.subject) return relayFailure("relay_forbidden", "permission is not owned by this caller");
538
+ const request = await transaction.lockLatestRevocationRequest(input.grantId, input.chainId);
539
+ return {
540
+ source,
541
+ current: request ? {
542
+ request,
543
+ decision: await readDecision(transaction, request)
544
+ } : null
545
+ };
546
+ }
547
+ function statusOf(state, clock) {
548
+ return Object.freeze({
549
+ grantId: state.request.signingRequest.permissionRequest.requestId,
550
+ chainId: state.request.signingRequest.chainId,
551
+ operationId: state.request.operationId,
552
+ expiresAt: state.request.expiresAt,
553
+ status: state.decision?.outcome ?? (relayNow(clock) > state.request.expiresAt ? "expired" : "pending")
554
+ });
555
+ }
556
+ function reusable(state, clock) {
557
+ return state !== null && (state.decision !== void 0 || relayNow(clock) <= state.request.expiresAt);
558
+ }
559
+ /** Authenticated metadata lookup. It never prepares, opens KMS custody or submits. */
560
+ async function fetchClientPhoneRevocation(input) {
561
+ return withRelayTransaction(input.store, async (transaction) => {
562
+ const { current } = await readClientState(transaction, input);
563
+ if (!current) return relayFailure("relay_not_found", "revocation request is absent");
564
+ return statusOf(current, input.clock);
565
+ });
566
+ }
567
+ /**
568
+ * Returns the current grant/chain request before touching preparation or KMS.
569
+ * Only positively expired, undecided custody may be replaced. Approved custody
570
+ * remains immutable even after expiry: operation evidence owns any replacement.
571
+ * Pure preparation runs outside the transaction; final admission locks the
572
+ * original grant again so concurrent callers share the winner's exact request.
573
+ */
574
+ async function requestOwnerPhoneRevocation(input) {
575
+ const { store, clock, kms, directory, caller, grantId, chainId, requestTtlMs, prepare } = input;
576
+ if (!Number.isSafeInteger(chainId) || chainId < 1 || !Number.isSafeInteger(requestTtlMs) || requestTtlMs < 1) return relayFailure("relay_request_invalid", "revocation chain or TTL is invalid");
577
+ const source = await withRelayTransaction(store, async (transaction) => {
578
+ const { source: request, current } = await readClientState(transaction, input);
579
+ if (current && reusable(current, clock)) return { retained: statusOf(current, clock) };
580
+ const scope = classifyStoredAuthorizationScope(request.requestedScope, request.requestId);
581
+ const decision = await transaction.lockAuthorizationDecision(grantId);
582
+ if (scope.kind !== "permission-request" || decision?.outcome !== "approved") return relayFailure("relay_request_invalid", "permission is not approved");
583
+ const owner = await directory.resolveRevocationOwner(caller, {
584
+ requestId: grantId,
585
+ requestedScope: request.requestedScope,
586
+ chainId
587
+ });
588
+ if (!owner) return relayFailure("relay_forbidden", "account or chain is not assigned");
589
+ const retained = await readApprovedPermission(transaction, kms, request, decision, scope.request);
590
+ if (!retained) return relayFailure("relay_record_unreadable", "approved permission is unreadable");
591
+ return {
592
+ request: scope.request,
593
+ artifact: retained.plaintext,
594
+ owner
595
+ };
596
+ });
597
+ if ("retained" in source) return Object.freeze({
598
+ ...source.retained,
599
+ created: false
600
+ });
601
+ let signingRequest;
602
+ try {
603
+ signingRequest = parseKernelRevocationSigningRequest(await prepare(Object.freeze({
604
+ request: source.request,
605
+ artifact: source.artifact,
606
+ chainId
607
+ })));
608
+ } catch {
609
+ return relayFailure("relay_request_invalid", "revocation preparation failed");
610
+ }
611
+ if (signingRequest.chainId !== chainId || hashPermissionRequest(signingRequest.permissionRequest) !== hashPermissionRequest(source.request)) return relayFailure("relay_request_invalid", "prepared revocation changed its permission or chain");
612
+ return withRelayTransaction(store, async (transaction) => {
613
+ const { current } = await readClientState(transaction, input);
614
+ if (current && reusable(current, clock)) return Object.freeze({
615
+ ...statusOf(current, clock),
616
+ created: false
617
+ });
618
+ const createdAt = relayNow(clock);
619
+ const expiresAt = timestamp(createdAt + requestTtlMs, "revocation expiry", "relay_request_invalid");
620
+ const record = Object.freeze({
621
+ version: OAATH_REVOCATION_REQUEST_RECORD_VERSION,
622
+ operationId: REVOCATION_OPERATION_PREFIX + randomIdentifier(),
623
+ ...source.owner,
624
+ createdAt,
625
+ expiresAt,
626
+ signingRequest
627
+ });
628
+ if (!await transaction.insertRevocationRequest(record)) return relayFailure("relay_state_ambiguous", "revocation request insertion did not settle");
629
+ return Object.freeze({
630
+ ...statusOf({
631
+ request: record,
632
+ decision: void 0
633
+ }, clock),
634
+ created: true
635
+ });
636
+ });
637
+ }
638
+ async function readOwnerState(transaction, input) {
639
+ if (input.caller.role !== "owner") return relayFailure("relay_forbidden", "caller may not decide");
640
+ const state = await readRevocationState(transaction, input.operationId);
641
+ if (state.request.ownerSubject !== input.caller.subject) return relayFailure("relay_forbidden", "revocation belongs to another owner");
642
+ return state;
643
+ }
644
+ /** Shared durable request/decision binding; caller admission belongs to each use case. */
645
+ async function readRevocationState(transaction, operationId) {
646
+ const request = await transaction.lockRevocationRequest(operationId);
647
+ if (!request) return relayFailure("relay_not_found", "revocation request is absent");
648
+ return {
649
+ request,
650
+ decision: await readDecision(transaction, request)
651
+ };
652
+ }
653
+ async function readDecision(transaction, request) {
654
+ const decision = await transaction.lockRevocationDecision(request.operationId);
655
+ if (decision && (decision.decidedAt < request.createdAt || decision.decidedAt > request.expiresAt)) return relayFailure("relay_record_unreadable", "revocation decision time contradicts its request");
656
+ return decision;
657
+ }
658
+ function requirePending(request, clock) {
659
+ if (relayNow(clock) > request.expiresAt) return relayFailure("relay_expired", "revocation request expired");
660
+ }
661
+ async function fetchOwnerPhoneRevocation(input) {
662
+ const { request, decision } = await withRelayTransaction(input.store, (transaction) => readOwnerState(transaction, input));
663
+ if (decision) return relayFailure("relay_already_decided", "revocation is already decided");
664
+ requirePending(request, input.clock);
665
+ return projectOwnerPhoneRevocation({
666
+ operationId: request.operationId,
667
+ ownerSubject: request.ownerSubject,
668
+ expiresAt: request.expiresAt,
669
+ request: request.signingRequest
670
+ });
671
+ }
672
+ function answer(decision, settlement) {
673
+ return Object.freeze({
674
+ version: "oaath.native-revocation-decision/v1",
675
+ operationId: decision.operationId,
676
+ outcome: decision.outcome,
677
+ decidedAt: decision.decidedAt,
678
+ settlement
679
+ });
680
+ }
681
+ async function submitOwnerPhoneRevocationDecision(input) {
682
+ const initial = await withRelayTransaction(input.store, (transaction) => readOwnerState(transaction, input));
683
+ if (initial.decision) return answer(initial.decision, "replayed");
684
+ requirePending(initial.request, input.clock);
685
+ const artifactRef = input.command.outcome === "approved" ? await sealArtifact(input.kms, verifyKernelV4RevocationOwnerSigningArtifact(initial.request.signingRequest, input.command.artifact)) : null;
686
+ return withRelayTransaction(input.store, async (transaction) => {
687
+ const { request, decision } = await readOwnerState(transaction, input);
688
+ if (decision) return answer(decision, "replayed");
689
+ requirePending(request, input.clock);
690
+ const decidedAt = relayNow(input.clock);
691
+ if (decidedAt < request.createdAt || decidedAt > request.expiresAt) return relayFailure("relay_expired", "decision is outside the request window");
692
+ const record = Object.freeze({
693
+ version: OAATH_REVOCATION_DECISION_RECORD_VERSION,
694
+ operationId: request.operationId,
695
+ outcome: input.command.outcome,
696
+ decidedAt,
697
+ artifactRef
698
+ });
699
+ if (!await transaction.insertRevocationDecision(record)) return relayFailure("relay_state_ambiguous", "revocation decision insertion did not settle");
700
+ return answer(record, "decided");
701
+ });
702
+ }
703
+ //#endregion
704
+ export { withRelayTransaction as S, verifyKernelV4ReplayableInstallOwnerSigningArtifact as _, submitOwnerPhoneRevocationDecision as a, openArtifact as b, projectOwnerPhonePermissionSigning as c, classifyStoredAuthorizationScope as d, createAuthorizationRequest as f, ownerPhoneDisplayPayload as g, NATIVE_DISPLAY_PAYLOAD_LENGTH as h, requestOwnerPhoneRevocation as i, projectOwnerPhoneRequest as l, readAuthorizationState as m, fetchOwnerPhoneRevocation as n, projectOwnerPhoneRevocation as o, fetchAuthorizationRequest as p, readRevocationState as r, OAATH_NATIVE_PROJECTION_VERSION as s, fetchClientPhoneRevocation as t, prepareOwnerPhonePermissionApproval as u, parseApprovedPermission as v, sealArtifact as x, readApprovedPermission as y };
705
+
706
+ //# sourceMappingURL=service-Cf4f-9KP.js.map