@oaath/server 0.1.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/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/apns.d.ts +172 -0
- package/dist/apns.d.ts.map +1 -0
- package/dist/apns.js +488 -0
- package/dist/apns.js.map +1 -0
- package/dist/authentication-HUYWk5Y2.d.ts +46 -0
- package/dist/authentication-HUYWk5Y2.d.ts.map +1 -0
- package/dist/challenge-AGTmUfj2.js +71 -0
- package/dist/challenge-AGTmUfj2.js.map +1 -0
- package/dist/decision-dEe9n108.d.ts +53 -0
- package/dist/decision-dEe9n108.d.ts.map +1 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1135 -0
- package/dist/index.js.map +1 -0
- package/dist/interface-WwvwWs8e.d.ts +114 -0
- package/dist/interface-WwvwWs8e.d.ts.map +1 -0
- package/dist/native.d.ts +30 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +2 -0
- package/dist/postgres.d.ts +37 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +401 -0
- package/dist/postgres.js.map +1 -0
- package/dist/projection-dGIkE0rI.js +516 -0
- package/dist/projection-dGIkE0rI.js.map +1 -0
- package/dist/projection-g6k_CCUK.d.ts +136 -0
- package/dist/projection-g6k_CCUK.d.ts.map +1 -0
- package/dist/records-Bgr-9PUh.js +231 -0
- package/dist/records-Bgr-9PUh.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
import { _ as jsonResponse, a as OAATH_ENCRYPTED_ARTIFACT_RECORD_VERSION, b as relayFailure, c as canonicalIdentifier, d as parseAuthorizationRequestRecord, f as parseCapabilityInvalidationRecord, g as RELAY_ERROR_STATUS, h as OaathRelayError, i as OAATH_CAPABILITY_INVALIDATION_RECORD_VERSION, l as parseAuthorizationCodeRecord, m as timestamp, n as OAATH_AUTHORIZATION_DECISION_RECORD_VERSION, o as RELAY_LIMITS, p as parseEncryptedArtifactRecord, r as OAATH_AUTHORIZATION_REQUEST_RECORD_VERSION, s as boundedText, t as OAATH_AUTHORIZATION_CODE_RECORD_VERSION, u as parseAuthorizationDecisionRecord, v as relayErrorCode, y as relayErrorResponse } from "./records-Bgr-9PUh.js";
|
|
2
|
+
import { a as relayNow, i as verifyPkceS256, r as sha256Base64Url, t as isCodeChallengeS256 } from "./challenge-AGTmUfj2.js";
|
|
3
|
+
import { a as submitAuthorizationDecision, c as readAuthorizationState, d as withRelayTransaction, i as submitOwnerPhoneDecision, l as openArtifact, o as createAuthorizationRequest, r as projectOwnerPhoneRequest, s as fetchAuthorizationRequest, u as sealArtifact } from "./projection-dGIkE0rI.js";
|
|
4
|
+
import { OAATH_OPERATOR_CREDENTIAL_PROFILE_VERSION, OAATH_SERVICE_BOOTSTRAP_VERSION, captureDenseArray, captureRecord, exactCapturedRecord, exactRecord, parseServiceBootstrap } from "@oaath/protocol";
|
|
5
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
6
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
7
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
8
|
+
//#region src/artifact/claim.ts
|
|
9
|
+
/**
|
|
10
|
+
* One-time encrypted artifact release.
|
|
11
|
+
*
|
|
12
|
+
* ```text
|
|
13
|
+
* state and owner the artifact record's `claimedAt` owns "released"
|
|
14
|
+
* transitions sealed -> claimed, exactly once
|
|
15
|
+
* terminal claimed; a second claim fails closed
|
|
16
|
+
* retry positively safe? no. The claim commits before the KMS is asked to
|
|
17
|
+
* decrypt, so a decrypt or transport failure burns the
|
|
18
|
+
* artifact instead of risking a second release
|
|
19
|
+
* crash/reload a crash before commit leaves the artifact claimable
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @author taek <leekt216@gmail.com>
|
|
23
|
+
*/
|
|
24
|
+
async function claimEncryptedArtifact(input) {
|
|
25
|
+
const claimedAt = relayNow(input.clock);
|
|
26
|
+
const claimed = await withRelayTransaction(input.store, async (transaction) => {
|
|
27
|
+
const record = await transaction.lockEncryptedArtifact(input.artifactId);
|
|
28
|
+
if (!record || record.clientId !== input.caller.clientId) return relayFailure("relay_not_found", "encrypted artifact does not exist");
|
|
29
|
+
if (record.claimedAt !== null) relayFailure("relay_artifact_already_claimed", "encrypted artifact was already claimed");
|
|
30
|
+
if (!await transaction.claimEncryptedArtifact(input.artifactId, claimedAt)) relayFailure("relay_state_ambiguous", "one-time claim did not apply under its row lock");
|
|
31
|
+
return record;
|
|
32
|
+
});
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
requestId: claimed.requestId,
|
|
35
|
+
artifact: await openArtifact(input.kms, claimed.ciphertextRef)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/authorization/code.ts
|
|
40
|
+
/**
|
|
41
|
+
* One-time authorization code consume.
|
|
42
|
+
*
|
|
43
|
+
* ```text
|
|
44
|
+
* state and owner the code record's `consumedAt` owns "used"
|
|
45
|
+
* transitions issued -> consumed, exactly once
|
|
46
|
+
* terminal consumed; a second consume fails closed
|
|
47
|
+
* retry positively safe? no, in either direction: a released code is never
|
|
48
|
+
* re-released, and an ambiguous commit is never retried
|
|
49
|
+
* crash/reload the guarded update and the release decide together in one
|
|
50
|
+
* transaction under the code's row lock
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* A failed PKCE or redirect binding still burns the code, so a stolen code
|
|
54
|
+
* cannot be brute-forced against the stored challenge.
|
|
55
|
+
*
|
|
56
|
+
* The endpoint is not an oracle for whether a guessed code was correct: an
|
|
57
|
+
* unknown code, a code bound to another client, a wrong redirect URI, and a
|
|
58
|
+
* wrong verifier all leave as the single `relay_code_invalid` code and status.
|
|
59
|
+
*
|
|
60
|
+
* @author taek <leekt216@gmail.com>
|
|
61
|
+
*/
|
|
62
|
+
async function consumeAuthorizationCode(input) {
|
|
63
|
+
const consumedAt = relayNow(input.clock);
|
|
64
|
+
const codeHash = await sha256Base64Url(input.code);
|
|
65
|
+
const transaction = await input.store.begin();
|
|
66
|
+
let outcome;
|
|
67
|
+
try {
|
|
68
|
+
const record = await transaction.lockAuthorizationCode(codeHash);
|
|
69
|
+
if (!record || record.clientId !== input.caller.clientId) relayFailure("relay_code_invalid", "authorization code redemption failed");
|
|
70
|
+
if (record.consumedAt !== null) relayFailure("relay_code_already_consumed", "authorization code was already consumed");
|
|
71
|
+
if (consumedAt >= record.expiresAt) relayFailure("relay_expired", "authorization code expired");
|
|
72
|
+
const bound = record.redirectUri === input.redirectUri && await verifyPkceS256(input.codeVerifier, record.codeChallenge);
|
|
73
|
+
if (!await transaction.consumeAuthorizationCode(codeHash, consumedAt)) relayFailure("relay_state_ambiguous", "one-time consume did not apply under its row lock");
|
|
74
|
+
if (!bound) {
|
|
75
|
+
await transaction.lockEncryptedArtifact(record.artifactId);
|
|
76
|
+
await transaction.claimEncryptedArtifact(record.artifactId, consumedAt);
|
|
77
|
+
}
|
|
78
|
+
outcome = bound ? Object.freeze({
|
|
79
|
+
kind: "released",
|
|
80
|
+
released: Object.freeze({
|
|
81
|
+
requestId: record.requestId,
|
|
82
|
+
artifactId: record.artifactId
|
|
83
|
+
})
|
|
84
|
+
}) : Object.freeze({
|
|
85
|
+
kind: "burned",
|
|
86
|
+
code: "relay_code_invalid"
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
await transaction.rollback();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
await transaction.commit();
|
|
93
|
+
if (outcome.kind === "burned") relayFailure(outcome.code, "authorization code redemption failed and the code was burned");
|
|
94
|
+
return outcome.released;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/authorization/resume.ts
|
|
98
|
+
/**
|
|
99
|
+
* Resume: fresh authentication plus a grant recovery read.
|
|
100
|
+
*
|
|
101
|
+
* Resume never transitions anything and never re-releases a code or an artifact.
|
|
102
|
+
* The caller re-authenticates on every call, and receives only enough state to
|
|
103
|
+
* decide whether its own grant work is still valid.
|
|
104
|
+
*
|
|
105
|
+
* @author taek <leekt216@gmail.com>
|
|
106
|
+
*/
|
|
107
|
+
function resumeAuthorization(input) {
|
|
108
|
+
const now = relayNow(input.clock);
|
|
109
|
+
return withRelayTransaction(input.store, (transaction) => readAuthorizationState(transaction, input.requestId, now, (request) => request.clientId === input.caller.clientId));
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/authorization/pickup.ts
|
|
113
|
+
/**
|
|
114
|
+
* Authenticated client pickup of a released authorization code.
|
|
115
|
+
*
|
|
116
|
+
* ```text
|
|
117
|
+
* state and owner reads the request and decision records; writes nothing
|
|
118
|
+
* transitions none — pickup is idempotent
|
|
119
|
+
* terminal follows the decision record
|
|
120
|
+
* retry positively safe? yes: the code stays one-shot at consumption, where the
|
|
121
|
+
* PKCE verifier and the code hash lock decide
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* The relay mints the code, so releasing the sealed copy to the authenticated
|
|
125
|
+
* creating client adds no authority: consumption still requires the verifier
|
|
126
|
+
* only that client holds. The request's stored clientId owns who may pick up;
|
|
127
|
+
* any other caller reads absence, never existence.
|
|
128
|
+
*
|
|
129
|
+
* @author taek <leekt216@gmail.com>
|
|
130
|
+
*/
|
|
131
|
+
async function fetchAuthorizationCode(input) {
|
|
132
|
+
const now = relayNow(input.clock);
|
|
133
|
+
const decided = await withRelayTransaction(input.store, async (transaction) => {
|
|
134
|
+
const request = await transaction.lockAuthorizationRequest(input.requestId);
|
|
135
|
+
if (!request || request.clientId !== input.caller.clientId) return relayFailure("relay_not_found", "authorization request does not exist");
|
|
136
|
+
const decision = await transaction.lockAuthorizationDecision(input.requestId);
|
|
137
|
+
if (!decision) {
|
|
138
|
+
if (now >= request.expiresAt) return relayFailure("relay_expired", "authorization request expired");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return decision;
|
|
142
|
+
});
|
|
143
|
+
if (decided === null) return Object.freeze({ outcome: "pending" });
|
|
144
|
+
if (decided.outcome === "rejected" || decided.codeRef === null) return Object.freeze({
|
|
145
|
+
outcome: "rejected",
|
|
146
|
+
decidedAt: decided.decidedAt
|
|
147
|
+
});
|
|
148
|
+
const codeExpiresAt = decided.codeExpiresAt ?? 0;
|
|
149
|
+
if (now >= codeExpiresAt) return relayFailure("relay_expired", "authorization code expired");
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
outcome: "approved",
|
|
152
|
+
decidedAt: decided.decidedAt,
|
|
153
|
+
code: await openArtifact(input.kms, decided.codeRef),
|
|
154
|
+
codeExpiresAt
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/security/authentication.ts
|
|
159
|
+
/**
|
|
160
|
+
* Deployment-owned client/device authentication port.
|
|
161
|
+
*
|
|
162
|
+
* The relay never parses credentials. It hands the request to the deployment,
|
|
163
|
+
* which returns the authenticated bindings or nothing. `clientId` and `subject`
|
|
164
|
+
* come only from here: wire input may never declare either.
|
|
165
|
+
*
|
|
166
|
+
* @author taek <leekt216@gmail.com>
|
|
167
|
+
*/
|
|
168
|
+
const MAX_REDIRECT_URIS = 8;
|
|
169
|
+
function captureCaller(value) {
|
|
170
|
+
const context = /* @__PURE__ */ new WeakSet();
|
|
171
|
+
const fail = (message) => relayFailure("relay_internal", message);
|
|
172
|
+
const record = exactRecord(value, [
|
|
173
|
+
"role",
|
|
174
|
+
"clientId",
|
|
175
|
+
"subject",
|
|
176
|
+
"redirectUris"
|
|
177
|
+
], "authenticated caller", context, fail);
|
|
178
|
+
if (record.role !== "client" && record.role !== "owner") return relayFailure("relay_internal", "authenticated caller role is unsupported");
|
|
179
|
+
const redirectUris = captureDenseArray(record.redirectUris, "caller redirectUris", context, fail);
|
|
180
|
+
if (redirectUris.length > MAX_REDIRECT_URIS) return relayFailure("relay_internal", "caller redirectUris is unbounded");
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
role: record.role,
|
|
183
|
+
clientId: canonicalIdentifier(record.clientId, "caller clientId", "relay_internal"),
|
|
184
|
+
subject: canonicalIdentifier(record.subject, "caller subject", "relay_internal"),
|
|
185
|
+
redirectUris: Object.freeze(redirectUris.map((uri) => boundedText(uri, RELAY_LIMITS.redirectUri, "caller redirectUri", "relay_internal")))
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
async function authenticateCaller(authentication, request, required) {
|
|
189
|
+
let authenticated;
|
|
190
|
+
try {
|
|
191
|
+
authenticated = await authentication.authenticate(request);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof OaathRelayError) throw error;
|
|
194
|
+
return relayFailure("relay_unauthenticated", "authentication port failed");
|
|
195
|
+
}
|
|
196
|
+
if (authenticated === null || authenticated === void 0) return relayFailure("relay_unauthenticated", "caller is not authenticated");
|
|
197
|
+
const caller = captureCaller(authenticated);
|
|
198
|
+
if (!(typeof required === "string" ? [required] : required).includes(caller.role)) return relayFailure("relay_forbidden", "caller may not act in the required role");
|
|
199
|
+
return caller;
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/security/rate-limit.ts
|
|
203
|
+
/**
|
|
204
|
+
* Optional deployment-owned limiter port.
|
|
205
|
+
*
|
|
206
|
+
* There is no default limiter: an unset port means no limiting, and the relay
|
|
207
|
+
* never invents a policy the deployment did not choose. A limiter that fails is
|
|
208
|
+
* treated as `limited`, because an unreadable limiter is not permission.
|
|
209
|
+
*
|
|
210
|
+
* @author taek <leekt216@gmail.com>
|
|
211
|
+
*/
|
|
212
|
+
async function assertWithinRateLimit(limiter, input) {
|
|
213
|
+
if (!limiter) return;
|
|
214
|
+
let verdict;
|
|
215
|
+
try {
|
|
216
|
+
verdict = await limiter.check(input);
|
|
217
|
+
} catch {
|
|
218
|
+
return relayFailure("relay_rate_limited", "rate limiter is unreadable");
|
|
219
|
+
}
|
|
220
|
+
if (verdict !== "allowed") return relayFailure("relay_rate_limited", "rate limiter rejected the call");
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/relay/handler.ts
|
|
224
|
+
/**
|
|
225
|
+
* Fetch-standard relay handler.
|
|
226
|
+
*
|
|
227
|
+
* Every wire input is exact-captured once here and handed to a use case as typed
|
|
228
|
+
* data. Every failure leaves as a structured code projected to a status; no
|
|
229
|
+
* message text, driver output, or internal detail reaches a response body.
|
|
230
|
+
*
|
|
231
|
+
* Endpoints:
|
|
232
|
+
*
|
|
233
|
+
* ```text
|
|
234
|
+
* POST /authorization/requests client create request
|
|
235
|
+
* GET /authorization/requests/{requestId} owner fetch request
|
|
236
|
+
* POST /authorization/requests/{requestId}/decision owner approve or reject
|
|
237
|
+
* GET /authorization/requests/{requestId}/code client released-code pickup
|
|
238
|
+
* POST /authorization/codes/consume client one-time code consume
|
|
239
|
+
* POST /authorization/artifacts/{artifactId}/claim client one-time artifact claim
|
|
240
|
+
* POST /authorization/resume client fresh auth + recovery read
|
|
241
|
+
* GET /bootstrap client URL-only service context
|
|
242
|
+
* POST /invalidations client capability invalidation
|
|
243
|
+
* POST /chains/{chainId}/{port} client chain execution relay,
|
|
244
|
+
* port in reads |
|
|
245
|
+
* observation | bundler |
|
|
246
|
+
* quote | submissions | usage
|
|
247
|
+
* POST /chains/{chainId}/paymaster/{method} client registered paymaster proxy,
|
|
248
|
+
* method in stub-data | data
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* EXPERIMENTAL PREVIEW routes for the owner-phone approval surface. Preview
|
|
252
|
+
* means: no stability guarantee and no production qualification. Their wire
|
|
253
|
+
* shapes are pinned field-for-field by the strict Swift decoders in
|
|
254
|
+
* `native/ios/Sources/OwnerPhone/{Projection,Decision}.swift`.
|
|
255
|
+
*
|
|
256
|
+
* ```text
|
|
257
|
+
* GET /native/projections/{operationId} owner consent projection
|
|
258
|
+
* POST /native/decisions/{operationId} owner approve or reject saga
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @author taek <leekt216@gmail.com>
|
|
262
|
+
*/
|
|
263
|
+
const DEFAULT_REQUEST_TTL_MS = 3e5;
|
|
264
|
+
const DEFAULT_CODE_TTL_MS = 6e4;
|
|
265
|
+
const DEFAULT_MAX_BODY_BYTES = 65536;
|
|
266
|
+
const MAX_TTL_MS = 864e5;
|
|
267
|
+
/** RFC 6749 ceiling, matching `MAX_AUTHORIZATION_CODE_LIFETIME` in @oaath/protocol. */
|
|
268
|
+
const MAX_CODE_TTL_MS = 6e5;
|
|
269
|
+
const MAX_PAYMASTER_REQUEST_TIMEOUT_MS = 3e4;
|
|
270
|
+
const HASH$1 = /^0x[0-9a-f]{64}$/u;
|
|
271
|
+
const INVALID = "relay_request_invalid";
|
|
272
|
+
const OPTION_KEYS = [
|
|
273
|
+
"store",
|
|
274
|
+
"authentication",
|
|
275
|
+
"kms",
|
|
276
|
+
"clock",
|
|
277
|
+
"rateLimit",
|
|
278
|
+
"requestTtlMs",
|
|
279
|
+
"codeTtlMs",
|
|
280
|
+
"maxBodyBytes",
|
|
281
|
+
"bootstrap",
|
|
282
|
+
"chains",
|
|
283
|
+
"sessionSigner",
|
|
284
|
+
"paymasterServices"
|
|
285
|
+
];
|
|
286
|
+
const CHAIN_PORT_NAMES = [
|
|
287
|
+
"reads",
|
|
288
|
+
"observation",
|
|
289
|
+
"bundler",
|
|
290
|
+
"quote",
|
|
291
|
+
"submission"
|
|
292
|
+
];
|
|
293
|
+
/**
|
|
294
|
+
* The Grant a chain port request acts for, where the port's contract names
|
|
295
|
+
* one: a submission's prepared operation and a usage read both carry it.
|
|
296
|
+
* Reading it here is bounded extraction for the invalidation gate, not
|
|
297
|
+
* interpretation — the port's own owner still captures the payload exactly.
|
|
298
|
+
*/
|
|
299
|
+
function chainRequestGrantId(name, request) {
|
|
300
|
+
try {
|
|
301
|
+
if (name === "usage") {
|
|
302
|
+
const grantId = request?.grantId;
|
|
303
|
+
return typeof grantId === "string" && grantId.length > 0 ? grantId : null;
|
|
304
|
+
}
|
|
305
|
+
if (name === "submission") {
|
|
306
|
+
const grantId = (request?.prepared)?.grantId;
|
|
307
|
+
return typeof grantId === "string" && grantId.length > 0 ? grantId : null;
|
|
308
|
+
}
|
|
309
|
+
} catch {}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Ports may be class instances, so only their required capabilities are checked.
|
|
314
|
+
* Narrowing follows the runtime check.
|
|
315
|
+
*/
|
|
316
|
+
function requirePort(value, methods, label) {
|
|
317
|
+
if (!value || typeof value !== "object") return relayFailure("relay_internal", `${label} must be an object`);
|
|
318
|
+
for (const method of methods) if (typeof value[method] !== "function") return relayFailure("relay_internal", `${label} must implement ${method}()`);
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
function duration(value, fallback, label, maximum = MAX_TTL_MS) {
|
|
322
|
+
if (value === void 0) return fallback;
|
|
323
|
+
const milliseconds = timestamp(value, label, "relay_internal");
|
|
324
|
+
if (milliseconds < 1 || milliseconds > maximum) return relayFailure("relay_internal", `${label} must be a bounded positive duration`);
|
|
325
|
+
return milliseconds;
|
|
326
|
+
}
|
|
327
|
+
function captureChainPorts(value) {
|
|
328
|
+
if (!Array.isArray(value)) return relayFailure("relay_internal", "chains must be an array of chain ports");
|
|
329
|
+
const chains = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const entry of value) {
|
|
331
|
+
const port = requirePort(entry, [...CHAIN_PORT_NAMES], "chain port");
|
|
332
|
+
if (typeof port.chainId !== "number" || !Number.isSafeInteger(port.chainId) || port.chainId < 1) return relayFailure("relay_internal", "chain port chainId must be a positive integer");
|
|
333
|
+
if (port.usage !== null && typeof port.usage !== "function") return relayFailure("relay_internal", "chain port usage must be a function or null");
|
|
334
|
+
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
|
+
if (chains.has(port.chainId)) return relayFailure("relay_internal", "chain ports repeat a chainId");
|
|
336
|
+
chains.set(port.chainId, port);
|
|
337
|
+
}
|
|
338
|
+
return chains;
|
|
339
|
+
}
|
|
340
|
+
function capturePaymasterServices(value, context, chains) {
|
|
341
|
+
if (value === void 0) return /* @__PURE__ */ new Map();
|
|
342
|
+
const entries = captureDenseArray(value, "paymaster services", context, (message) => relayFailure("relay_internal", message));
|
|
343
|
+
const services = /* @__PURE__ */ new Map();
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
const service = exactCapturedRecord(captureRecord(entry, "paymaster service", context, (message) => relayFailure("relay_internal", message)), [
|
|
346
|
+
"chainId",
|
|
347
|
+
"providerId",
|
|
348
|
+
"requestTimeoutMs",
|
|
349
|
+
"provider"
|
|
350
|
+
], "paymaster service", (message) => relayFailure("relay_internal", message));
|
|
351
|
+
const chainId = service.chainId;
|
|
352
|
+
if (typeof chainId !== "number" || !Number.isSafeInteger(chainId) || chainId < 1) relayFailure("relay_internal", "paymaster service chainId must be a positive integer");
|
|
353
|
+
if (!chains.has(chainId)) relayFailure("relay_internal", "paymaster service chain is not configured");
|
|
354
|
+
if (services.has(chainId)) relayFailure("relay_internal", "paymaster services repeat a chainId");
|
|
355
|
+
if (service.requestTimeoutMs === void 0) relayFailure("relay_internal", "paymaster requestTimeoutMs is required");
|
|
356
|
+
services.set(chainId, Object.freeze({
|
|
357
|
+
chainId,
|
|
358
|
+
providerId: canonicalIdentifier(service.providerId, "paymaster providerId", "relay_internal"),
|
|
359
|
+
requestTimeoutMs: duration(service.requestTimeoutMs, 0, "paymaster requestTimeoutMs", MAX_PAYMASTER_REQUEST_TIMEOUT_MS),
|
|
360
|
+
provider: requirePort(service.provider, ["getPaymasterStubData", "getPaymasterData"], "paymaster provider")
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
return services;
|
|
364
|
+
}
|
|
365
|
+
function captureOptions(value) {
|
|
366
|
+
const context = /* @__PURE__ */ new WeakSet();
|
|
367
|
+
const record = captureRecord(value, "relay handler options", context, (message) => relayFailure("relay_internal", message));
|
|
368
|
+
for (const key of Object.keys(record)) if (!OPTION_KEYS.includes(key)) relayFailure("relay_internal", "relay handler options contain an unknown field");
|
|
369
|
+
const chains = record.chains === void 0 ? /* @__PURE__ */ new Map() : captureChainPorts(record.chains);
|
|
370
|
+
const rateLimit = record.rateLimit === void 0 ? void 0 : requirePort(record.rateLimit, ["check"], "rateLimit");
|
|
371
|
+
const paymasterServices = capturePaymasterServices(record.paymasterServices, context, chains);
|
|
372
|
+
if (paymasterServices.size > 0) {
|
|
373
|
+
if (record.bootstrap === void 0) relayFailure("relay_internal", "a paymaster service requires a bootstrap surface");
|
|
374
|
+
if (rateLimit === void 0) relayFailure("relay_internal", "a paymaster service requires a rate limiter");
|
|
375
|
+
}
|
|
376
|
+
let sessionSigner = null;
|
|
377
|
+
if (record.sessionSigner !== void 0) {
|
|
378
|
+
if (record.bootstrap === void 0) relayFailure("relay_internal", "a session signer requires a bootstrap surface");
|
|
379
|
+
const custody = captureRecord(record.sessionSigner, "relay session signer configuration", context, (message) => relayFailure("relay_internal", message));
|
|
380
|
+
if (custody.mode !== "application_backend" && custody.mode !== "oaath_hosted") relayFailure("relay_internal", "session signer mode is unsupported");
|
|
381
|
+
if (typeof custody.providerId !== "string" || custody.providerId.length < 1) relayFailure("relay_internal", "session signer providerId is invalid");
|
|
382
|
+
sessionSigner = Object.freeze({
|
|
383
|
+
mode: custody.mode,
|
|
384
|
+
providerId: custody.providerId,
|
|
385
|
+
provider: requirePort(custody.provider, ["credential", "sign"], "session signer provider")
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
let bootstrap = null;
|
|
389
|
+
if (record.bootstrap !== void 0) {
|
|
390
|
+
if (chains.size === 0) relayFailure("relay_internal", "a bootstrap surface requires chain ports");
|
|
391
|
+
const identity = captureRecord(record.bootstrap, "relay bootstrap configuration", context, (message) => relayFailure("relay_internal", message));
|
|
392
|
+
try {
|
|
393
|
+
bootstrap = parseServiceBootstrap({
|
|
394
|
+
version: OAATH_SERVICE_BOOTSTRAP_VERSION,
|
|
395
|
+
application: identity.application,
|
|
396
|
+
userHandle: identity.userHandle,
|
|
397
|
+
account: identity.account,
|
|
398
|
+
ownerValidator: identity.ownerValidator,
|
|
399
|
+
chains: [...chains.values()].map((port) => {
|
|
400
|
+
const paymasterService = paymasterServices.get(port.chainId);
|
|
401
|
+
return {
|
|
402
|
+
chainId: port.chainId,
|
|
403
|
+
usage: port.usage !== null,
|
|
404
|
+
feePayer: port.feePayer,
|
|
405
|
+
paymasterService: paymasterService === void 0 ? null : { providerId: paymasterService.providerId },
|
|
406
|
+
staticPaymasterConfigurationHash: port.staticPaymasterConfigurationHash
|
|
407
|
+
};
|
|
408
|
+
}),
|
|
409
|
+
sessionSigner: sessionSigner === null ? {
|
|
410
|
+
mode: "frontend",
|
|
411
|
+
providerId: null
|
|
412
|
+
} : {
|
|
413
|
+
mode: sessionSigner.mode,
|
|
414
|
+
providerId: sessionSigner.providerId
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
} catch {
|
|
418
|
+
relayFailure("relay_internal", "relay bootstrap configuration is invalid");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return Object.freeze({
|
|
422
|
+
store: requirePort(record.store, ["begin", "close"], "store"),
|
|
423
|
+
authentication: requirePort(record.authentication, ["authenticate"], "authentication"),
|
|
424
|
+
kms: requirePort(record.kms, ["encrypt", "decrypt"], "kms"),
|
|
425
|
+
clock: requirePort(record.clock, ["now"], "clock"),
|
|
426
|
+
rateLimit,
|
|
427
|
+
requestTtlMs: duration(record.requestTtlMs, DEFAULT_REQUEST_TTL_MS, "requestTtlMs"),
|
|
428
|
+
codeTtlMs: duration(record.codeTtlMs, DEFAULT_CODE_TTL_MS, "codeTtlMs", MAX_CODE_TTL_MS),
|
|
429
|
+
maxBodyBytes: duration(record.maxBodyBytes, DEFAULT_MAX_BODY_BYTES, "maxBodyBytes"),
|
|
430
|
+
bootstrap,
|
|
431
|
+
sessionSigner,
|
|
432
|
+
chains,
|
|
433
|
+
paymasterServices
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function requireMethod(request, expected) {
|
|
437
|
+
if (request.method !== expected) relayFailure("relay_method_not_allowed", "route does not accept this method");
|
|
438
|
+
}
|
|
439
|
+
async function bodyRecord(request, maxBodyBytes) {
|
|
440
|
+
if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) relayFailure(INVALID, "request body must be application/json");
|
|
441
|
+
let text;
|
|
442
|
+
try {
|
|
443
|
+
text = await request.text();
|
|
444
|
+
} catch {
|
|
445
|
+
return relayFailure(INVALID, "request body is unreadable");
|
|
446
|
+
}
|
|
447
|
+
if (new TextEncoder().encode(text).byteLength > maxBodyBytes) return relayFailure(INVALID, "request body exceeds its bound");
|
|
448
|
+
let parsed;
|
|
449
|
+
try {
|
|
450
|
+
parsed = JSON.parse(text);
|
|
451
|
+
} catch {
|
|
452
|
+
return relayFailure(INVALID, "request body is not JSON");
|
|
453
|
+
}
|
|
454
|
+
return captureRecord(parsed, "request body", /* @__PURE__ */ new WeakSet(), (message) => relayFailure(INVALID, message));
|
|
455
|
+
}
|
|
456
|
+
function exactBody(record, keys) {
|
|
457
|
+
return exactCapturedRecord(record, keys, "request body", (message) => relayFailure(INVALID, message));
|
|
458
|
+
}
|
|
459
|
+
async function invokePaymasterService(service, method, caller, params) {
|
|
460
|
+
const controller = new AbortController();
|
|
461
|
+
let timeout;
|
|
462
|
+
try {
|
|
463
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
464
|
+
timeout = setTimeout(() => {
|
|
465
|
+
controller.abort();
|
|
466
|
+
reject(/* @__PURE__ */ new Error("paymaster request deadline elapsed"));
|
|
467
|
+
}, service.requestTimeoutMs);
|
|
468
|
+
});
|
|
469
|
+
const capability = method === "stub-data" ? service.provider.getPaymasterStubData : service.provider.getPaymasterData;
|
|
470
|
+
const request = Object.freeze({
|
|
471
|
+
caller: Object.freeze({
|
|
472
|
+
clientId: caller.clientId,
|
|
473
|
+
subject: caller.subject
|
|
474
|
+
}),
|
|
475
|
+
params,
|
|
476
|
+
signal: controller.signal
|
|
477
|
+
});
|
|
478
|
+
return await Promise.race([Promise.resolve(Reflect.apply(capability, void 0, [request])), deadline]);
|
|
479
|
+
} catch {
|
|
480
|
+
return relayFailure("relay_chain_unavailable", "paymaster provider did not answer");
|
|
481
|
+
} finally {
|
|
482
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function decisionCommand(record) {
|
|
486
|
+
if (record.outcome === "approved") {
|
|
487
|
+
const exact = exactBody(record, ["outcome", "artifact"]);
|
|
488
|
+
return Object.freeze({
|
|
489
|
+
outcome: "approved",
|
|
490
|
+
artifact: boundedText(exact.artifact, RELAY_LIMITS.artifactPlaintext, "artifact", INVALID)
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (record.outcome === "rejected") {
|
|
494
|
+
exactBody(record, ["outcome"]);
|
|
495
|
+
return Object.freeze({ outcome: "rejected" });
|
|
496
|
+
}
|
|
497
|
+
return relayFailure(INVALID, "decision outcome is unsupported");
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* EXPERIMENTAL PREVIEW — the phone decision body: `{command, artifact?}`.
|
|
501
|
+
* `approve` hands over the artifact the client will claim once; `reject`
|
|
502
|
+
* carries nothing else. The saga answers the stored outcome on a replay.
|
|
503
|
+
*/
|
|
504
|
+
function phoneDecisionCommand(record) {
|
|
505
|
+
if (record.command === "approve") {
|
|
506
|
+
const exact = exactBody(record, ["command", "artifact"]);
|
|
507
|
+
return Object.freeze({
|
|
508
|
+
outcome: "approved",
|
|
509
|
+
artifact: boundedText(exact.artifact, RELAY_LIMITS.artifactPlaintext, "artifact", INVALID)
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
if (record.command === "reject") {
|
|
513
|
+
exactBody(record, ["command"]);
|
|
514
|
+
return Object.freeze({ outcome: "rejected" });
|
|
515
|
+
}
|
|
516
|
+
return relayFailure(INVALID, "decision command is unsupported");
|
|
517
|
+
}
|
|
518
|
+
function stateBody(state) {
|
|
519
|
+
return {
|
|
520
|
+
requestId: state.requestId,
|
|
521
|
+
clientId: state.clientId,
|
|
522
|
+
redirectUri: state.redirectUri,
|
|
523
|
+
requestedScope: state.requestedScope,
|
|
524
|
+
expiresAt: state.expiresAt,
|
|
525
|
+
expired: state.expired,
|
|
526
|
+
decision: state.decision
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function createRelayHandler(options) {
|
|
530
|
+
const captured = captureOptions(options);
|
|
531
|
+
const authenticate = async (request, role, route) => {
|
|
532
|
+
const caller = await authenticateCaller(captured.authentication, request, role);
|
|
533
|
+
await assertWithinRateLimit(captured.rateLimit, {
|
|
534
|
+
route,
|
|
535
|
+
clientId: caller.clientId
|
|
536
|
+
});
|
|
537
|
+
return caller;
|
|
538
|
+
};
|
|
539
|
+
const route = async (request) => {
|
|
540
|
+
const segments = new URL(request.url).pathname.split("/").filter((part) => part.length > 0);
|
|
541
|
+
const [head, group, third, fourth] = segments;
|
|
542
|
+
if (head === "native") {
|
|
543
|
+
if (segments.length === 3 && group === "projections") {
|
|
544
|
+
requireMethod(request, "GET");
|
|
545
|
+
const caller = await authenticate(request, "owner", "native.project");
|
|
546
|
+
const projection = await projectOwnerPhoneRequest({
|
|
547
|
+
store: captured.store,
|
|
548
|
+
clock: captured.clock,
|
|
549
|
+
caller,
|
|
550
|
+
requestId: canonicalIdentifier(third, "operationId", INVALID)
|
|
551
|
+
});
|
|
552
|
+
return jsonResponse(200, projection);
|
|
553
|
+
}
|
|
554
|
+
if (segments.length === 3 && group === "decisions") {
|
|
555
|
+
requireMethod(request, "POST");
|
|
556
|
+
const caller = await authenticate(request, "owner", "native.decide");
|
|
557
|
+
const command = phoneDecisionCommand(await bodyRecord(request, captured.maxBodyBytes));
|
|
558
|
+
const decided = await submitOwnerPhoneDecision({
|
|
559
|
+
store: captured.store,
|
|
560
|
+
clock: captured.clock,
|
|
561
|
+
kms: captured.kms,
|
|
562
|
+
caller,
|
|
563
|
+
operationId: canonicalIdentifier(third, "operationId", INVALID),
|
|
564
|
+
command,
|
|
565
|
+
codeTtlMs: captured.codeTtlMs
|
|
566
|
+
});
|
|
567
|
+
return jsonResponse(200, decided);
|
|
568
|
+
}
|
|
569
|
+
return relayFailure("relay_not_found", "route does not exist");
|
|
570
|
+
}
|
|
571
|
+
if (head === "bootstrap" && segments.length === 1) {
|
|
572
|
+
requireMethod(request, "GET");
|
|
573
|
+
await authenticate(request, "client", "bootstrap.fetch");
|
|
574
|
+
if (captured.bootstrap === null) return relayFailure("relay_not_found", "route does not exist");
|
|
575
|
+
return jsonResponse(200, captured.bootstrap);
|
|
576
|
+
}
|
|
577
|
+
if (head === "session-signers") {
|
|
578
|
+
const custody = captured.sessionSigner;
|
|
579
|
+
if (custody === null) return relayFailure("relay_not_found", "route does not exist");
|
|
580
|
+
if (segments.length === 1) {
|
|
581
|
+
requireMethod(request, "POST");
|
|
582
|
+
const caller = await authenticate(request, "client", "session-signers.credential");
|
|
583
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["deviceId"]);
|
|
584
|
+
const deviceId = boundedText(body.deviceId, 256, "session signer deviceId", INVALID);
|
|
585
|
+
let credential;
|
|
586
|
+
try {
|
|
587
|
+
credential = await custody.provider.credential({
|
|
588
|
+
clientId: caller.clientId,
|
|
589
|
+
subject: caller.subject,
|
|
590
|
+
deviceId
|
|
591
|
+
});
|
|
592
|
+
} catch {
|
|
593
|
+
return relayFailure("relay_internal", "session signer provider did not answer");
|
|
594
|
+
}
|
|
595
|
+
return jsonResponse(200, { operatorCredential: credential });
|
|
596
|
+
}
|
|
597
|
+
if (segments.length === 2 && group === "signatures") {
|
|
598
|
+
requireMethod(request, "POST");
|
|
599
|
+
const caller = await authenticate(request, "client", "session-signers.sign");
|
|
600
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["deviceId", "hash"]);
|
|
601
|
+
const deviceId = boundedText(body.deviceId, 256, "session signer deviceId", INVALID);
|
|
602
|
+
if (typeof body.hash !== "string" || !/^0x[0-9a-f]{64}$/u.test(body.hash)) return relayFailure(INVALID, "session signer hash must be one exact 32-byte hash");
|
|
603
|
+
let signature;
|
|
604
|
+
try {
|
|
605
|
+
signature = await custody.provider.sign({
|
|
606
|
+
clientId: caller.clientId,
|
|
607
|
+
subject: caller.subject,
|
|
608
|
+
deviceId,
|
|
609
|
+
hash: body.hash
|
|
610
|
+
});
|
|
611
|
+
} catch {
|
|
612
|
+
return relayFailure("relay_internal", "session signer provider did not answer");
|
|
613
|
+
}
|
|
614
|
+
return jsonResponse(200, { signature });
|
|
615
|
+
}
|
|
616
|
+
return relayFailure("relay_not_found", "route does not exist");
|
|
617
|
+
}
|
|
618
|
+
if (head === "invalidations" && segments.length === 1) {
|
|
619
|
+
requireMethod(request, "POST");
|
|
620
|
+
const caller = await authenticate(request, "client", "invalidations.create");
|
|
621
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["grantId", "capabilityHash"]);
|
|
622
|
+
const grantId = canonicalIdentifier(body.grantId, "grantId", INVALID);
|
|
623
|
+
const capabilityHash = boundedText(body.capabilityHash, 66, "capabilityHash", INVALID);
|
|
624
|
+
if (!/^0x[0-9a-f]{64}$/u.test(capabilityHash)) return relayFailure(INVALID, "capabilityHash must be a lowercase 32-byte hash");
|
|
625
|
+
const record = await withRelayTransaction(captured.store, async (transaction) => {
|
|
626
|
+
const existing = await transaction.lockCapabilityInvalidation(grantId);
|
|
627
|
+
if (existing) {
|
|
628
|
+
if (existing.clientId !== caller.clientId) return relayFailure("relay_not_found", "grant does not exist");
|
|
629
|
+
return existing;
|
|
630
|
+
}
|
|
631
|
+
const created = Object.freeze({
|
|
632
|
+
version: OAATH_CAPABILITY_INVALIDATION_RECORD_VERSION,
|
|
633
|
+
grantId,
|
|
634
|
+
clientId: caller.clientId,
|
|
635
|
+
capabilityHash,
|
|
636
|
+
invalidatedAt: relayNow(captured.clock)
|
|
637
|
+
});
|
|
638
|
+
if (!await transaction.insertCapabilityInvalidation(created)) return relayFailure("relay_internal", "capability invalidation did not commit");
|
|
639
|
+
return created;
|
|
640
|
+
});
|
|
641
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`oaath-relay-invalidation:v1:${record.clientId}:${record.grantId}:${record.capabilityHash}:${record.invalidatedAt}`));
|
|
642
|
+
const evidenceHash = `0x${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
643
|
+
return jsonResponse(200, {
|
|
644
|
+
evidenceHash,
|
|
645
|
+
invalidatedAt: Math.floor(record.invalidatedAt / 1e3)
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (head === "chains" && segments.length === 4 && third === "paymaster") {
|
|
649
|
+
requireMethod(request, "POST");
|
|
650
|
+
const service = captured.paymasterServices.get(Number(group));
|
|
651
|
+
const method = fourth === "stub-data" || fourth === "data" ? fourth : null;
|
|
652
|
+
if (service === void 0 || method === null) return relayFailure("relay_not_found", "route does not exist");
|
|
653
|
+
const result = await invokePaymasterService(service, method, await authenticate(request, "client", `chains.paymaster.${method}`), exactBody(await bodyRecord(request, captured.maxBodyBytes), ["params"]).params);
|
|
654
|
+
return jsonResponse(200, result === void 0 ? {
|
|
655
|
+
present: false,
|
|
656
|
+
result: null
|
|
657
|
+
} : {
|
|
658
|
+
present: true,
|
|
659
|
+
result
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (head === "chains") {
|
|
663
|
+
if (segments.length !== 3) return relayFailure("relay_not_found", "route does not exist");
|
|
664
|
+
requireMethod(request, "POST");
|
|
665
|
+
const port = captured.chains.get(Number(group));
|
|
666
|
+
const name = third === "submissions" ? "submission" : third === "reads" || third === "observation" || third === "bundler" || third === "quote" || third === "usage" ? third : null;
|
|
667
|
+
if (!port || name === null) return relayFailure("relay_not_found", "route does not exist");
|
|
668
|
+
const caller = await authenticate(request, name === "submission" ? ["client", "owner"] : "client", `chains.${name}`);
|
|
669
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["request"]);
|
|
670
|
+
const capability = name === "usage" ? port.usage : port[name];
|
|
671
|
+
if (capability === null) return relayFailure("relay_not_found", "route does not exist");
|
|
672
|
+
const grantId = caller.role === "owner" ? null : chainRequestGrantId(name, body.request);
|
|
673
|
+
if (grantId !== null) {
|
|
674
|
+
if (await withRelayTransaction(captured.store, (transaction) => transaction.lockCapabilityInvalidation(grantId)) !== void 0) return relayFailure("relay_capability_invalidated", "the Grant's capability is invalidated");
|
|
675
|
+
}
|
|
676
|
+
let result;
|
|
677
|
+
try {
|
|
678
|
+
result = await capability(body.request);
|
|
679
|
+
} catch {
|
|
680
|
+
return relayFailure("relay_chain_unavailable", "chain port did not answer");
|
|
681
|
+
}
|
|
682
|
+
return jsonResponse(200, result === void 0 ? {
|
|
683
|
+
present: false,
|
|
684
|
+
result: null
|
|
685
|
+
} : {
|
|
686
|
+
present: true,
|
|
687
|
+
result
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (head !== "authorization") relayFailure("relay_not_found", "route does not exist");
|
|
691
|
+
if (segments.length === 2 && group === "requests") {
|
|
692
|
+
requireMethod(request, "POST");
|
|
693
|
+
const caller = await authenticate(request, "client", "authorization.create");
|
|
694
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), [
|
|
695
|
+
"redirectUri",
|
|
696
|
+
"codeChallenge",
|
|
697
|
+
"requestedScope"
|
|
698
|
+
]);
|
|
699
|
+
const created = await createAuthorizationRequest({
|
|
700
|
+
store: captured.store,
|
|
701
|
+
clock: captured.clock,
|
|
702
|
+
caller,
|
|
703
|
+
redirectUri: boundedText(body.redirectUri, RELAY_LIMITS.redirectUri, "redirectUri", INVALID),
|
|
704
|
+
codeChallenge: boundedText(body.codeChallenge, RELAY_LIMITS.codeChallenge, "codeChallenge", INVALID),
|
|
705
|
+
requestedScope: boundedText(body.requestedScope, RELAY_LIMITS.requestedScope, "requestedScope", INVALID),
|
|
706
|
+
requestTtlMs: captured.requestTtlMs
|
|
707
|
+
});
|
|
708
|
+
return jsonResponse(201, created);
|
|
709
|
+
}
|
|
710
|
+
if (segments.length === 3 && group === "requests") {
|
|
711
|
+
requireMethod(request, "GET");
|
|
712
|
+
const caller = await authenticate(request, "owner", "authorization.fetch");
|
|
713
|
+
const state = await fetchAuthorizationRequest({
|
|
714
|
+
store: captured.store,
|
|
715
|
+
clock: captured.clock,
|
|
716
|
+
caller,
|
|
717
|
+
requestId: canonicalIdentifier(third, "requestId", INVALID)
|
|
718
|
+
});
|
|
719
|
+
return jsonResponse(200, stateBody(state));
|
|
720
|
+
}
|
|
721
|
+
if (segments.length === 4 && group === "requests" && fourth === "decision") {
|
|
722
|
+
requireMethod(request, "POST");
|
|
723
|
+
const caller = await authenticate(request, "owner", "authorization.decide");
|
|
724
|
+
const command = decisionCommand(await bodyRecord(request, captured.maxBodyBytes));
|
|
725
|
+
const decided = await submitAuthorizationDecision({
|
|
726
|
+
store: captured.store,
|
|
727
|
+
clock: captured.clock,
|
|
728
|
+
kms: captured.kms,
|
|
729
|
+
caller,
|
|
730
|
+
requestId: canonicalIdentifier(third, "requestId", INVALID),
|
|
731
|
+
command,
|
|
732
|
+
codeTtlMs: captured.codeTtlMs
|
|
733
|
+
});
|
|
734
|
+
return jsonResponse(200, decided);
|
|
735
|
+
}
|
|
736
|
+
if (segments.length === 4 && group === "requests" && fourth === "code") {
|
|
737
|
+
requireMethod(request, "GET");
|
|
738
|
+
const caller = await authenticate(request, "client", "authorization.code");
|
|
739
|
+
const fetched = await fetchAuthorizationCode({
|
|
740
|
+
store: captured.store,
|
|
741
|
+
clock: captured.clock,
|
|
742
|
+
kms: captured.kms,
|
|
743
|
+
caller,
|
|
744
|
+
requestId: canonicalIdentifier(third, "requestId", INVALID)
|
|
745
|
+
});
|
|
746
|
+
return jsonResponse(200, fetched);
|
|
747
|
+
}
|
|
748
|
+
if (segments.length === 3 && group === "codes" && third === "consume") {
|
|
749
|
+
requireMethod(request, "POST");
|
|
750
|
+
const caller = await authenticate(request, "client", "authorization.consume");
|
|
751
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), [
|
|
752
|
+
"code",
|
|
753
|
+
"codeVerifier",
|
|
754
|
+
"redirectUri"
|
|
755
|
+
]);
|
|
756
|
+
const consumed = await consumeAuthorizationCode({
|
|
757
|
+
store: captured.store,
|
|
758
|
+
clock: captured.clock,
|
|
759
|
+
caller,
|
|
760
|
+
code: canonicalIdentifier(body.code, "code", INVALID),
|
|
761
|
+
codeVerifier: boundedText(body.codeVerifier, RELAY_LIMITS.codeVerifier, "codeVerifier", INVALID),
|
|
762
|
+
redirectUri: boundedText(body.redirectUri, RELAY_LIMITS.redirectUri, "redirectUri", INVALID)
|
|
763
|
+
});
|
|
764
|
+
return jsonResponse(200, consumed);
|
|
765
|
+
}
|
|
766
|
+
if (segments.length === 4 && group === "artifacts" && fourth === "claim") {
|
|
767
|
+
requireMethod(request, "POST");
|
|
768
|
+
const caller = await authenticate(request, "client", "authorization.claim");
|
|
769
|
+
const claimed = await claimEncryptedArtifact({
|
|
770
|
+
store: captured.store,
|
|
771
|
+
clock: captured.clock,
|
|
772
|
+
kms: captured.kms,
|
|
773
|
+
caller,
|
|
774
|
+
artifactId: canonicalIdentifier(third, "artifactId", INVALID)
|
|
775
|
+
});
|
|
776
|
+
return jsonResponse(200, claimed);
|
|
777
|
+
}
|
|
778
|
+
if (segments.length === 2 && group === "resume") {
|
|
779
|
+
requireMethod(request, "POST");
|
|
780
|
+
const caller = await authenticate(request, "client", "authorization.resume");
|
|
781
|
+
const body = exactBody(await bodyRecord(request, captured.maxBodyBytes), ["requestId"]);
|
|
782
|
+
const state = await resumeAuthorization({
|
|
783
|
+
store: captured.store,
|
|
784
|
+
clock: captured.clock,
|
|
785
|
+
caller,
|
|
786
|
+
requestId: canonicalIdentifier(body.requestId, "requestId", INVALID)
|
|
787
|
+
});
|
|
788
|
+
return jsonResponse(200, stateBody(state));
|
|
789
|
+
}
|
|
790
|
+
return relayFailure("relay_not_found", "route does not exist");
|
|
791
|
+
};
|
|
792
|
+
return async (request) => {
|
|
793
|
+
try {
|
|
794
|
+
return await route(request);
|
|
795
|
+
} catch (error) {
|
|
796
|
+
return relayErrorResponse(relayErrorCode(error));
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
//#endregion
|
|
801
|
+
//#region src/security/redact.ts
|
|
802
|
+
/**
|
|
803
|
+
* Bounded credential and URL redaction for diagnostics only.
|
|
804
|
+
*
|
|
805
|
+
* Nothing here shapes a response or a machine decision. Its only job is to make
|
|
806
|
+
* an adopter's log line safe and finite: no authorization code, verifier, token,
|
|
807
|
+
* artifact, ciphertext, credential-bearing URL, or unbounded provider blob.
|
|
808
|
+
*
|
|
809
|
+
* @author taek <leekt216@gmail.com>
|
|
810
|
+
*/
|
|
811
|
+
const REDACTED = "[redacted]";
|
|
812
|
+
const UNREADABLE_URL = "[unreadable-url]";
|
|
813
|
+
const TRUNCATED = "[truncated]";
|
|
814
|
+
const MAX_TEXT = 128;
|
|
815
|
+
const MAX_DEPTH = 3;
|
|
816
|
+
const MAX_ENTRIES = 16;
|
|
817
|
+
/** Key names whose values are never safe to log, matched after normalization. */
|
|
818
|
+
const SENSITIVE_KEYS = /* @__PURE__ */ new Set([
|
|
819
|
+
"apikey",
|
|
820
|
+
"artifact",
|
|
821
|
+
"authorization",
|
|
822
|
+
"bearer",
|
|
823
|
+
"ciphertext",
|
|
824
|
+
"ciphertextref",
|
|
825
|
+
"clientsecret",
|
|
826
|
+
"code",
|
|
827
|
+
"codechallenge",
|
|
828
|
+
"codeverifier",
|
|
829
|
+
"cookie",
|
|
830
|
+
"credential",
|
|
831
|
+
"idtoken",
|
|
832
|
+
"key",
|
|
833
|
+
"password",
|
|
834
|
+
"plaintext",
|
|
835
|
+
"privatekey",
|
|
836
|
+
"secret",
|
|
837
|
+
"session",
|
|
838
|
+
"sessionkey",
|
|
839
|
+
"setcookie",
|
|
840
|
+
"signature",
|
|
841
|
+
"token",
|
|
842
|
+
"verifier"
|
|
843
|
+
]);
|
|
844
|
+
function normalizeKey(key) {
|
|
845
|
+
return key.toLowerCase().replaceAll(/[^a-z0-9]/gu, "");
|
|
846
|
+
}
|
|
847
|
+
function isUrlKey(normalized) {
|
|
848
|
+
return normalized.endsWith("url") || normalized.endsWith("uri");
|
|
849
|
+
}
|
|
850
|
+
function boundText(value) {
|
|
851
|
+
return value.length <= MAX_TEXT ? value : `${value.slice(0, MAX_TEXT)}${TRUNCATED}`;
|
|
852
|
+
}
|
|
853
|
+
/** Keeps scheme, host, and path. Drops userinfo, query, and fragment. */
|
|
854
|
+
function redactUrl(value) {
|
|
855
|
+
if (typeof value !== "string") return UNREADABLE_URL;
|
|
856
|
+
let url;
|
|
857
|
+
try {
|
|
858
|
+
url = new URL(value);
|
|
859
|
+
} catch {
|
|
860
|
+
return UNREADABLE_URL;
|
|
861
|
+
}
|
|
862
|
+
const port = url.port === "" ? "" : `:${url.port}`;
|
|
863
|
+
return boundText(`${url.protocol}//${url.hostname}${port}${url.pathname}`);
|
|
864
|
+
}
|
|
865
|
+
function redactValue(value, depth) {
|
|
866
|
+
if (value === null) return null;
|
|
867
|
+
switch (typeof value) {
|
|
868
|
+
case "string": return boundText(value);
|
|
869
|
+
case "number": return Number.isFinite(value) ? value : String(value);
|
|
870
|
+
case "boolean": return value;
|
|
871
|
+
case "bigint": return boundText(value.toString());
|
|
872
|
+
case "object": break;
|
|
873
|
+
default: return REDACTED;
|
|
874
|
+
}
|
|
875
|
+
if (depth >= MAX_DEPTH) return REDACTED;
|
|
876
|
+
if (Array.isArray(value)) {
|
|
877
|
+
const items = value.slice(0, MAX_ENTRIES).map((item) => redactValue(item, depth + 1));
|
|
878
|
+
return value.length > MAX_ENTRIES ? [...items, TRUNCATED] : items;
|
|
879
|
+
}
|
|
880
|
+
if (value instanceof Error) return {
|
|
881
|
+
name: boundText(value.name),
|
|
882
|
+
message: REDACTED
|
|
883
|
+
};
|
|
884
|
+
return redactRecord(value, depth);
|
|
885
|
+
}
|
|
886
|
+
function redactRecord(value, depth) {
|
|
887
|
+
const redacted = {};
|
|
888
|
+
let entries = 0;
|
|
889
|
+
for (const key of Object.keys(value)) {
|
|
890
|
+
if (entries >= MAX_ENTRIES) {
|
|
891
|
+
redacted[TRUNCATED] = true;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
entries += 1;
|
|
895
|
+
const normalized = normalizeKey(key);
|
|
896
|
+
if (SENSITIVE_KEYS.has(normalized)) redacted[key] = REDACTED;
|
|
897
|
+
else if (isUrlKey(normalized)) redacted[key] = redactUrl(value[key]);
|
|
898
|
+
else redacted[key] = redactValue(value[key], depth + 1);
|
|
899
|
+
}
|
|
900
|
+
return redacted;
|
|
901
|
+
}
|
|
902
|
+
/** Returns a bounded, credential-free projection safe to hand to a logger. */
|
|
903
|
+
function redactForLog(value) {
|
|
904
|
+
return redactValue(value, 0);
|
|
905
|
+
}
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region src/session-signer/kms-provider.ts
|
|
908
|
+
/**
|
|
909
|
+
* Reference hosted session-signer provider: one KMS-sealed secp256k1 operator
|
|
910
|
+
* key per authenticated `(clientId, subject, deviceId)` identity, signing one
|
|
911
|
+
* exact 32-byte hash at a time.
|
|
912
|
+
*
|
|
913
|
+
* Custody boundary: the private scalar exists in plaintext only inside this
|
|
914
|
+
* process during key generation and signing; at rest it is exactly one
|
|
915
|
+
* KMS-sealed string. No route, log, or return value ever carries key material
|
|
916
|
+
* — `credential` answers the public operator profile and `sign` answers one
|
|
917
|
+
* 65-byte ECDSA signature.
|
|
918
|
+
*
|
|
919
|
+
* Rotation invariant: one identity maps to one credential for this provider's
|
|
920
|
+
* lifetime. There is no rotation path here — a new key requires a new
|
|
921
|
+
* identity, and therefore a newly approved Grant; nothing can silently swap
|
|
922
|
+
* the public key under an existing approval.
|
|
923
|
+
*
|
|
924
|
+
* Like `createMemoryRelayStore`, the in-memory registry makes this a
|
|
925
|
+
* reference implementation, not a production durability claim: a deployment
|
|
926
|
+
* that must survive restarts persists the sealed entries in its own store.
|
|
927
|
+
*
|
|
928
|
+
* @author taek <leekt216@gmail.com>
|
|
929
|
+
*/
|
|
930
|
+
const HASH = /^0x[0-9a-f]{64}$/u;
|
|
931
|
+
const MAX_IDENTITY_PART = 256;
|
|
932
|
+
function identityKey(request) {
|
|
933
|
+
for (const part of [
|
|
934
|
+
request.clientId,
|
|
935
|
+
request.subject,
|
|
936
|
+
request.deviceId
|
|
937
|
+
]) if (typeof part !== "string" || part.length < 1 || part.length > MAX_IDENTITY_PART) throw new Error("session signer identity is invalid");
|
|
938
|
+
return JSON.stringify([
|
|
939
|
+
request.clientId,
|
|
940
|
+
request.subject,
|
|
941
|
+
request.deviceId
|
|
942
|
+
]);
|
|
943
|
+
}
|
|
944
|
+
function addressOf(privateKey) {
|
|
945
|
+
const publicKey = secp256k1.getPublicKey(privateKey, false);
|
|
946
|
+
return `0x${bytesToHex(keccak_256(publicKey.slice(1)).slice(12))}`;
|
|
947
|
+
}
|
|
948
|
+
function createKmsSessionSignerProvider(input) {
|
|
949
|
+
const kms = input.kms;
|
|
950
|
+
/** identity -> KMS-sealed private scalar; the only at-rest key form. */
|
|
951
|
+
const sealed = /* @__PURE__ */ new Map();
|
|
952
|
+
/** Serializes first-use creation so one identity never mints two keys. */
|
|
953
|
+
const creating = /* @__PURE__ */ new Map();
|
|
954
|
+
async function sealedKeyFor(identity) {
|
|
955
|
+
const existing = sealed.get(identity);
|
|
956
|
+
if (existing !== void 0) return existing;
|
|
957
|
+
const pending = creating.get(identity) ?? (async () => {
|
|
958
|
+
const scalar = secp256k1.utils.randomPrivateKey();
|
|
959
|
+
const reference = String(await kms.encrypt(bytesToHex(scalar)));
|
|
960
|
+
scalar.fill(0);
|
|
961
|
+
sealed.set(identity, reference);
|
|
962
|
+
return reference;
|
|
963
|
+
})();
|
|
964
|
+
creating.set(identity, pending);
|
|
965
|
+
pending.finally(() => creating.delete(identity));
|
|
966
|
+
return pending;
|
|
967
|
+
}
|
|
968
|
+
return Object.freeze({
|
|
969
|
+
async credential(request) {
|
|
970
|
+
const identity = identityKey(request);
|
|
971
|
+
const privateKey = hexToBytes(String(await kms.decrypt(await sealedKeyFor(identity))));
|
|
972
|
+
try {
|
|
973
|
+
return Object.freeze({
|
|
974
|
+
version: OAATH_OPERATOR_CREDENTIAL_PROFILE_VERSION,
|
|
975
|
+
kind: "ecdsa",
|
|
976
|
+
address: addressOf(privateKey)
|
|
977
|
+
});
|
|
978
|
+
} finally {
|
|
979
|
+
privateKey.fill(0);
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
async sign(request) {
|
|
983
|
+
const identity = identityKey(request);
|
|
984
|
+
if (typeof request.hash !== "string" || !HASH.test(request.hash)) throw new Error("session signer hash must be one exact 32-byte hash");
|
|
985
|
+
const reference = sealed.get(identity);
|
|
986
|
+
if (reference === void 0) throw new Error("session signer identity holds no credential");
|
|
987
|
+
const privateKey = hexToBytes(String(await kms.decrypt(reference)));
|
|
988
|
+
try {
|
|
989
|
+
const signature = secp256k1.sign(hexToBytes(request.hash.slice(2)), privateKey, { lowS: true });
|
|
990
|
+
if (signature.recovery !== 0 && signature.recovery !== 1) throw new Error("session signer produced an unusable signature");
|
|
991
|
+
return `0x${bytesToHex(signature.toCompactRawBytes())}${(27 + signature.recovery).toString(16)}`;
|
|
992
|
+
} finally {
|
|
993
|
+
privateKey.fill(0);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/store/memory.ts
|
|
1000
|
+
/**
|
|
1001
|
+
* In-memory relay store implementing the same transaction contract as
|
|
1002
|
+
* PostgreSQL. It is durable only for the life of the process: use it for local
|
|
1003
|
+
* development and tests, never for a deployment that must survive a restart.
|
|
1004
|
+
*
|
|
1005
|
+
* ponytail: one process-wide writer lock stands in for row locks. It is strictly
|
|
1006
|
+
* stronger than `SELECT ... FOR UPDATE`, so one-shot semantics are identical;
|
|
1007
|
+
* swap in per-row locks only if a single-process relay ever needs throughput.
|
|
1008
|
+
*
|
|
1009
|
+
* @author taek <leekt216@gmail.com>
|
|
1010
|
+
*/
|
|
1011
|
+
function emptyTables() {
|
|
1012
|
+
return {
|
|
1013
|
+
requests: /* @__PURE__ */ new Map(),
|
|
1014
|
+
decisions: /* @__PURE__ */ new Map(),
|
|
1015
|
+
codes: /* @__PURE__ */ new Map(),
|
|
1016
|
+
artifacts: /* @__PURE__ */ new Map(),
|
|
1017
|
+
invalidations: /* @__PURE__ */ new Map()
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
function copyTables(tables) {
|
|
1021
|
+
return {
|
|
1022
|
+
requests: new Map(tables.requests),
|
|
1023
|
+
decisions: new Map(tables.decisions),
|
|
1024
|
+
codes: new Map(tables.codes),
|
|
1025
|
+
artifacts: new Map(tables.artifacts),
|
|
1026
|
+
invalidations: new Map(tables.invalidations)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function insert(table, key, record) {
|
|
1030
|
+
if (table.has(key)) return false;
|
|
1031
|
+
table.set(key, record);
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
function read(table, key, parse) {
|
|
1035
|
+
const stored = table.get(key);
|
|
1036
|
+
return stored === void 0 ? void 0 : parse(stored);
|
|
1037
|
+
}
|
|
1038
|
+
function createMemoryRelayStore() {
|
|
1039
|
+
let committed = emptyTables();
|
|
1040
|
+
let closed = false;
|
|
1041
|
+
let tail = Promise.resolve();
|
|
1042
|
+
async function acquire() {
|
|
1043
|
+
const prior = tail;
|
|
1044
|
+
let release = () => {};
|
|
1045
|
+
tail = new Promise((resolve) => {
|
|
1046
|
+
release = resolve;
|
|
1047
|
+
});
|
|
1048
|
+
await prior;
|
|
1049
|
+
return release;
|
|
1050
|
+
}
|
|
1051
|
+
return {
|
|
1052
|
+
async begin() {
|
|
1053
|
+
if (closed) relayFailure("relay_store_unavailable", "memory relay store is closed");
|
|
1054
|
+
const release = await acquire();
|
|
1055
|
+
const staged = copyTables(committed);
|
|
1056
|
+
let settled = false;
|
|
1057
|
+
const open = () => {
|
|
1058
|
+
if (settled) relayFailure("relay_internal", "relay transaction already settled");
|
|
1059
|
+
return staged;
|
|
1060
|
+
};
|
|
1061
|
+
const settle = (apply) => {
|
|
1062
|
+
if (settled) relayFailure("relay_internal", "relay transaction already settled");
|
|
1063
|
+
settled = true;
|
|
1064
|
+
if (apply) committed = staged;
|
|
1065
|
+
release();
|
|
1066
|
+
};
|
|
1067
|
+
return {
|
|
1068
|
+
async lockAuthorizationRequest(requestId) {
|
|
1069
|
+
return read(open().requests, requestId, parseAuthorizationRequestRecord);
|
|
1070
|
+
},
|
|
1071
|
+
async insertAuthorizationRequest(record) {
|
|
1072
|
+
return insert(open().requests, record.requestId, record);
|
|
1073
|
+
},
|
|
1074
|
+
async lockAuthorizationDecision(requestId) {
|
|
1075
|
+
return read(open().decisions, requestId, parseAuthorizationDecisionRecord);
|
|
1076
|
+
},
|
|
1077
|
+
async insertAuthorizationDecision(record) {
|
|
1078
|
+
return insert(open().decisions, record.requestId, record);
|
|
1079
|
+
},
|
|
1080
|
+
async lockAuthorizationCode(codeHash) {
|
|
1081
|
+
return read(open().codes, codeHash, parseAuthorizationCodeRecord);
|
|
1082
|
+
},
|
|
1083
|
+
async insertAuthorizationCode(record) {
|
|
1084
|
+
return insert(open().codes, record.codeHash, record);
|
|
1085
|
+
},
|
|
1086
|
+
async consumeAuthorizationCode(codeHash, consumedAt) {
|
|
1087
|
+
const codes = open().codes;
|
|
1088
|
+
const record = read(codes, codeHash, parseAuthorizationCodeRecord);
|
|
1089
|
+
if (!record || record.consumedAt !== null) return false;
|
|
1090
|
+
codes.set(codeHash, Object.freeze({
|
|
1091
|
+
...record,
|
|
1092
|
+
consumedAt
|
|
1093
|
+
}));
|
|
1094
|
+
return true;
|
|
1095
|
+
},
|
|
1096
|
+
async lockCapabilityInvalidation(grantId) {
|
|
1097
|
+
return read(open().invalidations, grantId, parseCapabilityInvalidationRecord);
|
|
1098
|
+
},
|
|
1099
|
+
async insertCapabilityInvalidation(record) {
|
|
1100
|
+
return insert(open().invalidations, record.grantId, record);
|
|
1101
|
+
},
|
|
1102
|
+
async lockEncryptedArtifact(artifactId) {
|
|
1103
|
+
return read(open().artifacts, artifactId, parseEncryptedArtifactRecord);
|
|
1104
|
+
},
|
|
1105
|
+
async insertEncryptedArtifact(record) {
|
|
1106
|
+
return insert(open().artifacts, record.artifactId, record);
|
|
1107
|
+
},
|
|
1108
|
+
async claimEncryptedArtifact(artifactId, claimedAt) {
|
|
1109
|
+
const artifacts = open().artifacts;
|
|
1110
|
+
const record = read(artifacts, artifactId, parseEncryptedArtifactRecord);
|
|
1111
|
+
if (!record || record.claimedAt !== null) return false;
|
|
1112
|
+
artifacts.set(artifactId, Object.freeze({
|
|
1113
|
+
...record,
|
|
1114
|
+
claimedAt
|
|
1115
|
+
}));
|
|
1116
|
+
return true;
|
|
1117
|
+
},
|
|
1118
|
+
async commit() {
|
|
1119
|
+
settle(true);
|
|
1120
|
+
},
|
|
1121
|
+
async rollback() {
|
|
1122
|
+
if (!settled) settle(false);
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
},
|
|
1126
|
+
async close() {
|
|
1127
|
+
closed = true;
|
|
1128
|
+
committed = emptyTables();
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
//#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 };
|
|
1134
|
+
|
|
1135
|
+
//# sourceMappingURL=index.js.map
|