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