@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.
- package/README.md +239 -5
- package/dist/apns.d.ts +2 -2
- package/dist/apns.js +2 -2
- package/dist/{challenge-AGTmUfj2.js → challenge-DmpGSogj.js} +2 -2
- package/dist/{challenge-AGTmUfj2.js.map → challenge-DmpGSogj.js.map} +1 -1
- package/dist/inbox-B80EUVht.js +249 -0
- package/dist/inbox-B80EUVht.js.map +1 -0
- package/dist/index.d.ts +9 -255
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +631 -48
- package/dist/index.js.map +1 -1
- package/dist/interface-DHpwP0wr.d.ts +245 -0
- package/dist/interface-DHpwP0wr.d.ts.map +1 -0
- package/dist/kernel.d.ts +42 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +80 -0
- package/dist/kernel.js.map +1 -0
- package/dist/kms-BWkywdJM.d.ts +18 -0
- package/dist/kms-BWkywdJM.d.ts.map +1 -0
- package/dist/native.d.ts +3 -30
- package/dist/native.js +115 -2
- package/dist/native.js.map +1 -0
- package/dist/postgres.d.ts +38 -3
- package/dist/postgres.d.ts.map +1 -1
- package/dist/postgres.js +301 -14
- package/dist/postgres.js.map +1 -1
- package/dist/projection-B7IB9irs.d.ts +228 -0
- package/dist/projection-B7IB9irs.d.ts.map +1 -0
- package/dist/{records-Bgr-9PUh.js → records-BQNDxbPg.js} +32 -8
- package/dist/records-BQNDxbPg.js.map +1 -0
- package/dist/records-BUvqmp0D.js +70 -0
- package/dist/records-BUvqmp0D.js.map +1 -0
- package/dist/service-Cf4f-9KP.js +706 -0
- package/dist/service-Cf4f-9KP.js.map +1 -0
- package/dist/service-J51bOVe6.d.ts +483 -0
- package/dist/service-J51bOVe6.d.ts.map +1 -0
- package/package.json +39 -8
- package/LICENSE +0 -201
- package/dist/authentication-HUYWk5Y2.d.ts +0 -46
- package/dist/authentication-HUYWk5Y2.d.ts.map +0 -1
- package/dist/decision-dEe9n108.d.ts +0 -53
- package/dist/decision-dEe9n108.d.ts.map +0 -1
- package/dist/interface-WwvwWs8e.d.ts +0 -114
- package/dist/interface-WwvwWs8e.d.ts.map +0 -1
- package/dist/native.d.ts.map +0 -1
- package/dist/projection-dGIkE0rI.js +0 -516
- package/dist/projection-dGIkE0rI.js.map +0 -1
- package/dist/projection-g6k_CCUK.d.ts +0 -136
- package/dist/projection-g6k_CCUK.d.ts.map +0 -1
- 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
|