@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.
- 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
|
@@ -1,516 +0,0 @@
|
|
|
1
|
-
import { b as relayFailure, h as OaathRelayError, n as OAATH_AUTHORIZATION_DECISION_RECORD_VERSION, o as RELAY_LIMITS, r as OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION, s as boundedText } from "./records-Bgr-9PUh.js";
|
|
2
|
-
import { a as relayNow, n as randomIdentifier, r as sha256Base64Url, t as isCodeChallengeS256 } from "./challenge-AGTmUfj2.js";
|
|
3
|
-
import { hashOwnerSigningRequest, parseKernelV4ReplayableInstallOwnerSigningRequest, parseOwnerSigningArtifact, parseOwnerSigningRequest, parsePermissionRequest, serializeOwnerSigningArtifact } from "@oaath/protocol";
|
|
4
|
-
import { p256 } from "@noble/curves/nist.js";
|
|
5
|
-
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
6
|
-
//#region src/store/interface.ts
|
|
7
|
-
/**
|
|
8
|
-
* Runs one transition inside one transaction. The body commits, or the
|
|
9
|
-
* transaction is rolled back and the canonical failure is preserved.
|
|
10
|
-
*/
|
|
11
|
-
async function withRelayTransaction(store, run) {
|
|
12
|
-
const transaction = await store.begin();
|
|
13
|
-
let result;
|
|
14
|
-
try {
|
|
15
|
-
result = await run(transaction);
|
|
16
|
-
} catch (error) {
|
|
17
|
-
await transaction.rollback();
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
20
|
-
await transaction.commit();
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/artifact/encrypt.ts
|
|
25
|
-
/**
|
|
26
|
-
* KMS sealing boundary. Plaintext never reaches the store.
|
|
27
|
-
*
|
|
28
|
-
* @author taek <leekt216@gmail.com>
|
|
29
|
-
*/
|
|
30
|
-
const KMS_UNAVAILABLE = "relay_kms_unavailable";
|
|
31
|
-
/**
|
|
32
|
-
* Encrypts an approval artifact and returns the opaque reference to store.
|
|
33
|
-
* A port that echoes plaintext is rejected: the store must never hold it.
|
|
34
|
-
*/
|
|
35
|
-
async function sealArtifact(kms, plaintext) {
|
|
36
|
-
let sealed;
|
|
37
|
-
try {
|
|
38
|
-
sealed = await kms.encrypt(plaintext);
|
|
39
|
-
} catch {
|
|
40
|
-
return relayFailure(KMS_UNAVAILABLE, "KMS encrypt failed");
|
|
41
|
-
}
|
|
42
|
-
const ciphertextRef = boundedText(sealed, RELAY_LIMITS.ciphertextRef, "KMS ciphertext reference", KMS_UNAVAILABLE);
|
|
43
|
-
if (ciphertextRef.includes(plaintext)) return relayFailure(KMS_UNAVAILABLE, "KMS reference contains plaintext");
|
|
44
|
-
return ciphertextRef;
|
|
45
|
-
}
|
|
46
|
-
/** Opens a reference this KMS previously sealed. Called only after the claim commits. */
|
|
47
|
-
async function openArtifact(kms, ciphertextRef) {
|
|
48
|
-
let opened;
|
|
49
|
-
try {
|
|
50
|
-
opened = await kms.decrypt(ciphertextRef);
|
|
51
|
-
} catch {
|
|
52
|
-
return relayFailure(KMS_UNAVAILABLE, "KMS decrypt failed");
|
|
53
|
-
}
|
|
54
|
-
return boundedText(opened, RELAY_LIMITS.artifactPlaintext, "KMS plaintext", KMS_UNAVAILABLE);
|
|
55
|
-
}
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/authorization/owner-signing.ts
|
|
58
|
-
/**
|
|
59
|
-
* Pure authorization of one returned Kernel owner-signing artifact.
|
|
60
|
-
*
|
|
61
|
-
* The protocol owns both exact wire shapes. This server owner adds only the
|
|
62
|
-
* release policy: the request must use the first-runtime P-256 credential, the
|
|
63
|
-
* artifact must be its one canonical JSON representation, its request hash must
|
|
64
|
-
* bind the exact request, and its compact low-S signature must verify the exact
|
|
65
|
-
* Kernel digest. It allocates and persists nothing.
|
|
66
|
-
*
|
|
67
|
-
* @author taek <leekt216@gmail.com>
|
|
68
|
-
*/
|
|
69
|
-
const INVALID = "relay_request_invalid";
|
|
70
|
-
/**
|
|
71
|
-
* Returns the canonical artifact plaintext only after every request, key,
|
|
72
|
-
* request-hash, and signature binding agrees.
|
|
73
|
-
*/
|
|
74
|
-
function verifyKernelV4ReplayableInstallOwnerSigningArtifact(requestValue, artifactPlaintext) {
|
|
75
|
-
try {
|
|
76
|
-
const request = parseKernelV4ReplayableInstallOwnerSigningRequest(requestValue);
|
|
77
|
-
if (request.signer.ownerCredential.kind !== "p256") throw new TypeError();
|
|
78
|
-
const received = boundedText(artifactPlaintext, RELAY_LIMITS.artifactPlaintext, "owner signing artifact", INVALID);
|
|
79
|
-
const artifact = parseOwnerSigningArtifact(JSON.parse(received));
|
|
80
|
-
const canonical = serializeOwnerSigningArtifact(artifact);
|
|
81
|
-
if (received !== canonical) throw new TypeError();
|
|
82
|
-
if (artifact.requestHash !== hashOwnerSigningRequest(request)) throw new TypeError();
|
|
83
|
-
if (!p256.verify(hexToBytes(artifact.signature.slice(2)), hexToBytes(request.expectedDigest.slice(2)), hexToBytes(request.signer.ownerCredential.publicKey.slice(2)), {
|
|
84
|
-
format: "compact",
|
|
85
|
-
lowS: true,
|
|
86
|
-
prehash: false
|
|
87
|
-
})) throw new TypeError();
|
|
88
|
-
return canonical;
|
|
89
|
-
} catch {
|
|
90
|
-
return relayFailure(INVALID, "Kernel owner signing artifact is invalid");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/authorization/request.ts
|
|
95
|
-
/**
|
|
96
|
-
* Create and read an authorization request.
|
|
97
|
-
*
|
|
98
|
-
* The request record is immutable once created. Its decision is a separate
|
|
99
|
-
* terminal record, so "was this decided?" has exactly one owner.
|
|
100
|
-
*
|
|
101
|
-
* @author taek <leekt216@gmail.com>
|
|
102
|
-
*/
|
|
103
|
-
async function createAuthorizationRequest(input) {
|
|
104
|
-
if (!isCodeChallengeS256(input.codeChallenge)) return relayFailure("relay_request_invalid", "codeChallenge must be an S256 challenge");
|
|
105
|
-
if (!input.caller.redirectUris.includes(input.redirectUri)) return relayFailure("relay_forbidden", "redirectUri is not registered for this client");
|
|
106
|
-
const createdAt = relayNow(input.clock);
|
|
107
|
-
const record = Object.freeze({
|
|
108
|
-
version: OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION,
|
|
109
|
-
requestId: randomIdentifier(),
|
|
110
|
-
clientId: input.caller.clientId,
|
|
111
|
-
subject: input.caller.subject,
|
|
112
|
-
redirectUri: input.redirectUri,
|
|
113
|
-
codeChallenge: input.codeChallenge,
|
|
114
|
-
requestedScope: input.requestedScope,
|
|
115
|
-
createdAt,
|
|
116
|
-
expiresAt: createdAt + input.requestTtlMs
|
|
117
|
-
});
|
|
118
|
-
await withRelayTransaction(input.store, async (transaction) => {
|
|
119
|
-
if (!await transaction.insertAuthorizationRequest(record)) return relayFailure("relay_internal", "authorization request identifier is not unique");
|
|
120
|
-
});
|
|
121
|
-
return Object.freeze({
|
|
122
|
-
requestId: record.requestId,
|
|
123
|
-
expiresAt: record.expiresAt
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Row-locked read of the request and its decision. Read paths take the same
|
|
128
|
-
* exclusive lock as transitions.
|
|
129
|
-
*
|
|
130
|
-
* ponytail: one lock class for reads and writes; add a shared read mode only if
|
|
131
|
-
* approval-screen traffic measurably contends.
|
|
132
|
-
*/
|
|
133
|
-
async function readAuthorizationState(transaction, requestId, now, match) {
|
|
134
|
-
const request = await transaction.lockAuthorizationRequest(requestId);
|
|
135
|
-
if (!request || !match(request)) return relayFailure("relay_not_found", "authorization request does not exist");
|
|
136
|
-
const decision = await transaction.lockAuthorizationDecision(requestId);
|
|
137
|
-
return Object.freeze({
|
|
138
|
-
requestId: request.requestId,
|
|
139
|
-
clientId: request.clientId,
|
|
140
|
-
redirectUri: request.redirectUri,
|
|
141
|
-
requestedScope: request.requestedScope,
|
|
142
|
-
expiresAt: request.expiresAt,
|
|
143
|
-
expired: now >= request.expiresAt,
|
|
144
|
-
decision: decision ? Object.freeze({
|
|
145
|
-
outcome: decision.outcome,
|
|
146
|
-
decidedAt: decision.decidedAt
|
|
147
|
-
}) : null
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
/** Read for the approving owner. */
|
|
151
|
-
function fetchAuthorizationRequest(input) {
|
|
152
|
-
const now = relayNow(input.clock);
|
|
153
|
-
return withRelayTransaction(input.store, (transaction) => readAuthorizationState(transaction, input.requestId, now, (request) => request.subject === input.caller.subject));
|
|
154
|
-
}
|
|
155
|
-
//#endregion
|
|
156
|
-
//#region src/authorization/scope.ts
|
|
157
|
-
/**
|
|
158
|
-
* Authoritative classification of one immutable stored authorization scope.
|
|
159
|
-
*
|
|
160
|
-
* The current server may approve an exact protocol PermissionRequest or the
|
|
161
|
-
* one exact Kernel replayable-install request backed by a P-256 owner. Other
|
|
162
|
-
* closed owner-signing requests are captured for projection and hashing but
|
|
163
|
-
* remain reject-only. Unknown, malformed, and legacy scopes stay readable and
|
|
164
|
-
* rejectable, but never authorize artifact creation.
|
|
165
|
-
*
|
|
166
|
-
* @author taek <leekt216@gmail.com>
|
|
167
|
-
*/
|
|
168
|
-
function classifyStoredAuthorizationScope(requestedScope, requestId) {
|
|
169
|
-
try {
|
|
170
|
-
const parsed = JSON.parse(requestedScope);
|
|
171
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return Object.freeze({
|
|
172
|
-
kind: "unverified",
|
|
173
|
-
decision: "reject-only"
|
|
174
|
-
});
|
|
175
|
-
try {
|
|
176
|
-
return Object.freeze({
|
|
177
|
-
kind: "permission-request",
|
|
178
|
-
decision: "approve-or-reject",
|
|
179
|
-
request: parsePermissionRequest({
|
|
180
|
-
...parsed,
|
|
181
|
-
requestId
|
|
182
|
-
})
|
|
183
|
-
});
|
|
184
|
-
} catch {
|
|
185
|
-
const request = parseOwnerSigningRequest(parsed);
|
|
186
|
-
try {
|
|
187
|
-
const kernelRequest = parseKernelV4ReplayableInstallOwnerSigningRequest(request);
|
|
188
|
-
if (kernelRequest.signer.ownerCredential.kind === "p256") return Object.freeze({
|
|
189
|
-
kind: "kernel-owner-signing-request",
|
|
190
|
-
decision: "approve-or-reject",
|
|
191
|
-
request: kernelRequest
|
|
192
|
-
});
|
|
193
|
-
} catch {}
|
|
194
|
-
return Object.freeze({
|
|
195
|
-
kind: "owner-signing-request",
|
|
196
|
-
decision: "reject-only",
|
|
197
|
-
request
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
} catch {
|
|
201
|
-
return Object.freeze({
|
|
202
|
-
kind: "unverified",
|
|
203
|
-
decision: "reject-only"
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
//#endregion
|
|
208
|
-
//#region src/authorization/decision.ts
|
|
209
|
-
/**
|
|
210
|
-
* Authoritative approve/reject transition.
|
|
211
|
-
*
|
|
212
|
-
* ```text
|
|
213
|
-
* state and owner the decision record owns "decided"; the request record
|
|
214
|
-
* never mutates
|
|
215
|
-
* persisted evidence the immutable requestedScope is reclassified from the
|
|
216
|
-
* durable request before every approval attempt
|
|
217
|
-
* resource occupied? a refused approval occupies nothing and performs no KMS
|
|
218
|
-
* sealing; a rejection occupies only its decision row
|
|
219
|
-
* retry positively safe? refused approval is read-only; a terminal outcome is
|
|
220
|
-
* replayed by the native saga and rejected by this owner
|
|
221
|
-
* transitions permission or verified Kernel owner signing:
|
|
222
|
-
* undecided -> approved | rejected;
|
|
223
|
-
* every reject-only scope: undecided -> rejected once
|
|
224
|
-
* terminal both outcomes; a second decide fails relay_already_decided
|
|
225
|
-
* crash/reload the decision, the code, and the sealed artifact commit in
|
|
226
|
-
* one transaction on the row-locked request, so a crash
|
|
227
|
-
* leaves the request undecided and nothing released
|
|
228
|
-
* cleanup owner the relay transaction; refused approvals allocate nothing
|
|
229
|
-
* ```
|
|
230
|
-
*
|
|
231
|
-
* Subject binding: the authoritative subject is recovered from the stored
|
|
232
|
-
* authorization request by `requestId` and compared against the authenticated
|
|
233
|
-
* owner. No wire field names the subject. A decision envelope that carries a
|
|
234
|
-
* subject identifier is rejected as an unknown field by exact capture, because a
|
|
235
|
-
* field that only *names* a subject is not cryptographically bound to it and can
|
|
236
|
-
* never decide authority.
|
|
237
|
-
*
|
|
238
|
-
* @author taek <leekt216@gmail.com>
|
|
239
|
-
*/
|
|
240
|
-
async function submitAuthorizationDecision(input) {
|
|
241
|
-
const decidedAt = relayNow(input.clock);
|
|
242
|
-
let approvedArtifact;
|
|
243
|
-
if (input.command.outcome === "approved") {
|
|
244
|
-
const state = await fetchAuthorizationRequest({
|
|
245
|
-
store: input.store,
|
|
246
|
-
clock: input.clock,
|
|
247
|
-
caller: input.caller,
|
|
248
|
-
requestId: input.requestId
|
|
249
|
-
});
|
|
250
|
-
if (state.decision !== null) return relayFailure("relay_already_decided", "authorization request is already decided");
|
|
251
|
-
if (state.expired) return relayFailure("relay_expired", "authorization request expired");
|
|
252
|
-
const scope = classifyStoredAuthorizationScope(state.requestedScope, state.requestId);
|
|
253
|
-
if (scope.kind === "permission-request") approvedArtifact = input.command.artifact;
|
|
254
|
-
else if (scope.kind === "kernel-owner-signing-request") approvedArtifact = verifyKernelV4ReplayableInstallOwnerSigningArtifact(scope.request, input.command.artifact);
|
|
255
|
-
else return relayFailure("relay_request_invalid", "the authorization scope is reject-only");
|
|
256
|
-
}
|
|
257
|
-
let approved;
|
|
258
|
-
if (input.command.outcome === "approved") {
|
|
259
|
-
if (approvedArtifact === void 0) return relayFailure("relay_internal", "approved artifact was not authorized");
|
|
260
|
-
const code = randomIdentifier();
|
|
261
|
-
approved = Object.freeze({
|
|
262
|
-
code,
|
|
263
|
-
artifactId: randomIdentifier(),
|
|
264
|
-
ciphertextRef: await sealArtifact(input.kms, approvedArtifact),
|
|
265
|
-
codeRef: await sealArtifact(input.kms, code)
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
const codeHash = approved ? await sha256Base64Url(approved.code) : void 0;
|
|
269
|
-
const decision = Object.freeze({
|
|
270
|
-
version: OAATH_AUTHORIZATION_DECISION_RECORD_VERSION,
|
|
271
|
-
requestId: input.requestId,
|
|
272
|
-
outcome: input.command.outcome,
|
|
273
|
-
decidedAt,
|
|
274
|
-
codeRef: approved?.codeRef ?? null,
|
|
275
|
-
codeExpiresAt: approved ? decidedAt + input.codeTtlMs : null
|
|
276
|
-
});
|
|
277
|
-
const redirectUri = await withRelayTransaction(input.store, async (transaction) => {
|
|
278
|
-
const request = await transaction.lockAuthorizationRequest(input.requestId);
|
|
279
|
-
if (!request || request.subject !== input.caller.subject) return relayFailure("relay_not_found", "authorization request does not exist");
|
|
280
|
-
if (await transaction.lockAuthorizationDecision(input.requestId)) return relayFailure("relay_already_decided", "authorization request is already decided");
|
|
281
|
-
if (decidedAt >= request.expiresAt) return relayFailure("relay_expired", "authorization request expired");
|
|
282
|
-
if (!await transaction.insertAuthorizationDecision(decision)) return relayFailure("relay_already_decided", "authorization request is already decided");
|
|
283
|
-
if (approved && codeHash !== void 0) {
|
|
284
|
-
if (!(await transaction.insertAuthorizationCode(Object.freeze({
|
|
285
|
-
version: "oaath.authorization-code-record/v1",
|
|
286
|
-
codeHash,
|
|
287
|
-
requestId: request.requestId,
|
|
288
|
-
clientId: request.clientId,
|
|
289
|
-
redirectUri: request.redirectUri,
|
|
290
|
-
codeChallenge: request.codeChallenge,
|
|
291
|
-
artifactId: approved.artifactId,
|
|
292
|
-
createdAt: decidedAt,
|
|
293
|
-
expiresAt: decidedAt + input.codeTtlMs,
|
|
294
|
-
consumedAt: null
|
|
295
|
-
})) && await transaction.insertEncryptedArtifact(Object.freeze({
|
|
296
|
-
version: "oaath.encrypted-artifact-record/v1",
|
|
297
|
-
artifactId: approved.artifactId,
|
|
298
|
-
requestId: request.requestId,
|
|
299
|
-
clientId: request.clientId,
|
|
300
|
-
ciphertextRef: approved.ciphertextRef,
|
|
301
|
-
createdAt: decidedAt,
|
|
302
|
-
claimedAt: null
|
|
303
|
-
})))) return relayFailure("relay_internal", "released identifier is not unique");
|
|
304
|
-
}
|
|
305
|
-
return request.redirectUri;
|
|
306
|
-
});
|
|
307
|
-
if (approved && codeHash !== void 0) return Object.freeze({
|
|
308
|
-
outcome: "approved",
|
|
309
|
-
decidedAt,
|
|
310
|
-
code: approved.code,
|
|
311
|
-
artifactId: approved.artifactId,
|
|
312
|
-
redirectUri,
|
|
313
|
-
codeExpiresAt: decidedAt + input.codeTtlMs
|
|
314
|
-
});
|
|
315
|
-
return Object.freeze({
|
|
316
|
-
outcome: "rejected",
|
|
317
|
-
decidedAt
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
//#endregion
|
|
321
|
-
//#region src/native/decision.ts
|
|
322
|
-
async function submitOwnerPhoneDecision(input) {
|
|
323
|
-
if (input.caller.role !== "owner") return relayFailure("relay_forbidden", "caller may not act in the required role");
|
|
324
|
-
try {
|
|
325
|
-
const release = await submitAuthorizationDecision({
|
|
326
|
-
store: input.store,
|
|
327
|
-
clock: input.clock,
|
|
328
|
-
kms: input.kms,
|
|
329
|
-
caller: input.caller,
|
|
330
|
-
requestId: input.operationId,
|
|
331
|
-
command: input.command,
|
|
332
|
-
codeTtlMs: input.codeTtlMs
|
|
333
|
-
});
|
|
334
|
-
return Object.freeze({
|
|
335
|
-
operationId: input.operationId,
|
|
336
|
-
outcome: release.outcome,
|
|
337
|
-
decidedAt: release.decidedAt,
|
|
338
|
-
settlement: "decided",
|
|
339
|
-
release
|
|
340
|
-
});
|
|
341
|
-
} catch (error) {
|
|
342
|
-
if (!(error instanceof OaathRelayError) || error.code !== "relay_already_decided") throw error;
|
|
343
|
-
}
|
|
344
|
-
const state = await fetchAuthorizationRequest({
|
|
345
|
-
store: input.store,
|
|
346
|
-
clock: input.clock,
|
|
347
|
-
caller: input.caller,
|
|
348
|
-
requestId: input.operationId
|
|
349
|
-
});
|
|
350
|
-
if (state.decision === null) return relayFailure("relay_internal", "decided request has no decision record");
|
|
351
|
-
return Object.freeze({
|
|
352
|
-
operationId: input.operationId,
|
|
353
|
-
outcome: state.decision.outcome,
|
|
354
|
-
decidedAt: state.decision.decidedAt,
|
|
355
|
-
settlement: "replayed",
|
|
356
|
-
release: null
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
//#endregion
|
|
360
|
-
//#region src/native/projection.ts
|
|
361
|
-
/**
|
|
362
|
-
* EXPERIMENTAL PREVIEW — authenticated owner-phone consent projection.
|
|
363
|
-
*
|
|
364
|
-
* This is a preview surface reached through `@oaath/server/native` and the
|
|
365
|
-
* relay's preview HTTP routes (`GET /native/projections/{operationId}`). It has
|
|
366
|
-
* no stability guarantee and no production qualification: real iOS, Apple
|
|
367
|
-
* provisioning, Secure Enclave, and hosted operations are later work.
|
|
368
|
-
*
|
|
369
|
-
* What it owns: turning one stored authorization request into the projection
|
|
370
|
-
* the owner's phone renders before deciding. It owns nothing about deciding;
|
|
371
|
-
* the relay's decision use case stays the only decision owner.
|
|
372
|
-
*
|
|
373
|
-
* Two very different surfaces share this file, and only one of them is opaque:
|
|
374
|
-
*
|
|
375
|
-
* - The **push** projection (`operationId`, `displayPayload`, `expiresAt`)
|
|
376
|
-
* transits Apple inside an APNs payload, so it stays opaque by design: no
|
|
377
|
-
* permission, scope, client, chain, or account detail may ride it. The APNs
|
|
378
|
-
* sender enforces that subset exactly.
|
|
379
|
-
* - The **consent** projection returned here travels only the authenticated
|
|
380
|
-
* relay → owner channel, so it deliberately carries the full authorization
|
|
381
|
-
* request: the requesting client identity and the requested scope. The owner
|
|
382
|
-
* must see exactly the authority they grant before approving.
|
|
383
|
-
*
|
|
384
|
-
* `displayPayload` is not a secret and not authority: it is a deterministic
|
|
385
|
-
* digest of the request and its bound subject, so the owner can check that the
|
|
386
|
-
* code on the phone is the code the browser shows. Holding it authorizes
|
|
387
|
-
* nothing — deciding still requires the authenticated bound owner.
|
|
388
|
-
*
|
|
389
|
-
* @author taek <leekt216@gmail.com>
|
|
390
|
-
*/
|
|
391
|
-
/** Bounded base64url match code length. 48 bits is plenty to compare by eye. */
|
|
392
|
-
const NATIVE_DISPLAY_PAYLOAD_LENGTH = 8;
|
|
393
|
-
/** Versioned consent envelope; the Swift decoder pins this exact value. */
|
|
394
|
-
const OAATH_NATIVE_PROJECTION_VERSION = "oaath.native-projection/v4";
|
|
395
|
-
const DISPLAY_DOMAIN = "oaath.native-display/v1:";
|
|
396
|
-
/**
|
|
397
|
-
* The stored scope is the requesting client's own JSON permission scope (the
|
|
398
|
-
* SDK serializes a permission request without its `requestId`; the relay's
|
|
399
|
-
* request id is the binding). An unparseable scope is not a failure: the owner
|
|
400
|
-
* still gets the raw text to review, never a silent omission.
|
|
401
|
-
*/
|
|
402
|
-
function projectCredential(credential) {
|
|
403
|
-
if (credential.kind === "ecdsa") return Object.freeze({
|
|
404
|
-
kind: "ecdsa",
|
|
405
|
-
address: credential.address ?? ""
|
|
406
|
-
});
|
|
407
|
-
if (credential.kind === "p256") return Object.freeze({
|
|
408
|
-
kind: "p256",
|
|
409
|
-
publicKey: credential.publicKey ?? ""
|
|
410
|
-
});
|
|
411
|
-
return Object.freeze({
|
|
412
|
-
kind: "webauthn",
|
|
413
|
-
publicKey: credential.publicKey ?? "",
|
|
414
|
-
authenticatorIdHash: credential.authenticatorIdHash ?? ""
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
async function projectOwnerPhoneScope(requestedScope, operationId) {
|
|
418
|
-
try {
|
|
419
|
-
const classified = classifyStoredAuthorizationScope(requestedScope, operationId);
|
|
420
|
-
if (classified.kind === "permission-request") {
|
|
421
|
-
const request = classified.request;
|
|
422
|
-
return Object.freeze({
|
|
423
|
-
kind: "permission-request",
|
|
424
|
-
decision: "approve-or-reject",
|
|
425
|
-
application: Object.freeze({
|
|
426
|
-
applicationId: request.application.applicationId,
|
|
427
|
-
clientId: request.application.clientId,
|
|
428
|
-
origin: request.application.origin,
|
|
429
|
-
deviceFingerprint: (await sha256Base64Url(`${DISPLAY_DOMAIN}device:${request.application.deviceId}`)).slice(0, 8)
|
|
430
|
-
}),
|
|
431
|
-
account: Object.freeze({
|
|
432
|
-
accountIndex: request.logicalAccount.accountIndex,
|
|
433
|
-
kernelVersion: request.logicalAccount.kernelVersion,
|
|
434
|
-
factoryRoute: request.logicalAccount.factoryRoute,
|
|
435
|
-
entryPointVersion: request.logicalAccount.entryPoint.version,
|
|
436
|
-
ownerCredential: projectCredential(request.logicalAccount.ownerCredential)
|
|
437
|
-
}),
|
|
438
|
-
operatorCredential: projectCredential(request.operatorCredential),
|
|
439
|
-
sessionSigner: request.sessionSigner === null ? null : Object.freeze({
|
|
440
|
-
mode: request.sessionSigner.mode,
|
|
441
|
-
providerId: request.sessionSigner.providerId
|
|
442
|
-
}),
|
|
443
|
-
chainScope: request.chainScope,
|
|
444
|
-
calls: Object.freeze(request.policy.calls.map((call) => Object.freeze({
|
|
445
|
-
target: call.target,
|
|
446
|
-
selector: call.selector,
|
|
447
|
-
valueLimit: call.valueLimit,
|
|
448
|
-
argumentEquals: Object.freeze(call.argumentEquals.map((rule) => Object.freeze({
|
|
449
|
-
index: rule.index,
|
|
450
|
-
value: rule.value
|
|
451
|
-
})))
|
|
452
|
-
}))),
|
|
453
|
-
requestedAt: request.requestedAt,
|
|
454
|
-
expiresAt: request.expiresAt,
|
|
455
|
-
policyValidAfter: request.policy.validAfter,
|
|
456
|
-
policyValidUntil: request.policy.validUntil,
|
|
457
|
-
perChainOperationLimit: request.policy.perChainOperationLimit
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
if (classified.kind === "kernel-owner-signing-request") return Object.freeze({
|
|
461
|
-
kind: "owner-signing-request",
|
|
462
|
-
decision: "approve-or-reject",
|
|
463
|
-
requestHash: hashOwnerSigningRequest(classified.request),
|
|
464
|
-
request: classified.request
|
|
465
|
-
});
|
|
466
|
-
if (classified.kind === "owner-signing-request") return Object.freeze({
|
|
467
|
-
kind: "owner-signing-request",
|
|
468
|
-
decision: "reject-only",
|
|
469
|
-
requestHash: hashOwnerSigningRequest(classified.request),
|
|
470
|
-
request: classified.request
|
|
471
|
-
});
|
|
472
|
-
return Object.freeze({
|
|
473
|
-
kind: "raw",
|
|
474
|
-
decision: "reject-only",
|
|
475
|
-
text: requestedScope
|
|
476
|
-
});
|
|
477
|
-
} catch {
|
|
478
|
-
return Object.freeze({
|
|
479
|
-
kind: "raw",
|
|
480
|
-
decision: "reject-only",
|
|
481
|
-
text: requestedScope
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Projects a pending request for its bound owner. An expired or already decided
|
|
487
|
-
* request is not projectable: there is nothing left to approve, so nothing is
|
|
488
|
-
* ever pushed or rendered for it.
|
|
489
|
-
*/
|
|
490
|
-
async function projectOwnerPhoneRequest(input) {
|
|
491
|
-
if (input.caller.role !== "owner") return relayFailure("relay_forbidden", "caller may not act in the required role");
|
|
492
|
-
const state = await fetchAuthorizationRequest({
|
|
493
|
-
store: input.store,
|
|
494
|
-
clock: input.clock,
|
|
495
|
-
caller: input.caller,
|
|
496
|
-
requestId: input.requestId
|
|
497
|
-
});
|
|
498
|
-
if (state.expired) return relayFailure("relay_expired", "authorization request expired");
|
|
499
|
-
if (state.decision !== null) return relayFailure("relay_already_decided", "authorization request is already decided");
|
|
500
|
-
const digest = await sha256Base64Url(`${DISPLAY_DOMAIN}${input.caller.subject}:${state.requestId}`);
|
|
501
|
-
return Object.freeze({
|
|
502
|
-
version: OAATH_NATIVE_PROJECTION_VERSION,
|
|
503
|
-
operationId: state.requestId,
|
|
504
|
-
displayPayload: digest.slice(0, 8),
|
|
505
|
-
expiresAt: state.expiresAt,
|
|
506
|
-
client: Object.freeze({
|
|
507
|
-
clientId: state.clientId,
|
|
508
|
-
redirectUri: state.redirectUri
|
|
509
|
-
}),
|
|
510
|
-
scope: await projectOwnerPhoneScope(state.requestedScope, state.requestId)
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
//#endregion
|
|
514
|
-
export { submitAuthorizationDecision as a, readAuthorizationState as c, withRelayTransaction as d, submitOwnerPhoneDecision as i, openArtifact as l, OAATH_NATIVE_PROJECTION_VERSION as n, createAuthorizationRequest as o, projectOwnerPhoneRequest as r, fetchAuthorizationRequest as s, NATIVE_DISPLAY_PAYLOAD_LENGTH as t, sealArtifact as u };
|
|
515
|
-
|
|
516
|
-
//# sourceMappingURL=projection-dGIkE0rI.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"projection-dGIkE0rI.js","names":[],"sources":["../src/store/interface.ts","../src/artifact/encrypt.ts","../src/authorization/owner-signing.ts","../src/authorization/request.ts","../src/authorization/scope.ts","../src/authorization/decision.ts","../src/native/decision.ts","../src/native/projection.ts"],"sourcesContent":["/**\n * Durable relay transaction contract.\n *\n * State model owned by this contract:\n *\n * ```text\n * state and owner authorization request (immutable), decision (terminal),\n * code (one-shot), encrypted artifact (one-shot)\n * persisted evidence one row per record, one current schema version\n * resource occupied? yes: a decision row occupies its request; a consumed\n * code and a claimed artifact are terminal\n * retry positively safe? no: an ambiguous commit is never retried\n * transitions request -> decision(approved|rejected) once;\n * code: issued -> consumed once;\n * artifact: sealed -> claimed once\n * crash/reload every transition is decided inside one transaction on a\n * row locked for update; a crash leaves the prior state\n * cleanup owner the caller rolls back; the store releases its handle\n * ```\n *\n * Every `lock*` read must acquire an exclusive row lock for the life of the\n * transaction (`SELECT ... FOR UPDATE` semantics). Every one-shot writer\n * (`consumeAuthorizationCode`, `claimEncryptedArtifact`) must additionally\n * guard on the terminal column being null and report whether *this* call\n * performed the transition.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport type {\n AuthorizationCodeRecord,\n AuthorizationDecisionRecord,\n AuthorizationRequestRecord,\n CapabilityInvalidationRecord,\n EncryptedArtifactRecord,\n} from \"./records.js\";\n\nexport interface RelayTransaction {\n lockAuthorizationRequest(requestId: string): Promise<AuthorizationRequestRecord | undefined>;\n /** Returns false when the identifier already exists. */\n insertAuthorizationRequest(record: AuthorizationRequestRecord): Promise<boolean>;\n\n lockAuthorizationDecision(requestId: string): Promise<AuthorizationDecisionRecord | undefined>;\n /** Returns false when the request was already decided. */\n insertAuthorizationDecision(record: AuthorizationDecisionRecord): Promise<boolean>;\n\n lockAuthorizationCode(codeHash: string): Promise<AuthorizationCodeRecord | undefined>;\n insertAuthorizationCode(record: AuthorizationCodeRecord): Promise<boolean>;\n /** Returns true only when this call set `consumed_at`. */\n consumeAuthorizationCode(codeHash: string, consumedAt: number): Promise<boolean>;\n\n lockCapabilityInvalidation(grantId: string): Promise<CapabilityInvalidationRecord | undefined>;\n /** Returns false when the Grant's capability is already invalidated. */\n insertCapabilityInvalidation(record: CapabilityInvalidationRecord): Promise<boolean>;\n\n lockEncryptedArtifact(artifactId: string): Promise<EncryptedArtifactRecord | undefined>;\n insertEncryptedArtifact(record: EncryptedArtifactRecord): Promise<boolean>;\n /** Returns true only when this call set `claimed_at`. */\n claimEncryptedArtifact(artifactId: string, claimedAt: number): Promise<boolean>;\n\n /** Throws `relay_state_ambiguous` when the outcome cannot be proven. */\n commit(): Promise<void>;\n /** Never throws; rollback failure is a suppressed diagnostic. */\n rollback(): Promise<void>;\n}\n\nexport interface RelayStore {\n begin(): Promise<RelayTransaction>;\n close(): Promise<void>;\n}\n\n/**\n * Runs one transition inside one transaction. The body commits, or the\n * transaction is rolled back and the canonical failure is preserved.\n */\nexport async function withRelayTransaction<Value>(\n store: RelayStore,\n run: (transaction: RelayTransaction) => Promise<Value>,\n): Promise<Value> {\n const transaction = await store.begin();\n let result: Value;\n try {\n result = await run(transaction);\n } catch (error) {\n await transaction.rollback();\n throw error;\n }\n await transaction.commit();\n return result;\n}\n","/**\n * KMS sealing boundary. Plaintext never reaches the store.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport { relayFailure } from \"../relay/errors.js\";\nimport type { RelayKms } from \"../security/kms.js\";\nimport { boundedText, RELAY_LIMITS } from \"../store/records.js\";\n\nconst KMS_UNAVAILABLE = \"relay_kms_unavailable\" as const;\n\n/**\n * Encrypts an approval artifact and returns the opaque reference to store.\n * A port that echoes plaintext is rejected: the store must never hold it.\n */\nexport async function sealArtifact(kms: RelayKms, plaintext: string): Promise<string> {\n let sealed: unknown;\n try {\n sealed = await kms.encrypt(plaintext);\n } catch {\n return relayFailure(KMS_UNAVAILABLE, \"KMS encrypt failed\");\n }\n const ciphertextRef = boundedText(\n sealed,\n RELAY_LIMITS.ciphertextRef,\n \"KMS ciphertext reference\",\n KMS_UNAVAILABLE,\n );\n if (ciphertextRef.includes(plaintext)) {\n return relayFailure(KMS_UNAVAILABLE, \"KMS reference contains plaintext\");\n }\n return ciphertextRef;\n}\n\n/** Opens a reference this KMS previously sealed. Called only after the claim commits. */\nexport async function openArtifact(kms: RelayKms, ciphertextRef: string): Promise<string> {\n let opened: unknown;\n try {\n opened = await kms.decrypt(ciphertextRef);\n } catch {\n return relayFailure(KMS_UNAVAILABLE, \"KMS decrypt failed\");\n }\n return boundedText(opened, RELAY_LIMITS.artifactPlaintext, \"KMS plaintext\", KMS_UNAVAILABLE);\n}\n","/**\n * Pure authorization of one returned Kernel owner-signing artifact.\n *\n * The protocol owns both exact wire shapes. This server owner adds only the\n * release policy: the request must use the first-runtime P-256 credential, the\n * artifact must be its one canonical JSON representation, its request hash must\n * bind the exact request, and its compact low-S signature must verify the exact\n * Kernel digest. It allocates and persists nothing.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport { p256 } from \"@noble/curves/nist.js\";\nimport { hexToBytes } from \"@noble/hashes/utils.js\";\nimport {\n hashOwnerSigningRequest,\n parseKernelV4ReplayableInstallOwnerSigningRequest,\n parseOwnerSigningArtifact,\n serializeOwnerSigningArtifact,\n} from \"@oaath/protocol\";\nimport { relayFailure } from \"../relay/errors.js\";\nimport { boundedText, RELAY_LIMITS } from \"../store/records.js\";\n\nconst INVALID = \"relay_request_invalid\" as const;\n\n/**\n * Returns the canonical artifact plaintext only after every request, key,\n * request-hash, and signature binding agrees.\n */\nexport function verifyKernelV4ReplayableInstallOwnerSigningArtifact(\n requestValue: unknown,\n artifactPlaintext: unknown,\n): string {\n try {\n const request = parseKernelV4ReplayableInstallOwnerSigningRequest(requestValue);\n if (request.signer.ownerCredential.kind !== \"p256\") throw new TypeError();\n\n const received = boundedText(\n artifactPlaintext,\n RELAY_LIMITS.artifactPlaintext,\n \"owner signing artifact\",\n INVALID,\n );\n const artifact = parseOwnerSigningArtifact(JSON.parse(received) as unknown);\n const canonical = serializeOwnerSigningArtifact(artifact);\n if (received !== canonical) throw new TypeError();\n if (artifact.requestHash !== hashOwnerSigningRequest(request)) throw new TypeError();\n\n const verified = p256.verify(\n hexToBytes(artifact.signature.slice(2)),\n hexToBytes(request.expectedDigest.slice(2)),\n hexToBytes(request.signer.ownerCredential.publicKey.slice(2)),\n { format: \"compact\", lowS: true, prehash: false },\n );\n if (!verified) throw new TypeError();\n return canonical;\n } catch {\n return relayFailure(INVALID, \"Kernel owner signing artifact is invalid\");\n }\n}\n","/**\n * Create and read an authorization request.\n *\n * The request record is immutable once created. Its decision is a separate\n * terminal record, so \"was this decided?\" has exactly one owner.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport { type RelayClock, relayNow } from \"../clock.js\";\nimport { relayFailure } from \"../relay/errors.js\";\nimport type { RelayCaller } from \"../security/authentication.js\";\nimport type { RelayStore, RelayTransaction } from \"../store/interface.js\";\nimport { withRelayTransaction } from \"../store/interface.js\";\nimport {\n type AuthorizationDecisionOutcome,\n type AuthorizationRequestRecord,\n OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION,\n} from \"../store/records.js\";\nimport { isCodeChallengeS256, randomIdentifier } from \"./challenge.js\";\n\nexport interface CreateAuthorizationRequestInput {\n readonly store: RelayStore;\n readonly clock: RelayClock;\n /** Authenticated `client` caller; it owns `clientId` and `subject`. */\n readonly caller: RelayCaller;\n readonly redirectUri: string;\n readonly codeChallenge: string;\n readonly requestedScope: string;\n readonly requestTtlMs: number;\n}\n\nexport interface CreatedAuthorizationRequest {\n readonly requestId: string;\n readonly expiresAt: number;\n}\n\nexport interface AuthorizationState {\n readonly requestId: string;\n readonly clientId: string;\n readonly redirectUri: string;\n readonly requestedScope: string;\n readonly expiresAt: number;\n readonly expired: boolean;\n readonly decision: Readonly<{\n outcome: AuthorizationDecisionOutcome;\n decidedAt: number;\n }> | null;\n}\n\nexport async function createAuthorizationRequest(\n input: CreateAuthorizationRequestInput,\n): Promise<CreatedAuthorizationRequest> {\n if (!isCodeChallengeS256(input.codeChallenge)) {\n return relayFailure(\"relay_request_invalid\", \"codeChallenge must be an S256 challenge\");\n }\n // The deployment owns client registration, so only a redirect URI it bound to\n // this client may receive an authorization code.\n if (!input.caller.redirectUris.includes(input.redirectUri)) {\n return relayFailure(\"relay_forbidden\", \"redirectUri is not registered for this client\");\n }\n const createdAt = relayNow(input.clock);\n const record: AuthorizationRequestRecord = Object.freeze({\n version: OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION,\n requestId: randomIdentifier(),\n clientId: input.caller.clientId,\n subject: input.caller.subject,\n redirectUri: input.redirectUri,\n codeChallenge: input.codeChallenge,\n requestedScope: input.requestedScope,\n createdAt,\n expiresAt: createdAt + input.requestTtlMs,\n });\n await withRelayTransaction(input.store, async (transaction) => {\n if (!(await transaction.insertAuthorizationRequest(record))) {\n // 256 bits of CSPRNG output collided, or the store contradicts itself.\n return relayFailure(\"relay_internal\", \"authorization request identifier is not unique\");\n }\n });\n return Object.freeze({ requestId: record.requestId, expiresAt: record.expiresAt });\n}\n\n/**\n * Row-locked read of the request and its decision. Read paths take the same\n * exclusive lock as transitions.\n *\n * ponytail: one lock class for reads and writes; add a shared read mode only if\n * approval-screen traffic measurably contends.\n */\nexport async function readAuthorizationState(\n transaction: RelayTransaction,\n requestId: string,\n now: number,\n match: (request: AuthorizationRequestRecord) => boolean,\n): Promise<AuthorizationState> {\n const request = await transaction.lockAuthorizationRequest(requestId);\n // A caller that is not entitled to this request learns only that it is absent.\n if (!request || !match(request)) {\n return relayFailure(\"relay_not_found\", \"authorization request does not exist\");\n }\n const decision = await transaction.lockAuthorizationDecision(requestId);\n return Object.freeze({\n requestId: request.requestId,\n clientId: request.clientId,\n redirectUri: request.redirectUri,\n requestedScope: request.requestedScope,\n expiresAt: request.expiresAt,\n expired: now >= request.expiresAt,\n decision: decision\n ? Object.freeze({ outcome: decision.outcome, decidedAt: decision.decidedAt })\n : null,\n });\n}\n\nexport interface FetchAuthorizationRequestInput {\n readonly store: RelayStore;\n readonly clock: RelayClock;\n /** Authenticated `owner` caller; only the bound subject may read the request. */\n readonly caller: RelayCaller;\n readonly requestId: string;\n}\n\n/** Read for the approving owner. */\nexport function fetchAuthorizationRequest(\n input: FetchAuthorizationRequestInput,\n): Promise<AuthorizationState> {\n const now = relayNow(input.clock);\n return withRelayTransaction(input.store, (transaction) =>\n readAuthorizationState(\n transaction,\n input.requestId,\n now,\n (request) => request.subject === input.caller.subject,\n ),\n );\n}\n","/**\n * Authoritative classification of one immutable stored authorization scope.\n *\n * The current server may approve an exact protocol PermissionRequest or the\n * one exact Kernel replayable-install request backed by a P-256 owner. Other\n * closed owner-signing requests are captured for projection and hashing but\n * remain reject-only. Unknown, malformed, and legacy scopes stay readable and\n * rejectable, but never authorize artifact creation.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport {\n type KernelV4ReplayableInstallOwnerSigningRequest,\n type OwnerSigningRequest,\n type PermissionRequest,\n parseKernelV4ReplayableInstallOwnerSigningRequest,\n parseOwnerSigningRequest,\n parsePermissionRequest,\n} from \"@oaath/protocol\";\n\nexport type StoredAuthorizationScope =\n | Readonly<{\n kind: \"permission-request\";\n decision: \"approve-or-reject\";\n request: Readonly<PermissionRequest>;\n }>\n | Readonly<{\n kind: \"kernel-owner-signing-request\";\n decision: \"approve-or-reject\";\n request: Readonly<KernelV4ReplayableInstallOwnerSigningRequest>;\n }>\n | Readonly<{\n kind: \"owner-signing-request\";\n decision: \"reject-only\";\n request: Readonly<OwnerSigningRequest>;\n }>\n | Readonly<{ kind: \"unverified\"; decision: \"reject-only\" }>;\n\nexport function classifyStoredAuthorizationScope(\n requestedScope: string,\n requestId: string,\n): StoredAuthorizationScope {\n try {\n const parsed: unknown = JSON.parse(requestedScope);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return Object.freeze({ kind: \"unverified\", decision: \"reject-only\" });\n }\n try {\n return Object.freeze({\n kind: \"permission-request\",\n decision: \"approve-or-reject\",\n request: parsePermissionRequest({ ...parsed, requestId }),\n });\n } catch {\n const request = parseOwnerSigningRequest(parsed);\n try {\n const kernelRequest = parseKernelV4ReplayableInstallOwnerSigningRequest(request);\n if (kernelRequest.signer.ownerCredential.kind === \"p256\") {\n return Object.freeze({\n kind: \"kernel-owner-signing-request\",\n decision: \"approve-or-reject\",\n request: kernelRequest,\n });\n }\n } catch {\n // The generic closed request remains readable but reject-only.\n }\n return Object.freeze({\n kind: \"owner-signing-request\",\n decision: \"reject-only\",\n request,\n });\n }\n } catch {\n return Object.freeze({ kind: \"unverified\", decision: \"reject-only\" });\n }\n}\n","/**\n * Authoritative approve/reject transition.\n *\n * ```text\n * state and owner the decision record owns \"decided\"; the request record\n * never mutates\n * persisted evidence the immutable requestedScope is reclassified from the\n * durable request before every approval attempt\n * resource occupied? a refused approval occupies nothing and performs no KMS\n * sealing; a rejection occupies only its decision row\n * retry positively safe? refused approval is read-only; a terminal outcome is\n * replayed by the native saga and rejected by this owner\n * transitions permission or verified Kernel owner signing:\n * undecided -> approved | rejected;\n * every reject-only scope: undecided -> rejected once\n * terminal both outcomes; a second decide fails relay_already_decided\n * crash/reload the decision, the code, and the sealed artifact commit in\n * one transaction on the row-locked request, so a crash\n * leaves the request undecided and nothing released\n * cleanup owner the relay transaction; refused approvals allocate nothing\n * ```\n *\n * Subject binding: the authoritative subject is recovered from the stored\n * authorization request by `requestId` and compared against the authenticated\n * owner. No wire field names the subject. A decision envelope that carries a\n * subject identifier is rejected as an unknown field by exact capture, because a\n * field that only *names* a subject is not cryptographically bound to it and can\n * never decide authority.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport { sealArtifact } from \"../artifact/encrypt.js\";\nimport { type RelayClock, relayNow } from \"../clock.js\";\nimport { relayFailure } from \"../relay/errors.js\";\nimport type { RelayCaller } from \"../security/authentication.js\";\nimport type { RelayKms } from \"../security/kms.js\";\nimport type { RelayStore } from \"../store/interface.js\";\nimport { withRelayTransaction } from \"../store/interface.js\";\nimport {\n OAATH_AUTHORIZATION_CODE_RECORD_VERSION,\n OAATH_AUTHORIZATION_DECISION_RECORD_VERSION,\n OAATH_ENCRYPTED_ARTIFACT_RECORD_VERSION,\n} from \"../store/records.js\";\nimport { randomIdentifier, sha256Base64Url } from \"./challenge.js\";\nimport { verifyKernelV4ReplayableInstallOwnerSigningArtifact } from \"./owner-signing.js\";\nimport { fetchAuthorizationRequest } from \"./request.js\";\nimport { classifyStoredAuthorizationScope } from \"./scope.js\";\n\nexport type AuthorizationDecisionCommand =\n /** The owner approves and hands over the artifact the client will claim once. */\n Readonly<{ outcome: \"approved\"; artifact: string }> | Readonly<{ outcome: \"rejected\" }>;\n\nexport interface SubmitAuthorizationDecisionInput {\n readonly store: RelayStore;\n readonly clock: RelayClock;\n readonly kms: RelayKms;\n /** Authenticated `owner` caller; only the bound subject may decide. */\n readonly caller: RelayCaller;\n readonly requestId: string;\n readonly command: AuthorizationDecisionCommand;\n readonly codeTtlMs: number;\n}\n\nexport type SubmittedAuthorizationDecision =\n | Readonly<{\n outcome: \"approved\";\n decidedAt: number;\n /** Released exactly once, here. Only its SHA-256 is stored. */\n code: string;\n artifactId: string;\n redirectUri: string;\n codeExpiresAt: number;\n }>\n | Readonly<{ outcome: \"rejected\"; decidedAt: number }>;\n\nexport async function submitAuthorizationDecision(\n input: SubmitAuthorizationDecisionInput,\n): Promise<SubmittedAuthorizationDecision> {\n const decidedAt = relayNow(input.clock);\n let approvedArtifact: string | undefined;\n\n // Approval is the artifact-creating transition, so the shared decision owner\n // admits it only for an exact scope this server currently permits to release.\n // Refusal happens before KMS sealing or any durable decision/code/artifact.\n if (input.command.outcome === \"approved\") {\n const state = await fetchAuthorizationRequest({\n store: input.store,\n clock: input.clock,\n caller: input.caller,\n requestId: input.requestId,\n });\n // Preserve the transaction owner's existing precedence: the native saga\n // must recover an earlier terminal outcome without reclassifying its scope.\n if (state.decision !== null) {\n return relayFailure(\"relay_already_decided\", \"authorization request is already decided\");\n }\n if (state.expired) {\n return relayFailure(\"relay_expired\", \"authorization request expired\");\n }\n const scope = classifyStoredAuthorizationScope(state.requestedScope, state.requestId);\n if (scope.kind === \"permission-request\") {\n approvedArtifact = input.command.artifact;\n } else if (scope.kind === \"kernel-owner-signing-request\") {\n approvedArtifact = verifyKernelV4ReplayableInstallOwnerSigningArtifact(\n scope.request,\n input.command.artifact,\n );\n } else {\n return relayFailure(\"relay_request_invalid\", \"the authorization scope is reject-only\");\n }\n }\n\n // Seal before the transaction: the store only ever receives references, and\n // an uncommitted decision leaves nothing but unreferenced ciphertexts. The\n // code is sealed too so the authenticated client can pick it up later; PKCE\n // still guards its consumption.\n let approved:\n | Readonly<{ code: string; artifactId: string; ciphertextRef: string; codeRef: string }>\n | undefined;\n if (input.command.outcome === \"approved\") {\n if (approvedArtifact === undefined) {\n return relayFailure(\"relay_internal\", \"approved artifact was not authorized\");\n }\n const code = randomIdentifier();\n approved = Object.freeze({\n code,\n artifactId: randomIdentifier(),\n ciphertextRef: await sealArtifact(input.kms, approvedArtifact),\n codeRef: await sealArtifact(input.kms, code),\n });\n }\n const codeHash = approved ? await sha256Base64Url(approved.code) : undefined;\n const decision = Object.freeze({\n version: OAATH_AUTHORIZATION_DECISION_RECORD_VERSION,\n requestId: input.requestId,\n outcome: input.command.outcome,\n decidedAt,\n codeRef: approved?.codeRef ?? null,\n codeExpiresAt: approved ? decidedAt + input.codeTtlMs : null,\n } as const);\n\n const redirectUri = await withRelayTransaction(input.store, async (transaction) => {\n const request = await transaction.lockAuthorizationRequest(input.requestId);\n // The stored request owns the subject. A mismatch is reported as absence so\n // the endpoint is not an existence oracle for another subject's request.\n if (!request || request.subject !== input.caller.subject) {\n return relayFailure(\"relay_not_found\", \"authorization request does not exist\");\n }\n if (await transaction.lockAuthorizationDecision(input.requestId)) {\n return relayFailure(\"relay_already_decided\", \"authorization request is already decided\");\n }\n if (decidedAt >= request.expiresAt) {\n return relayFailure(\"relay_expired\", \"authorization request expired\");\n }\n if (!(await transaction.insertAuthorizationDecision(decision))) {\n return relayFailure(\"relay_already_decided\", \"authorization request is already decided\");\n }\n if (approved && codeHash !== undefined) {\n const inserted =\n (await transaction.insertAuthorizationCode(\n Object.freeze({\n version: OAATH_AUTHORIZATION_CODE_RECORD_VERSION,\n codeHash,\n requestId: request.requestId,\n clientId: request.clientId,\n redirectUri: request.redirectUri,\n codeChallenge: request.codeChallenge,\n artifactId: approved.artifactId,\n createdAt: decidedAt,\n expiresAt: decidedAt + input.codeTtlMs,\n consumedAt: null,\n }),\n )) &&\n (await transaction.insertEncryptedArtifact(\n Object.freeze({\n version: OAATH_ENCRYPTED_ARTIFACT_RECORD_VERSION,\n artifactId: approved.artifactId,\n requestId: request.requestId,\n clientId: request.clientId,\n ciphertextRef: approved.ciphertextRef,\n createdAt: decidedAt,\n claimedAt: null,\n }),\n ));\n if (!inserted) {\n return relayFailure(\"relay_internal\", \"released identifier is not unique\");\n }\n }\n return request.redirectUri;\n });\n\n if (approved && codeHash !== undefined) {\n return Object.freeze({\n outcome: \"approved\",\n decidedAt,\n code: approved.code,\n artifactId: approved.artifactId,\n redirectUri,\n codeExpiresAt: decidedAt + input.codeTtlMs,\n });\n }\n return Object.freeze({ outcome: \"rejected\", decidedAt });\n}\n","/**\n * EXPERIMENTAL PREVIEW — approve/reject saga keyed by the stable operation id.\n *\n * ```text\n * state and owner the relay's authorization decision record. This saga\n * owns no decision state of its own and keeps nothing in\n * memory; it routes to `submitAuthorizationDecision`.\n * persisted evidence one terminal decision row per request\n * resource occupied? yes: the decision occupies its request, and the\n * one-time code and artifact are released exactly once\n * retry positively safe? yes for the saga call, because a repeated call never\n * decides again: it answers the stored outcome\n * transitions undecided -> approved | rejected once, then replay-only\n * crash/reload the outcome is durable before the caller sees it, so a\n * phone that never saw its response retries and is\n * answered from the store, on any process\n * cleanup owner the relay transaction; nothing extra is allocated here\n * ```\n *\n * A second decide answers the *stored* outcome, not the resubmitted command: if\n * a rejected request is retried as approved, the answer is `rejected`. Compare\n * `outcome` against what you sent when that matters.\n *\n * The one-time code and encrypted artifact are released to the deciding call\n * only. A replay reports the outcome and no release material, because releasing\n * it twice is exactly what one-shot semantics forbid.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport type {\n AuthorizationDecisionCommand,\n SubmittedAuthorizationDecision,\n} from \"../authorization/decision.js\";\nimport { submitAuthorizationDecision } from \"../authorization/decision.js\";\nimport { fetchAuthorizationRequest } from \"../authorization/request.js\";\nimport type { RelayClock } from \"../clock.js\";\nimport { OaathRelayError, relayFailure } from \"../relay/errors.js\";\nimport type { RelayCaller } from \"../security/authentication.js\";\nimport type { RelayKms } from \"../security/kms.js\";\nimport type { RelayStore } from \"../store/interface.js\";\nimport type { AuthorizationDecisionOutcome } from \"../store/records.js\";\n\nexport interface SubmitOwnerPhoneDecisionInput {\n readonly store: RelayStore;\n readonly clock: RelayClock;\n readonly kms: RelayKms;\n /** Authenticated `owner` caller; only the bound subject may decide. */\n readonly caller: RelayCaller;\n /** `operationId` from `projectOwnerPhoneRequest`. */\n readonly operationId: string;\n readonly command: AuthorizationDecisionCommand;\n readonly codeTtlMs: number;\n}\n\nexport interface OwnerPhoneDecision {\n readonly operationId: string;\n /** The stored outcome, which on a replay may differ from the command sent. */\n readonly outcome: AuthorizationDecisionOutcome;\n readonly decidedAt: number;\n /** `decided` performed the transition; `replayed` answered the stored one. */\n readonly settlement: \"decided\" | \"replayed\";\n /** One-time release material, on the deciding call only. */\n readonly release: SubmittedAuthorizationDecision | null;\n}\n\nexport async function submitOwnerPhoneDecision(\n input: SubmitOwnerPhoneDecisionInput,\n): Promise<OwnerPhoneDecision> {\n if (input.caller.role !== \"owner\") {\n return relayFailure(\"relay_forbidden\", \"caller may not act in the required role\");\n }\n try {\n const release = await submitAuthorizationDecision({\n store: input.store,\n clock: input.clock,\n kms: input.kms,\n caller: input.caller,\n requestId: input.operationId,\n command: input.command,\n codeTtlMs: input.codeTtlMs,\n });\n return Object.freeze({\n operationId: input.operationId,\n outcome: release.outcome,\n decidedAt: release.decidedAt,\n settlement: \"decided\",\n release,\n });\n } catch (error) {\n // Only \"already decided\" is idempotent. Ambiguous, expired, unauthorized,\n // and unreadable stay failures: an unproven commit never becomes an answer.\n if (!(error instanceof OaathRelayError) || error.code !== \"relay_already_decided\") throw error;\n }\n const state = await fetchAuthorizationRequest({\n store: input.store,\n clock: input.clock,\n caller: input.caller,\n requestId: input.operationId,\n });\n if (state.decision === null) {\n return relayFailure(\"relay_internal\", \"decided request has no decision record\");\n }\n return Object.freeze({\n operationId: input.operationId,\n outcome: state.decision.outcome,\n decidedAt: state.decision.decidedAt,\n settlement: \"replayed\",\n release: null,\n });\n}\n","/**\n * EXPERIMENTAL PREVIEW — authenticated owner-phone consent projection.\n *\n * This is a preview surface reached through `@oaath/server/native` and the\n * relay's preview HTTP routes (`GET /native/projections/{operationId}`). It has\n * no stability guarantee and no production qualification: real iOS, Apple\n * provisioning, Secure Enclave, and hosted operations are later work.\n *\n * What it owns: turning one stored authorization request into the projection\n * the owner's phone renders before deciding. It owns nothing about deciding;\n * the relay's decision use case stays the only decision owner.\n *\n * Two very different surfaces share this file, and only one of them is opaque:\n *\n * - The **push** projection (`operationId`, `displayPayload`, `expiresAt`)\n * transits Apple inside an APNs payload, so it stays opaque by design: no\n * permission, scope, client, chain, or account detail may ride it. The APNs\n * sender enforces that subset exactly.\n * - The **consent** projection returned here travels only the authenticated\n * relay → owner channel, so it deliberately carries the full authorization\n * request: the requesting client identity and the requested scope. The owner\n * must see exactly the authority they grant before approving.\n *\n * `displayPayload` is not a secret and not authority: it is a deterministic\n * digest of the request and its bound subject, so the owner can check that the\n * code on the phone is the code the browser shows. Holding it authorizes\n * nothing — deciding still requires the authenticated bound owner.\n *\n * @author taek <leekt216@gmail.com>\n */\n\nimport {\n hashOwnerSigningRequest,\n type KernelV4ReplayableInstallOwnerSigningRequest,\n type OwnerSigningRequest,\n} from \"@oaath/protocol\";\nimport { sha256Base64Url } from \"../authorization/challenge.js\";\nimport { fetchAuthorizationRequest } from \"../authorization/request.js\";\nimport { classifyStoredAuthorizationScope } from \"../authorization/scope.js\";\nimport type { RelayClock } from \"../clock.js\";\nimport { relayFailure } from \"../relay/errors.js\";\nimport type { RelayCaller } from \"../security/authentication.js\";\nimport type { RelayStore } from \"../store/interface.js\";\n\n/** Bounded base64url match code length. 48 bits is plenty to compare by eye. */\nexport const NATIVE_DISPLAY_PAYLOAD_LENGTH = 8;\n\n/** Versioned consent envelope; the Swift decoder pins this exact value. */\nexport const OAATH_NATIVE_PROJECTION_VERSION = \"oaath.native-projection/v4\" as const;\n\nconst DISPLAY_DOMAIN = \"oaath.native-display/v1:\";\n\n/**\n * Whether the phone may offer approval for one projected scope. Permission\n * requests and exact Kernel replayable-install P-256 signing are approvable;\n * every other owner-signing, unknown, and malformed scope remains reject-only.\n */\nexport type OwnerPhoneDecisionCapability = \"approve-or-reject\" | \"reject-only\";\n\n/** One credential identity as the owner reviews it: public material only. */\nexport type OwnerPhoneCredentialProjection =\n | Readonly<{ kind: \"ecdsa\"; address: string }>\n | Readonly<{ kind: \"p256\"; publicKey: string }>\n | Readonly<{ kind: \"webauthn\"; publicKey: string; authenticatorIdHash: string }>;\n\n/**\n * The requested scope as the phone renders it. When the stored scope parses as\n * an `@oaath/protocol` permission request, every fact that determines who\n * receives authority, over which account, and under what limits is projected\n * structurally. A valid owner-signing request is also projected in full with\n * its protocol-owned hash; only exact Kernel replayable-install P-256 signing\n * is approvable. Anything else is returned as explicitly labeled raw text.\n */\nexport type OwnerPhoneScopeProjection =\n | Readonly<{\n kind: \"permission-request\";\n decision: \"approve-or-reject\";\n /** The application identity the signed request binds, verbatim. */\n application: Readonly<{\n applicationId: string;\n clientId: string;\n origin: string;\n /** Opaque device identity, fingerprinted: raw ids mean nothing to an owner. */\n deviceFingerprint: string;\n }>;\n /** The logical account this authority acts for. */\n account: Readonly<{\n accountIndex: string;\n kernelVersion: string;\n factoryRoute: string;\n entryPointVersion: string;\n ownerCredential: OwnerPhoneCredentialProjection;\n }>;\n /** The session credential that receives the scoped authority. */\n operatorCredential: OwnerPhoneCredentialProjection;\n /**\n * Where the session key lives: null for frontend custody (the page's\n * own non-extractable key), or the remote trust model the owner is\n * asked to approve — the request hash binds it, so approving this\n * display approves exactly this custody.\n */\n sessionSigner: Readonly<{\n mode: \"application_backend\" | \"oaath_hosted\";\n providerId: string;\n }> | null;\n chainScope: \"all\";\n calls: readonly Readonly<{\n target: string;\n selector: string;\n valueLimit: string;\n argumentEquals: readonly Readonly<{ index: number; value: string }>[];\n }>[];\n requestedAt: number;\n /** The permission request's own expiry, as the requesting client stated it. */\n expiresAt: number;\n /** The policy's on-chain validity window; inclusive, in Unix seconds. */\n policyValidAfter: number;\n policyValidUntil: number | null;\n perChainOperationLimit: number;\n }>\n | Readonly<{\n kind: \"owner-signing-request\";\n decision: \"approve-or-reject\";\n /** Canonical hash binding every captured request fact. */\n requestHash: `0x${string}`;\n /** Exact Kernel request independently refined by the protocol owner. */\n request: Readonly<KernelV4ReplayableInstallOwnerSigningRequest>;\n }>\n | Readonly<{\n kind: \"owner-signing-request\";\n decision: \"reject-only\";\n /** Canonical hash binding every captured request fact. */\n requestHash: `0x${string}`;\n /** Full immutable protocol request for independent device review. */\n request: Readonly<OwnerSigningRequest>;\n }>\n | Readonly<{ kind: \"raw\"; decision: \"reject-only\"; text: string }>;\n\nexport interface OwnerPhoneRequestProjection {\n readonly version: typeof OAATH_NATIVE_PROJECTION_VERSION;\n /**\n * Stable across every projection of the same request, and the key the\n * approve/reject saga is keyed by. It is the relay's own opaque request\n * identifier: one authoritative id, no second mapping table to lose.\n */\n readonly operationId: string;\n /** Bounded opaque match code, the only variable an APNs payload may carry. */\n readonly displayPayload: string;\n /** The stored request's expiry, in epoch milliseconds. */\n readonly expiresAt: number;\n /** The requesting client, exactly as the stored request binds it. */\n readonly client: Readonly<{ clientId: string; redirectUri: string }>;\n readonly scope: OwnerPhoneScopeProjection;\n}\n\n/**\n * The opaque subset of the projection that may transit Apple inside an APNs\n * payload. The sender exact-captures precisely these three fields, so consent\n * detail can never ride a notification.\n */\nexport type OwnerPhonePushProjection = Pick<\n OwnerPhoneRequestProjection,\n \"operationId\" | \"displayPayload\" | \"expiresAt\"\n>;\n\nexport interface ProjectOwnerPhoneRequestInput {\n readonly store: RelayStore;\n readonly clock: RelayClock;\n /** Authenticated `owner` caller; only the bound subject may be projected. */\n readonly caller: RelayCaller;\n readonly requestId: string;\n}\n\n/**\n * The stored scope is the requesting client's own JSON permission scope (the\n * SDK serializes a permission request without its `requestId`; the relay's\n * request id is the binding). An unparseable scope is not a failure: the owner\n * still gets the raw text to review, never a silent omission.\n */\nfunction projectCredential(\n credential: Readonly<{\n kind: \"ecdsa\" | \"p256\" | \"webauthn\";\n address?: string;\n publicKey?: string;\n authenticatorIdHash?: string;\n }>,\n): OwnerPhoneCredentialProjection {\n if (credential.kind === \"ecdsa\") {\n return Object.freeze({ kind: \"ecdsa\", address: credential.address ?? \"\" });\n }\n if (credential.kind === \"p256\") {\n return Object.freeze({ kind: \"p256\", publicKey: credential.publicKey ?? \"\" });\n }\n return Object.freeze({\n kind: \"webauthn\",\n publicKey: credential.publicKey ?? \"\",\n authenticatorIdHash: credential.authenticatorIdHash ?? \"\",\n });\n}\n\nexport async function projectOwnerPhoneScope(\n requestedScope: string,\n operationId: string,\n): Promise<OwnerPhoneScopeProjection> {\n try {\n const classified = classifyStoredAuthorizationScope(requestedScope, operationId);\n if (classified.kind === \"permission-request\") {\n const request = classified.request;\n return Object.freeze({\n kind: \"permission-request\",\n decision: \"approve-or-reject\",\n application: Object.freeze({\n applicationId: request.application.applicationId,\n clientId: request.application.clientId,\n origin: request.application.origin,\n deviceFingerprint: (\n await sha256Base64Url(`${DISPLAY_DOMAIN}device:${request.application.deviceId}`)\n ).slice(0, NATIVE_DISPLAY_PAYLOAD_LENGTH),\n }),\n account: Object.freeze({\n accountIndex: request.logicalAccount.accountIndex,\n kernelVersion: request.logicalAccount.kernelVersion,\n factoryRoute: request.logicalAccount.factoryRoute,\n entryPointVersion: request.logicalAccount.entryPoint.version,\n ownerCredential: projectCredential(request.logicalAccount.ownerCredential),\n }),\n operatorCredential: projectCredential(request.operatorCredential),\n sessionSigner:\n request.sessionSigner === null\n ? null\n : Object.freeze({\n mode: request.sessionSigner.mode,\n providerId: request.sessionSigner.providerId,\n }),\n chainScope: request.chainScope,\n calls: Object.freeze(\n request.policy.calls.map((call) =>\n Object.freeze({\n target: call.target,\n selector: call.selector,\n valueLimit: call.valueLimit,\n argumentEquals: Object.freeze(\n call.argumentEquals.map((rule) =>\n Object.freeze({ index: rule.index, value: rule.value }),\n ),\n ),\n }),\n ),\n ),\n requestedAt: request.requestedAt,\n expiresAt: request.expiresAt,\n policyValidAfter: request.policy.validAfter,\n policyValidUntil: request.policy.validUntil,\n perChainOperationLimit: request.policy.perChainOperationLimit,\n });\n }\n if (classified.kind === \"kernel-owner-signing-request\") {\n return Object.freeze({\n kind: \"owner-signing-request\",\n decision: \"approve-or-reject\",\n requestHash: hashOwnerSigningRequest(classified.request),\n request: classified.request,\n });\n }\n if (classified.kind === \"owner-signing-request\") {\n return Object.freeze({\n kind: \"owner-signing-request\",\n decision: \"reject-only\",\n requestHash: hashOwnerSigningRequest(classified.request),\n request: classified.request,\n });\n }\n return Object.freeze({ kind: \"raw\", decision: \"reject-only\", text: requestedScope });\n } catch {\n return Object.freeze({ kind: \"raw\", decision: \"reject-only\", text: requestedScope });\n }\n}\n\n/**\n * Projects a pending request for its bound owner. An expired or already decided\n * request is not projectable: there is nothing left to approve, so nothing is\n * ever pushed or rendered for it.\n */\nexport async function projectOwnerPhoneRequest(\n input: ProjectOwnerPhoneRequestInput,\n): Promise<OwnerPhoneRequestProjection> {\n if (input.caller.role !== \"owner\") {\n return relayFailure(\"relay_forbidden\", \"caller may not act in the required role\");\n }\n const state = await fetchAuthorizationRequest({\n store: input.store,\n clock: input.clock,\n caller: input.caller,\n requestId: input.requestId,\n });\n if (state.expired) {\n return relayFailure(\"relay_expired\", \"authorization request expired\");\n }\n if (state.decision !== null) {\n return relayFailure(\"relay_already_decided\", \"authorization request is already decided\");\n }\n const digest = await sha256Base64Url(\n `${DISPLAY_DOMAIN}${input.caller.subject}:${state.requestId}`,\n );\n return Object.freeze({\n version: OAATH_NATIVE_PROJECTION_VERSION,\n operationId: state.requestId,\n displayPayload: digest.slice(0, NATIVE_DISPLAY_PAYLOAD_LENGTH),\n expiresAt: state.expiresAt,\n client: Object.freeze({ clientId: state.clientId, redirectUri: state.redirectUri }),\n scope: await projectOwnerPhoneScope(state.requestedScope, state.requestId),\n });\n}\n"],"mappings":"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"}
|