@seams/wallet-server 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/artifact-manifest.json +23 -13
- package/cloudflare-router-ab/build/deriver-a/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/deriver-b/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/router/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/signing-worker/build-stamp.json +1 -1
- package/cloudflare-router-ab/build/tenant-root-control-plane/build-stamp.json +1 -1
- package/dist/esm/application-auth.js +2 -0
- package/dist/esm/auth/applicationAuth.js +85 -0
- package/dist/esm/auth/externalAuthService.js +58 -7
- package/dist/esm/auth/externalAuthStore.js +157 -21
- package/dist/esm/auth/externalOwnerOperations.js +82 -0
- package/dist/esm/cloud-host.js +4 -4
- package/dist/esm/core/nearRelayerAccountProvisioning.js +1 -1
- package/dist/esm/router/cloudflare/d1/auth/d1AuthorizedOperationRouteService.js +2 -0
- package/dist/esm/router/cloudflare/d1/auth/d1RecordedOwnerOperation.js +43 -0
- package/dist/esm/router/cloudflare/d1/auth/d1RouterApiAuthService.js +22 -1
- package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRecoveryService.js +1 -1
- package/dist/esm/router/cloudflare/d1/emailOtp/d1EmailOtpRegistrationEnrollmentFinalizer.js +1 -1
- package/dist/esm/router/cloudflare/d1/emailOtp/d1GoogleEmailOtpSessionResolver.js +1 -1
- package/dist/esm/router/cloudflare/d1/passkeyCustody/d1PasskeyCustodyRouteService.js +2 -2
- package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletCustodyCommitStore.js +2 -2
- package/dist/esm/router/cloudflare/d1/passkeyCustody/d1WalletRecoveryGoogleEmailOtpService.js +2 -2
- package/dist/esm/router/cloudflare/d1/registration/d1RegistrationCeremonyRecords.js +9 -0
- package/dist/esm/router/cloudflare/d1/registration/d1WalletExecutionAuthority.js +9 -16
- package/dist/esm/router/cloudflare/d1/registration/d1WalletRegistrationService.js +43 -1
- package/dist/esm/router/cloudflare/d1/registration/d1WalletRegistrationSetup.js +12 -0
- package/dist/esm/router/cloudflare/d1/registration/d1WalletRegistrationSetupExecution.js +1 -0
- package/dist/esm/router/cloudflare/d1/registration/registrationCompletionWrite.js +1 -0
- package/dist/esm/router/cloudflare/d1/registration/walletRegistrationSessionCommitReceipt.js +35 -0
- package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodBoundary.js +1 -1
- package/dist/esm/router/cloudflare/d1/wallet/d1WalletAuthMethodService.js +4 -3
- package/dist/esm/router/cloudflare.js +1785 -1489
- package/dist/esm/router/domains/ed25519Yao/export/routerAbEd25519YaoExport.js +2 -2
- package/dist/esm/router/domains/emailOtp/emailOtpRouteHandlers.js +1 -1
- package/dist/esm/router/domains/passkeyCustody/walletCustodyRegistrationCommit.js +1 -1
- package/dist/esm/router/domains/signingOperations/routerAbEd25519NormalSigningRoute.js +35 -1
- package/dist/esm/router/domains/walletRegistration/walletRegistrationReservation.js +17 -2
- package/dist/esm/router/domains/walletRegistration/walletRegistrationRoutes.js +9 -1
- package/dist/esm/router/domains/walletUnlock/walletUnlockRouteHandlers.js +1 -1
- package/dist/esm/router/transport/fetch/routes/ecdsaOperationStepUpFactor.js +2 -2
- package/dist/esm/router/transport/fetch/routes/externalAuth.js +139 -32
- package/dist/esm/router/transport/fetch/routes/passkeyCustody.js +44 -304
- package/dist/esm/router/transport/fetch/routes/sessions.js +2 -2
- package/dist/esm/router/transport/fetch/routes/thresholdEd25519.js +2 -2
- package/dist/esm/router/transport/fetch/routes/walletRegistration.js +2 -2
- package/dist/esm/router/transport/fetch/walletOwnerOperationAuthorization.js +306 -0
- package/dist/esm/shared-ts/src/auth/externalAuth.js +87 -14
- package/dist/esm/shared-ts/src/authorization/index.js +1 -1
- package/dist/esm/shared-ts/src/authorization/{walletCustodyOperation.js → walletOwnerOperation.js} +4 -4
- package/dist/esm/shared-ts/src/wallet-recovery/index.js +3 -3
- package/dist/esm/wasm/evm_crypto/pkg/evm_crypto_bg.wasm +0 -0
- package/dist/esm/wasm/near_signer/pkg/wasm_signer_worker_bg.wasm +0 -0
- package/dist/esm/wasm/shamir3pass_runtime/pkg/shamir3pass_runtime_bg.wasm +0 -0
- package/dist/types/shared-ts/src/auth/externalAuth.d.ts +51 -8
- package/dist/types/shared-ts/src/authorization/index.d.ts +1 -1
- package/dist/types/shared-ts/src/authorization/walletOwnerOperation.d.ts +10 -0
- package/dist/types/shared-ts/src/utils/registrationEstablishedSession.d.ts +4 -0
- package/dist/types/shared-ts/src/utils/registrationIntent.d.ts +2 -0
- package/dist/types/shared-ts/src/utils/walletManagementProtocol.d.ts +1 -1
- package/dist/types/wallet-server/src/application-auth.d.ts +3 -0
- package/dist/types/wallet-server/src/auth/applicationAuth.d.ts +29 -0
- package/dist/types/wallet-server/src/auth/externalAuthService.d.ts +29 -10
- package/dist/types/wallet-server/src/auth/externalAuthStore.d.ts +60 -6
- package/dist/types/wallet-server/src/auth/externalOwnerOperations.d.ts +18 -0
- package/dist/types/wallet-server/src/cloud-host.d.ts +1 -1
- package/dist/types/wallet-server/src/core/threeRouteRegistrationContracts.d.ts +12 -0
- package/dist/types/wallet-server/src/router/cloudflare/d1/auth/d1AuthorizedOperationRouteService.d.ts +5 -1
- package/dist/types/wallet-server/src/router/cloudflare/d1/auth/d1RecordedOwnerOperation.d.ts +16 -0
- package/dist/types/wallet-server/src/router/cloudflare/d1/registration/d1RegistrationCeremonyRecords.d.ts +1 -0
- package/dist/types/wallet-server/src/router/cloudflare/d1/registration/d1WalletRegistrationSetup.d.ts +2 -0
- package/dist/types/wallet-server/src/router/cloudflare/d1/registration/walletRegistrationSessionCommitReceipt.d.ts +7 -1
- package/dist/types/wallet-server/src/router/cloudflare/d1/wallet/d1WalletAuthMethodService.d.ts +1 -0
- package/dist/types/wallet-server/src/router/domains/signingOperations/routerAbEd25519NormalSigningRoute.d.ts +2 -2
- package/dist/types/wallet-server/src/router/domains/signingOperations/routerAbOperationStepUp.d.ts +1 -1
- package/dist/types/wallet-server/src/router/domains/walletRegistration/walletRegistrationInputs.d.ts +1 -0
- package/dist/types/wallet-server/src/router/domains/walletRegistration/walletRegistrationReservation.d.ts +11 -6
- package/dist/types/wallet-server/src/router/framework/authServicePort.d.ts +11 -0
- package/dist/types/wallet-server/src/router/transport/fetch/walletOwnerOperationAuthorization.d.ts +23 -0
- package/migrations/d1-signer/0086_application_auth.sql +67 -0
- package/migrations/d1-signer/0087_registration_without_session.sql +43 -0
- package/package.json +7 -2
- package/dist/types/shared-ts/src/authorization/walletCustodyOperation.d.ts +0 -10
|
@@ -226,7 +226,7 @@ var DefaultRouterAbNormalSigningAdmissionAdapter = class {
|
|
|
226
226
|
case "project_policy_rejected": return admissionFailure(403, "project_policy_rejected", "Router A/B normal-signing project policy rejected the request");
|
|
227
227
|
case "rate_limited": return admissionFailure(429, "rate_limited", "Router A/B normal-signing request is rate limited");
|
|
228
228
|
case "abuse_rejected": return admissionFailure(403, "abuse_rejected", "Router A/B normal-signing abuse policy rejected the request");
|
|
229
|
-
default: return assertNever$
|
|
229
|
+
default: return assertNever$16(policy);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
};
|
|
@@ -283,7 +283,7 @@ function abusePrincipalKey(input) {
|
|
|
283
283
|
input.curve
|
|
284
284
|
].join("");
|
|
285
285
|
}
|
|
286
|
-
function assertNever$
|
|
286
|
+
function assertNever$16(value) {
|
|
287
287
|
throw new Error(`Unexpected Router A/B normal-signing admission branch: ${String(value)}`);
|
|
288
288
|
}
|
|
289
289
|
//#endregion
|
|
@@ -4957,7 +4957,7 @@ function normalizeProjectPolicyDecision(decision) {
|
|
|
4957
4957
|
kind: "rejected",
|
|
4958
4958
|
retryAfterMs: requirePositiveInteger$2("retryAfterMs", decision.retryAfterMs)
|
|
4959
4959
|
};
|
|
4960
|
-
default: return assertNever$
|
|
4960
|
+
default: return assertNever$15(decision);
|
|
4961
4961
|
}
|
|
4962
4962
|
}
|
|
4963
4963
|
function normalizeAbuseDecision(decision) {
|
|
@@ -4971,7 +4971,7 @@ function normalizeAbuseDecision(decision) {
|
|
|
4971
4971
|
kind: "rejected",
|
|
4972
4972
|
retryAfterMs: requirePositiveInteger$2("retryAfterMs", decision.retryAfterMs)
|
|
4973
4973
|
};
|
|
4974
|
-
default: return assertNever$
|
|
4974
|
+
default: return assertNever$15(decision);
|
|
4975
4975
|
}
|
|
4976
4976
|
}
|
|
4977
4977
|
function parseProjectPolicyDecision(row) {
|
|
@@ -5015,7 +5015,7 @@ function requirePositiveInteger$2(label, value) {
|
|
|
5015
5015
|
}
|
|
5016
5016
|
throw new Error(`${label} must be a positive integer`);
|
|
5017
5017
|
}
|
|
5018
|
-
function assertNever$
|
|
5018
|
+
function assertNever$15(value) {
|
|
5019
5019
|
throw new Error(`Unexpected Router A/B normal-signing admission branch: ${String(value)}`);
|
|
5020
5020
|
}
|
|
5021
5021
|
function parsePolicyRow(row) {
|
|
@@ -5190,19 +5190,61 @@ const ED25519_CREDENTIAL_POLICY_PROJECTION = credentialPolicyProjection({
|
|
|
5190
5190
|
//#endregion
|
|
5191
5191
|
//#region ../shared-ts/src/auth/externalAuth.ts
|
|
5192
5192
|
const EXTERNAL_AUTH_PATH = "/wallet/external-auth";
|
|
5193
|
+
function parseExternalAuthProvider(raw) {
|
|
5194
|
+
const value = requireRecord$1(raw, "external authentication provider");
|
|
5195
|
+
switch (value.kind) {
|
|
5196
|
+
case "disabled": return { kind: "disabled" };
|
|
5197
|
+
case "google": return {
|
|
5198
|
+
kind: "google",
|
|
5199
|
+
clientId: requireCanonicalString(value.clientId, "clientId")
|
|
5200
|
+
};
|
|
5201
|
+
case "application": {
|
|
5202
|
+
const issuer = requireCanonicalString(value.issuer, "issuer");
|
|
5203
|
+
const url = new URL(issuer);
|
|
5204
|
+
if (url.protocol !== "https:" || url.origin !== issuer) throw new Error("Application issuer must be a canonical HTTPS origin");
|
|
5205
|
+
return {
|
|
5206
|
+
kind: "application",
|
|
5207
|
+
issuer,
|
|
5208
|
+
publicKeyB64u: parseEd25519PublicKeyB64u(value.publicKeyB64u, "publicKeyB64u")
|
|
5209
|
+
};
|
|
5210
|
+
}
|
|
5211
|
+
default: throw new Error("Unsupported external authentication provider");
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
function externalAuthConfiguration(provider, revision) {
|
|
5215
|
+
switch (provider.kind) {
|
|
5216
|
+
case "disabled": return {
|
|
5217
|
+
kind: "disabled",
|
|
5218
|
+
revision
|
|
5219
|
+
};
|
|
5220
|
+
case "google": return {
|
|
5221
|
+
kind: "google",
|
|
5222
|
+
revision,
|
|
5223
|
+
clientId: provider.clientId
|
|
5224
|
+
};
|
|
5225
|
+
case "application": return {
|
|
5226
|
+
kind: "application",
|
|
5227
|
+
revision,
|
|
5228
|
+
issuer: provider.issuer,
|
|
5229
|
+
publicKeyB64u: provider.publicKeyB64u
|
|
5230
|
+
};
|
|
5231
|
+
default: return assertNever$14(provider);
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5193
5234
|
function parseExternalAuthConfiguration(raw) {
|
|
5194
5235
|
const value = requireRecord$1(raw, "external authentication configuration");
|
|
5195
5236
|
if (typeof value.revision !== "number" || !Number.isSafeInteger(value.revision) || value.revision < 0) throw new Error("Invalid external authentication revision");
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5237
|
+
return externalAuthConfiguration(parseExternalAuthProvider(value), value.revision);
|
|
5238
|
+
}
|
|
5239
|
+
function serializeExternalIdentity(identity) {
|
|
5240
|
+
return JSON.stringify([
|
|
5241
|
+
identity.provider,
|
|
5242
|
+
identity.issuer,
|
|
5243
|
+
identity.subject
|
|
5244
|
+
]);
|
|
5245
|
+
}
|
|
5246
|
+
function assertNever$14(value) {
|
|
5247
|
+
throw new Error(`Unsupported external authentication provider: ${String(value)}`);
|
|
5206
5248
|
}
|
|
5207
5249
|
function externalAuthFailure(code) {
|
|
5208
5250
|
return {
|
|
@@ -5212,105 +5254,194 @@ function externalAuthFailure(code) {
|
|
|
5212
5254
|
};
|
|
5213
5255
|
}
|
|
5214
5256
|
//#endregion
|
|
5215
|
-
//#region src/
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
return "";
|
|
5257
|
+
//#region ../shared-ts/src/authorization/walletOwnerOperation.ts
|
|
5258
|
+
/** Public challenge digest for a single wallet-administration factor proof. */
|
|
5259
|
+
async function computeWalletOwnerAdminChallengeDigest(input) {
|
|
5260
|
+
return sha256Utf8DigestB64u(`seams:wallet-custody:challenge:v1|${alphabetizeStringify({
|
|
5261
|
+
walletId: input.walletId,
|
|
5262
|
+
operation: input.operation,
|
|
5263
|
+
payload: input.payload,
|
|
5264
|
+
requestOrigin: input.requestOrigin
|
|
5265
|
+
})}`);
|
|
5225
5266
|
}
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5267
|
+
//#endregion
|
|
5268
|
+
//#region src/auth/externalOwnerOperations.ts
|
|
5269
|
+
async function readExternalOwnerOperation(input) {
|
|
5270
|
+
const attempt = input.phase === "prepare" ? await input.store.prepareOwnerChallenge(input.binding) : await input.store.readOwnerAttempt(input.binding);
|
|
5271
|
+
if (!attempt) return externalAuthFailure("access_denied");
|
|
5272
|
+
if (attempt.challenge.kind !== "ready") return externalAuthFailure("access_denied");
|
|
5273
|
+
if (attempt.status === "verified" && attempt.challenge.expiresAtMs <= input.binding.nowMs) return externalAuthFailure("expired");
|
|
5274
|
+
const identity = serializeExternalIdentity(attempt.identity);
|
|
5275
|
+
let operation;
|
|
5276
|
+
let payload;
|
|
5277
|
+
switch (attempt.purpose) {
|
|
5278
|
+
case "associate":
|
|
5279
|
+
operation = "external_identity_associate";
|
|
5280
|
+
payload = {
|
|
5281
|
+
attemptId: attempt.attemptId,
|
|
5282
|
+
identity,
|
|
5283
|
+
originBinding: attempt.originBinding,
|
|
5284
|
+
revision: attempt.trustRevision,
|
|
5285
|
+
expiresAtMs: attempt.challenge.expiresAtMs
|
|
5286
|
+
};
|
|
5287
|
+
break;
|
|
5288
|
+
case "unlink":
|
|
5289
|
+
operation = "external_identity_unlink";
|
|
5290
|
+
payload = {
|
|
5291
|
+
attemptId: attempt.attemptId,
|
|
5292
|
+
associationId: attempt.associationId,
|
|
5293
|
+
identity,
|
|
5294
|
+
originBinding: attempt.originBinding,
|
|
5295
|
+
revision: attempt.trustRevision,
|
|
5296
|
+
expiresAtMs: attempt.challenge.expiresAtMs
|
|
5297
|
+
};
|
|
5298
|
+
break;
|
|
5299
|
+
default: return assertNever$13(attempt);
|
|
5300
|
+
}
|
|
5301
|
+
const challengeB64u = await computeWalletOwnerAdminChallengeDigest({
|
|
5302
|
+
walletId: attempt.walletId,
|
|
5303
|
+
operation,
|
|
5304
|
+
payload,
|
|
5305
|
+
requestOrigin: input.requestOrigin
|
|
5306
|
+
});
|
|
5307
|
+
return {
|
|
5308
|
+
ok: true,
|
|
5309
|
+
value: {
|
|
5310
|
+
attempt,
|
|
5311
|
+
operation,
|
|
5312
|
+
payload,
|
|
5313
|
+
challengeB64u
|
|
5232
5314
|
}
|
|
5315
|
+
};
|
|
5316
|
+
}
|
|
5317
|
+
function externalOwnerChallenge(operation) {
|
|
5318
|
+
const attempt = operation.attempt;
|
|
5319
|
+
if (attempt.challenge.kind !== "ready") throw new Error("Owner challenge is not prepared");
|
|
5320
|
+
switch (attempt.purpose) {
|
|
5321
|
+
case "associate": return {
|
|
5322
|
+
kind: "associate",
|
|
5323
|
+
walletId: attempt.walletId,
|
|
5324
|
+
attemptId: attempt.attemptId,
|
|
5325
|
+
identity: attempt.identity,
|
|
5326
|
+
challengeB64u: operation.challengeB64u,
|
|
5327
|
+
expiresAtMs: attempt.challenge.expiresAtMs
|
|
5328
|
+
};
|
|
5329
|
+
case "unlink": return {
|
|
5330
|
+
kind: "unlink",
|
|
5331
|
+
walletId: attempt.walletId,
|
|
5332
|
+
attemptId: attempt.attemptId,
|
|
5333
|
+
associationId: attempt.associationId,
|
|
5334
|
+
identity: attempt.identity,
|
|
5335
|
+
challengeB64u: operation.challengeB64u,
|
|
5336
|
+
expiresAtMs: attempt.challenge.expiresAtMs
|
|
5337
|
+
};
|
|
5338
|
+
default: return assertNever$13(attempt);
|
|
5233
5339
|
}
|
|
5234
|
-
return null;
|
|
5235
5340
|
}
|
|
5236
|
-
function
|
|
5237
|
-
|
|
5238
|
-
if (!value) return null;
|
|
5239
|
-
const maybeForwarded = value.includes(",") ? value.split(",")[0] : value;
|
|
5240
|
-
return String(maybeForwarded || "").trim() || null;
|
|
5341
|
+
function assertNever$13(value) {
|
|
5342
|
+
throw new Error(`Unsupported external owner operation: ${String(value)}`);
|
|
5241
5343
|
}
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5344
|
+
//#endregion
|
|
5345
|
+
//#region ../shared-ts/src/authorization/operationFingerprint.ts
|
|
5346
|
+
const CAPABILITY_OPERATION_FINGERPRINT_DOMAIN_V1 = "seams:authorization:capability-operation-fingerprint:v1";
|
|
5347
|
+
var CapabilityOperationEnvelopeProof = class {
|
|
5348
|
+
tenantId;
|
|
5349
|
+
principalId;
|
|
5350
|
+
capabilityId;
|
|
5351
|
+
operationId;
|
|
5352
|
+
operation;
|
|
5353
|
+
digests;
|
|
5354
|
+
retainProof() {
|
|
5355
|
+
return true;
|
|
5356
|
+
}
|
|
5357
|
+
constructor(fields) {
|
|
5358
|
+
this.retainProof();
|
|
5359
|
+
this.tenantId = fields.tenantId;
|
|
5360
|
+
this.principalId = fields.principalId;
|
|
5361
|
+
this.capabilityId = fields.capabilityId;
|
|
5362
|
+
this.operationId = fields.operationId;
|
|
5363
|
+
this.operation = fields.operation;
|
|
5364
|
+
this.digests = fields.digests;
|
|
5365
|
+
}
|
|
5366
|
+
};
|
|
5367
|
+
function buildCapabilityOperationEnvelope(fields) {
|
|
5368
|
+
return new CapabilityOperationEnvelopeProof({
|
|
5369
|
+
tenantId: fields.tenantId,
|
|
5370
|
+
principalId: fields.principalId,
|
|
5371
|
+
capabilityId: fields.capabilityId,
|
|
5372
|
+
operationId: fields.operationId,
|
|
5373
|
+
operation: fields.operation,
|
|
5374
|
+
digests: {
|
|
5375
|
+
laneDigest: fields.digests.laneDigest,
|
|
5376
|
+
intentDigest: fields.digests.intentDigest,
|
|
5377
|
+
displayDigest: fields.digests.displayDigest
|
|
5378
|
+
}
|
|
5379
|
+
});
|
|
5248
5380
|
}
|
|
5249
|
-
function
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5381
|
+
function canonicalCapabilityOperationFingerprintPreimageV1(envelope) {
|
|
5382
|
+
return `${CAPABILITY_OPERATION_FINGERPRINT_DOMAIN_V1}|${alphabetizeStringify({
|
|
5383
|
+
tenantId: envelope.tenantId,
|
|
5384
|
+
principalId: envelope.principalId,
|
|
5385
|
+
capabilityId: envelope.capabilityId,
|
|
5386
|
+
operationId: envelope.operationId,
|
|
5387
|
+
operation: envelope.operation,
|
|
5388
|
+
digests: envelope.digests
|
|
5389
|
+
})}`;
|
|
5254
5390
|
}
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
function failure$6(code, message) {
|
|
5258
|
-
return {
|
|
5259
|
-
ok: false,
|
|
5260
|
-
code,
|
|
5261
|
-
message
|
|
5262
|
-
};
|
|
5391
|
+
async function computeCapabilityOperationFingerprintDigest(envelope) {
|
|
5392
|
+
return parseCapabilityOperationFingerprintDigest(base64UrlEncode(await sha256BytesUtf8(canonicalCapabilityOperationFingerprintPreimageV1(envelope))));
|
|
5263
5393
|
}
|
|
5264
|
-
function
|
|
5265
|
-
return
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
};
|
|
5394
|
+
function parseCapabilityOperationFingerprintDigest(value) {
|
|
5395
|
+
return parseDigestB64u(value);
|
|
5396
|
+
}
|
|
5397
|
+
function parseSigningOperationFingerprintDigest(value) {
|
|
5398
|
+
if (typeof value !== "string" || !value.startsWith("sha256:")) throw new Error("signing operation fingerprint must use the sha256: prefix");
|
|
5399
|
+
return parseDigestB64u(value.slice(7));
|
|
5271
5400
|
}
|
|
5272
5401
|
//#endregion
|
|
5273
|
-
//#region src/
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5402
|
+
//#region ../shared-ts/src/utils/emailOtpDomain.ts
|
|
5403
|
+
const EMAIL_OTP_CHANNEL = "email_otp";
|
|
5404
|
+
/** Domain label that opens the AAD binding a released Email OTP factor to its challenge. */
|
|
5405
|
+
const EMAIL_OTP_FACTOR_RELEASE_AAD_DOMAIN_V1 = "seams/email-otp/factor-release/v1";
|
|
5406
|
+
const WALLET_UNLOCK_BACKENDS = ["passkey", EMAIL_OTP_CHANNEL];
|
|
5407
|
+
const WALLET_EMAIL_OTP_LOGIN_OPERATIONS = [
|
|
5408
|
+
"wallet_unlock",
|
|
5409
|
+
"transaction_sign",
|
|
5410
|
+
"export_key"
|
|
5411
|
+
];
|
|
5412
|
+
const WALLET_EMAIL_OTP_EXPORT_OPERATION = "export_key";
|
|
5413
|
+
const WALLET_EMAIL_OTP_UNLOCK_OPERATION = "wallet_unlock";
|
|
5414
|
+
const WALLET_EMAIL_OTP_TRANSACTION_SIGN_OPERATION = "transaction_sign";
|
|
5415
|
+
function isWalletUnlockBackend(value) {
|
|
5416
|
+
return WALLET_UNLOCK_BACKENDS.includes(value);
|
|
5279
5417
|
}
|
|
5280
|
-
function
|
|
5281
|
-
|
|
5282
|
-
for (const raw of String(input || "").split(",")) {
|
|
5283
|
-
const s = raw.trim();
|
|
5284
|
-
if (!s) continue;
|
|
5285
|
-
try {
|
|
5286
|
-
const u = new URL(s);
|
|
5287
|
-
out.add(normalizeParsedCorsOrigin(u));
|
|
5288
|
-
} catch {
|
|
5289
|
-
const stripped = s.replace(/\/$/, "");
|
|
5290
|
-
if (stripped) out.add(stripped);
|
|
5291
|
-
}
|
|
5292
|
-
}
|
|
5293
|
-
return Array.from(out);
|
|
5418
|
+
function isWalletEmailOtpLoginOperation(value) {
|
|
5419
|
+
return WALLET_EMAIL_OTP_LOGIN_OPERATIONS.includes(value);
|
|
5294
5420
|
}
|
|
5421
|
+
//#endregion
|
|
5422
|
+
//#region ../shared-ts/src/utils/base58.ts
|
|
5295
5423
|
/**
|
|
5296
|
-
*
|
|
5297
|
-
*
|
|
5424
|
+
* Encodes binary data to base58 string using Bitcoin's base58 alphabet.
|
|
5425
|
+
*
|
|
5426
|
+
* @param data - Binary data to encode (Uint8Array, ArrayBuffer, or number[])
|
|
5427
|
+
* @returns Base58-encoded string
|
|
5298
5428
|
*/
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
if (
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5429
|
+
const base58Encode = (data) => {
|
|
5430
|
+
if (data instanceof ArrayBuffer) return bs58.encode(new Uint8Array(data));
|
|
5431
|
+
if (Array.isArray(data)) return bs58.encode(new Uint8Array(data));
|
|
5432
|
+
return bs58.encode(data);
|
|
5433
|
+
};
|
|
5434
|
+
/**
|
|
5435
|
+
* Decodes a base58 string to binary data using Bitcoin's base58 alphabet.
|
|
5436
|
+
* Returns a Uint8Array containing the decoded bytes.
|
|
5437
|
+
*
|
|
5438
|
+
* @param base58String - Base58-encoded string to decode
|
|
5439
|
+
* @returns Uint8Array containing the decoded bytes
|
|
5440
|
+
* @throws Error if input contains invalid base58 characters
|
|
5441
|
+
*/
|
|
5442
|
+
const base58Decode = (base58String) => {
|
|
5443
|
+
return bs58.decode(base58String);
|
|
5444
|
+
};
|
|
5314
5445
|
//#endregion
|
|
5315
5446
|
//#region ../shared-ts/src/device-linking/contracts.ts
|
|
5316
5447
|
function isLinkSessionTerminalFailureV1(state) {
|
|
@@ -8419,170 +8550,1318 @@ function parseFixedB64u$1(value, expectedBytes, label) {
|
|
|
8419
8550
|
return bytes;
|
|
8420
8551
|
}
|
|
8421
8552
|
//#endregion
|
|
8422
|
-
//#region
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
if (
|
|
8427
|
-
ok: false,
|
|
8428
|
-
reason: "missing",
|
|
8429
|
-
message: "Router trace ID is missing"
|
|
8430
|
-
};
|
|
8431
|
-
if (typeof raw !== "string" || !TRACE_ID_PATTERN.test(raw)) return {
|
|
8432
|
-
ok: false,
|
|
8433
|
-
reason: "invalid",
|
|
8434
|
-
message: "Router trace ID must be 32 lowercase hexadecimal characters"
|
|
8435
|
-
};
|
|
8553
|
+
//#region src/authorization/domain.ts
|
|
8554
|
+
function buildExactWalletSessionQuotaProjectionV1(fields) {
|
|
8555
|
+
requireNonnegativeInteger(fields.remainingUses, "Wallet Session quota remaining uses");
|
|
8556
|
+
requirePositiveTime(fields.expiresAtMs, "Wallet Session quota expiry");
|
|
8557
|
+
if (fields.lifecycle === "exhausted" !== (fields.remainingUses === 0)) throw new Error("Wallet Session quota lifecycle disagrees with its remaining uses");
|
|
8436
8558
|
return {
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8559
|
+
kind: "exact_wallet_session_quota_projection_v1",
|
|
8560
|
+
lifecycle: fields.lifecycle,
|
|
8561
|
+
tenantId: fields.tenantId,
|
|
8562
|
+
principalId: fields.principalId,
|
|
8563
|
+
walletSessionId: fields.walletSessionId,
|
|
8564
|
+
quotaId: fields.quotaId,
|
|
8565
|
+
remainingUses: fields.remainingUses,
|
|
8566
|
+
expiresAtMs: fields.expiresAtMs
|
|
8442
8567
|
};
|
|
8443
8568
|
}
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8569
|
+
/**
|
|
8570
|
+
* Every capability subject must resolve through the authoritative active
|
|
8571
|
+
* authority that the authorization names. A signing or export subject resolves
|
|
8572
|
+
* only when its key family is activated and names the same material the
|
|
8573
|
+
* authority currently holds; an administration subject resolves only when it
|
|
8574
|
+
* names that same authority. Anything else is a capability the caller can no
|
|
8575
|
+
* longer exercise, not a corrupt row.
|
|
8576
|
+
*/
|
|
8577
|
+
function exactWalletSessionCapabilitySubjectsResolveAuthority(input) {
|
|
8578
|
+
return input.session.capabilitySubjects.every((subject) => {
|
|
8579
|
+
switch (subject.kind) {
|
|
8580
|
+
case "sign":
|
|
8581
|
+
case "export_keys": {
|
|
8582
|
+
const activation = subject.keyFamily === "ed25519" ? input.signerActivations.ed25519 : input.signerActivations.ecdsa;
|
|
8583
|
+
return activation !== void 0 && mpcMaterialActivationRefsEqual(activation.materialActivation, subject.materialActivation);
|
|
8584
|
+
}
|
|
8585
|
+
case "link_devices":
|
|
8586
|
+
case "revoke_devices": return subject.authorityId === input.session.authorityId;
|
|
8587
|
+
default: return false;
|
|
8588
|
+
}
|
|
8589
|
+
});
|
|
8453
8590
|
}
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
const
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8591
|
+
/**
|
|
8592
|
+
* The digest-free authorization projection published to the browser. It carries
|
|
8593
|
+
* the exact wallet, authority, method, capability, and lifetime identity a
|
|
8594
|
+
* reader needs to reconcile its own record, and never the credential digest.
|
|
8595
|
+
*/
|
|
8596
|
+
function projectExactWalletSessionAuthorizationV1(session) {
|
|
8597
|
+
const capabilitySubjects = session.capabilitySubjects.map((subject) => {
|
|
8598
|
+
switch (subject.kind) {
|
|
8599
|
+
case "sign":
|
|
8600
|
+
case "export_keys": return {
|
|
8601
|
+
kind: subject.kind,
|
|
8602
|
+
keyFamily: subject.keyFamily,
|
|
8603
|
+
materialActivation: subject.materialActivation
|
|
8604
|
+
};
|
|
8605
|
+
case "link_devices":
|
|
8606
|
+
case "revoke_devices": return { kind: subject.kind };
|
|
8607
|
+
default: throw new Error("Issued linked-device Wallet Session subject is invalid");
|
|
8608
|
+
}
|
|
8609
|
+
});
|
|
8610
|
+
const first = capabilitySubjects[0];
|
|
8611
|
+
if (!first) throw new Error("Issued linked-device Wallet Session has no subjects");
|
|
8612
|
+
return parseActiveWalletSessionV1({
|
|
8613
|
+
kind: "active_wallet_session_v1",
|
|
8614
|
+
walletId: session.walletId,
|
|
8615
|
+
authorityId: session.authorityId,
|
|
8616
|
+
authMethodId: session.walletAuthMethodId,
|
|
8617
|
+
authorizationId: session.authorizationId,
|
|
8618
|
+
quotaId: session.quotaId,
|
|
8619
|
+
authorityDigestB64u: session.authorityDigestB64u,
|
|
8620
|
+
authorityRevocationEpoch: session.authorityRevocationEpoch,
|
|
8621
|
+
capabilitySubjects: [first, ...capabilitySubjects.slice(1)],
|
|
8622
|
+
issuedAtMs: session.createdAtMs,
|
|
8623
|
+
expiresAtMs: session.expiresAtMs
|
|
8485
8624
|
});
|
|
8486
8625
|
}
|
|
8487
|
-
function
|
|
8488
|
-
|
|
8626
|
+
function projectActiveWalletSession(issued) {
|
|
8627
|
+
if (issued.quota.quotaId !== issued.session.quotaId) throw new Error("Issued Wallet Session quota does not belong to its authorization");
|
|
8628
|
+
return projectExactWalletSessionAuthorizationV1(issued.session);
|
|
8489
8629
|
}
|
|
8490
|
-
function
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8630
|
+
function appendWalletSessionSignerSubjects(subjects, kind, signerActivations) {
|
|
8631
|
+
if (signerActivations.keyFamilies.length === 1 && signerActivations.keyFamilies[0] === "ed25519") {
|
|
8632
|
+
if (!signerActivations.ed25519) throw new Error("Ed25519 signer activation is missing");
|
|
8633
|
+
subjects.push({
|
|
8634
|
+
kind,
|
|
8635
|
+
keyFamily: "ed25519",
|
|
8636
|
+
materialActivation: signerActivations.ed25519.materialActivation
|
|
8637
|
+
});
|
|
8498
8638
|
return;
|
|
8499
8639
|
}
|
|
8500
|
-
if (
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
const originNormalized = normalizeCorsOrigin(originRaw);
|
|
8509
|
-
if (originRaw && originNormalized && normalized.includes(originNormalized)) {
|
|
8510
|
-
allowedOrigin = originRaw;
|
|
8511
|
-
headers.set("Access-Control-Allow-Origin", originRaw);
|
|
8512
|
-
headers.append("Vary", "Origin");
|
|
8513
|
-
}
|
|
8514
|
-
}
|
|
8515
|
-
headers.set("Access-Control-Allow-Methods", CORS_ALLOW_METHODS);
|
|
8516
|
-
headers.set("Access-Control-Allow-Headers", CORS_ALLOW_HEADERS);
|
|
8517
|
-
if (allowedOrigin && request?.method === "OPTIONS") headers.set("Access-Control-Max-Age", String(CORS_PREFLIGHT_MAX_AGE_SECONDS));
|
|
8518
|
-
headers.set("Access-Control-Expose-Headers", "Server-Timing, X-Seams-Wallet-Region");
|
|
8519
|
-
if (allowedOrigin && allowedOrigin !== "*") headers.set("Access-Control-Allow-Credentials", "true");
|
|
8520
|
-
}
|
|
8521
|
-
async function readJson(request) {
|
|
8522
|
-
try {
|
|
8523
|
-
return await request.json();
|
|
8524
|
-
} catch {
|
|
8525
|
-
return null;
|
|
8640
|
+
if (signerActivations.keyFamilies.length === 1 && signerActivations.keyFamilies[0] === "ecdsa_secp256k1") {
|
|
8641
|
+
if (!signerActivations.ecdsa) throw new Error("ECDSA signer activation is missing");
|
|
8642
|
+
subjects.push({
|
|
8643
|
+
kind,
|
|
8644
|
+
keyFamily: "ecdsa_secp256k1",
|
|
8645
|
+
materialActivation: signerActivations.ecdsa.materialActivation
|
|
8646
|
+
});
|
|
8647
|
+
return;
|
|
8526
8648
|
}
|
|
8649
|
+
if (!signerActivations.ed25519 || !signerActivations.ecdsa) throw new Error("both-family signer activations are incomplete");
|
|
8650
|
+
subjects.push({
|
|
8651
|
+
kind,
|
|
8652
|
+
keyFamily: "ed25519",
|
|
8653
|
+
materialActivation: signerActivations.ed25519.materialActivation
|
|
8654
|
+
}, {
|
|
8655
|
+
kind,
|
|
8656
|
+
keyFamily: "ecdsa_secp256k1",
|
|
8657
|
+
materialActivation: signerActivations.ecdsa.materialActivation
|
|
8658
|
+
});
|
|
8527
8659
|
}
|
|
8528
|
-
function
|
|
8529
|
-
const
|
|
8530
|
-
|
|
8531
|
-
|
|
8660
|
+
function buildWalletSessionCapabilitySubjectsV1(authority) {
|
|
8661
|
+
const subjects = [];
|
|
8662
|
+
if (authority.permissions.includes("sign")) appendWalletSessionSignerSubjects(subjects, "sign", authority.signerActivations);
|
|
8663
|
+
if (authority.permissions.includes("export_keys")) appendWalletSessionSignerSubjects(subjects, "export_keys", authority.signerActivations);
|
|
8664
|
+
if (authority.permissions.includes("link_devices")) subjects.push({
|
|
8665
|
+
kind: "link_devices",
|
|
8666
|
+
authorityId: authority.authorityId
|
|
8532
8667
|
});
|
|
8533
|
-
|
|
8668
|
+
if (authority.permissions.includes("revoke_devices")) subjects.push({
|
|
8669
|
+
kind: "revoke_devices",
|
|
8670
|
+
authorityId: authority.authorityId
|
|
8671
|
+
});
|
|
8672
|
+
const [firstSubject, ...remainingSubjects] = subjects;
|
|
8673
|
+
if (!firstSubject) throw new Error("active wallet authority must produce capability subjects");
|
|
8674
|
+
return [firstSubject, ...remainingSubjects];
|
|
8534
8675
|
}
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
"
|
|
8539
|
-
"
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
"
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8676
|
+
function buildWalletSessionAuthorizationV2(fields) {
|
|
8677
|
+
requireNonnegativeInteger(fields.authorityRevocationEpoch, "Wallet Session authority revocation epoch");
|
|
8678
|
+
parseDigestB64u(fields.authorityDigestB64u);
|
|
8679
|
+
if (fields.capabilitySubjects.length === 0) throw new Error("Wallet Session capability subjects are required");
|
|
8680
|
+
requireOrderedTimes(fields.createdAtMs, fields.expiresAtMs, "Wallet Session authorization");
|
|
8681
|
+
const identityValues = [
|
|
8682
|
+
String(fields.authorizationId),
|
|
8683
|
+
String(fields.walletSessionId),
|
|
8684
|
+
String(fields.quotaId)
|
|
8685
|
+
];
|
|
8686
|
+
if (new Set(identityValues).size !== identityValues.length) throw new Error("Wallet Session authorization identities must be pairwise distinct");
|
|
8687
|
+
return {
|
|
8688
|
+
kind: "wallet_session_authorization_v2",
|
|
8689
|
+
tenantId: fields.tenantId,
|
|
8690
|
+
principalId: fields.principalId,
|
|
8691
|
+
walletId: fields.walletId,
|
|
8692
|
+
authorityId: fields.authorityId,
|
|
8693
|
+
walletAuthMethodId: fields.walletAuthMethodId,
|
|
8694
|
+
authorityDigestB64u: fields.authorityDigestB64u,
|
|
8695
|
+
authorityRevocationEpoch: fields.authorityRevocationEpoch,
|
|
8696
|
+
mintId: fields.mintId,
|
|
8697
|
+
authorizationId: fields.authorizationId,
|
|
8698
|
+
walletSessionId: fields.walletSessionId,
|
|
8699
|
+
quotaId: fields.quotaId,
|
|
8700
|
+
capabilitySubjects: fields.capabilitySubjects,
|
|
8701
|
+
createdAtMs: fields.createdAtMs,
|
|
8702
|
+
expiresAtMs: fields.expiresAtMs
|
|
8556
8703
|
};
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8704
|
+
}
|
|
8705
|
+
function walletSessionCapabilitySubjectsV1Equal(left, right) {
|
|
8706
|
+
if (left.length !== right.length) return false;
|
|
8707
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
8708
|
+
const leftSubject = left[index];
|
|
8709
|
+
const rightSubject = right[index];
|
|
8710
|
+
if (!leftSubject || !rightSubject || leftSubject.kind !== rightSubject.kind) return false;
|
|
8711
|
+
switch (leftSubject.kind) {
|
|
8712
|
+
case "sign":
|
|
8713
|
+
case "export_keys": break;
|
|
8714
|
+
case "link_devices":
|
|
8715
|
+
case "revoke_devices":
|
|
8716
|
+
if (rightSubject.kind !== leftSubject.kind || leftSubject.authorityId !== rightSubject.authorityId) return false;
|
|
8717
|
+
break;
|
|
8718
|
+
}
|
|
8719
|
+
if (leftSubject.kind === "sign" || leftSubject.kind === "export_keys") {
|
|
8720
|
+
if (rightSubject.kind !== leftSubject.kind || leftSubject.keyFamily !== rightSubject.keyFamily || !mpcMaterialActivationRefsEqual(leftSubject.materialActivation, rightSubject.materialActivation)) return false;
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
return true;
|
|
8724
|
+
}
|
|
8725
|
+
function walletSessionAuthorizationV2RecordsEqual(left, right) {
|
|
8726
|
+
return left.kind === right.kind && left.tenantId === right.tenantId && left.principalId === right.principalId && left.walletId === right.walletId && left.authorityId === right.authorityId && left.walletAuthMethodId === right.walletAuthMethodId && left.authorityDigestB64u === right.authorityDigestB64u && left.authorityRevocationEpoch === right.authorityRevocationEpoch && left.mintId === right.mintId && left.authorizationId === right.authorizationId && left.walletSessionId === right.walletSessionId && left.quotaId === right.quotaId && walletSessionCapabilitySubjectsV1Equal(left.capabilitySubjects, right.capabilitySubjects) && left.createdAtMs === right.createdAtMs && left.expiresAtMs === right.expiresAtMs;
|
|
8727
|
+
}
|
|
8728
|
+
function parseMpcMaterialActivationRefRequired(value) {
|
|
8729
|
+
const parsed = parseMpcMaterialActivationRef(value);
|
|
8730
|
+
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
8731
|
+
return parsed.value;
|
|
8732
|
+
}
|
|
8733
|
+
function parseWalletAuthorityIdRequired(value) {
|
|
8734
|
+
const parsed = parseWalletAuthorityId(value);
|
|
8735
|
+
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
8736
|
+
return parsed.value;
|
|
8737
|
+
}
|
|
8738
|
+
function parseWalletAuthMethodIdRequired(value) {
|
|
8739
|
+
const parsed = parseWalletAuthMethodId(value);
|
|
8740
|
+
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
8741
|
+
return parsed.value;
|
|
8742
|
+
}
|
|
8743
|
+
function parseWalletSessionCapabilitySubject(value) {
|
|
8744
|
+
if (!isWalletSessionCapabilitySubjectRecordV1(value) || typeof value.kind !== "string") throw new Error("Wallet Session capability subject must be an object");
|
|
8745
|
+
switch (value.kind) {
|
|
8746
|
+
case "sign":
|
|
8747
|
+
case "export_keys":
|
|
8748
|
+
if (Object.keys(value).sort().join("|") !== "keyFamily|kind|materialActivation") throw new Error("Wallet Session signer capability subject contains unexpected fields");
|
|
8749
|
+
if (value.keyFamily !== "ed25519" && value.keyFamily !== "ecdsa_secp256k1") throw new Error("Wallet Session signer capability subject key family is invalid");
|
|
8750
|
+
return {
|
|
8751
|
+
kind: value.kind,
|
|
8752
|
+
keyFamily: value.keyFamily,
|
|
8753
|
+
materialActivation: parseMpcMaterialActivationRefRequired(value.materialActivation)
|
|
8754
|
+
};
|
|
8755
|
+
case "link_devices":
|
|
8756
|
+
case "revoke_devices":
|
|
8757
|
+
if (Object.keys(value).sort().join("|") !== "authorityId|kind") throw new Error("Wallet Session administration capability subject contains unexpected fields");
|
|
8758
|
+
return {
|
|
8759
|
+
kind: value.kind,
|
|
8760
|
+
authorityId: parseWalletAuthorityIdRequired(value.authorityId)
|
|
8761
|
+
};
|
|
8762
|
+
default: throw new Error("Wallet Session capability subject kind is unsupported");
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
8765
|
+
function isWalletSessionCapabilitySubjectRecordV1(value) {
|
|
8766
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8767
|
+
}
|
|
8768
|
+
function parseWalletSessionCapabilitySubjects(value) {
|
|
8769
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error("Wallet Session capability subjects must be non-empty");
|
|
8770
|
+
const subjects = [];
|
|
8771
|
+
for (const subject of value) subjects.push(parseWalletSessionCapabilitySubject(subject));
|
|
8772
|
+
const [firstSubject, ...remainingSubjects] = subjects;
|
|
8773
|
+
if (!firstSubject) throw new Error("Wallet Session capability subjects must be non-empty");
|
|
8774
|
+
return [firstSubject, ...remainingSubjects];
|
|
8775
|
+
}
|
|
8776
|
+
function parseWalletSessionAuthorizationV2(value) {
|
|
8777
|
+
if (!isWalletSessionAuthorizationRecordV2(value)) throw new Error("Wallet Session authorization V2 contains unexpected fields");
|
|
8778
|
+
if (value.kind !== "wallet_session_authorization_v2") throw new Error("Wallet Session authorization V2 kind is invalid");
|
|
8779
|
+
const tenantId = parseTenantIdRequired(value.tenantId);
|
|
8780
|
+
const principalId = parsePrincipalIdRequired(value.principalId);
|
|
8781
|
+
const walletId = parseWalletIdRequired(value.walletId);
|
|
8782
|
+
const authorityId = parseWalletAuthorityIdRequired(value.authorityId);
|
|
8783
|
+
const walletAuthMethodId = parseWalletAuthMethodIdRequired(value.walletAuthMethodId);
|
|
8784
|
+
const authorityDigestB64u = parseDigestB64u(value.authorityDigestB64u);
|
|
8785
|
+
const mintIdResult = parseWalletSessionMintId(value.mintId);
|
|
8786
|
+
if (!mintIdResult.ok) throw new Error(mintIdResult.error.message);
|
|
8787
|
+
const authorizationIdResult = parseWalletSessionAuthorizationId(value.authorizationId);
|
|
8788
|
+
if (!authorizationIdResult.ok) throw new Error(authorizationIdResult.error.message);
|
|
8789
|
+
const walletSessionId = parseWalletSessionIdRequired(value.walletSessionId);
|
|
8790
|
+
const quotaId = parseMpcWalletSigningQuotaIdRequired(value.quotaId);
|
|
8791
|
+
return buildWalletSessionAuthorizationV2({
|
|
8792
|
+
tenantId,
|
|
8793
|
+
principalId,
|
|
8794
|
+
walletId,
|
|
8795
|
+
authorityId,
|
|
8796
|
+
walletAuthMethodId,
|
|
8797
|
+
authorityDigestB64u,
|
|
8798
|
+
authorityRevocationEpoch: requireNonnegativeInteger(value.authorityRevocationEpoch, "Wallet Session authority revocation epoch"),
|
|
8799
|
+
mintId: mintIdResult.value,
|
|
8800
|
+
authorizationId: authorizationIdResult.value,
|
|
8801
|
+
walletSessionId,
|
|
8802
|
+
quotaId,
|
|
8803
|
+
capabilitySubjects: parseWalletSessionCapabilitySubjects(value.capabilitySubjects),
|
|
8804
|
+
createdAtMs: requirePositiveTimestamp(value.createdAtMs, "Wallet Session createdAtMs"),
|
|
8805
|
+
expiresAtMs: requirePositiveTimestamp(value.expiresAtMs, "Wallet Session expiresAtMs")
|
|
8806
|
+
});
|
|
8807
|
+
}
|
|
8808
|
+
function isWalletSessionAuthorizationRecordV2(value) {
|
|
8809
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
8810
|
+
return Object.keys(value).sort().join("|") === "authorityDigestB64u|authorityId|authorityRevocationEpoch|authorizationId|capabilitySubjects|createdAtMs|expiresAtMs|kind|mintId|principalId|quotaId|tenantId|walletAuthMethodId|walletId|walletSessionId";
|
|
8811
|
+
}
|
|
8812
|
+
async function buildAuthorizedOperation(input) {
|
|
8813
|
+
if (input.tenantId !== input.operation.tenantId) throw new Error("authorized operation tenant must match its operation envelope");
|
|
8814
|
+
const operationFingerprintDigest = await computeCapabilityOperationFingerprintDigest(input.operation);
|
|
8815
|
+
return {
|
|
8816
|
+
...input,
|
|
8817
|
+
kind: "authorized_operation",
|
|
8818
|
+
operationFingerprintDigest,
|
|
8819
|
+
lifecycle: "claimed"
|
|
8820
|
+
};
|
|
8821
|
+
}
|
|
8822
|
+
const AUTHORIZED_OPERATION_RESULT_DIGEST_DOMAIN_V1 = "seams:authorization:authorized-operation-result:v1";
|
|
8823
|
+
function parseAuthorizedOperationReplayResponse(value) {
|
|
8824
|
+
if (!isAuthorizedOperationReplayResponseRecordV1(value)) throw new Error("authorized operation replay response must be an object");
|
|
8825
|
+
const keys = Object.keys(value).sort();
|
|
8826
|
+
if (keys.length !== 3 || keys.join("|") !== "bodyText|contentType|status") throw new Error("authorized operation replay response must contain only status, contentType, and bodyText");
|
|
8827
|
+
const status = value.status;
|
|
8828
|
+
if (typeof status !== "number" || !Number.isSafeInteger(status) || status < 200 || status > 599) throw new Error("authorized operation replay response status must be an HTTP status");
|
|
8829
|
+
const contentType = value.contentType;
|
|
8830
|
+
if (typeof contentType !== "string" || contentType.length === 0 || contentType.trim() !== contentType || contentType.length > 255 || /[\u0000-\u001f\u007f]/.test(contentType)) throw new Error("authorized operation replay response contentType is invalid");
|
|
8831
|
+
const bodyText = value.bodyText;
|
|
8832
|
+
if (typeof bodyText !== "string") throw new Error("authorized operation replay response bodyText must be text");
|
|
8833
|
+
if (new TextEncoder().encode(bodyText).byteLength > 65536) throw new Error("authorized operation replay response bodyText is too large");
|
|
8834
|
+
if (responseStatusHasNoBody(status) && bodyText.length !== 0) throw new Error("authorized operation replay response status cannot carry a body");
|
|
8835
|
+
return {
|
|
8836
|
+
status,
|
|
8837
|
+
contentType,
|
|
8838
|
+
bodyText
|
|
8839
|
+
};
|
|
8840
|
+
}
|
|
8841
|
+
function authorizedOperationReplayBodyInit(response) {
|
|
8842
|
+
const parsed = parseAuthorizedOperationReplayResponse(response);
|
|
8843
|
+
return responseStatusHasNoBody(parsed.status) ? null : parsed.bodyText;
|
|
8844
|
+
}
|
|
8845
|
+
function responseStatusHasNoBody(status) {
|
|
8846
|
+
return status === 204 || status === 205 || status === 304;
|
|
8847
|
+
}
|
|
8848
|
+
function isAuthorizedOperationReplayResponseRecordV1(value) {
|
|
8849
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
8850
|
+
return Object.keys(value).sort().join("|") === "bodyText|contentType|status";
|
|
8851
|
+
}
|
|
8852
|
+
async function computeAuthorizedOperationResultDigest(response) {
|
|
8853
|
+
return parseDigestB64u(base64UrlEncode(await sha256BytesUtf8(`${AUTHORIZED_OPERATION_RESULT_DIGEST_DOMAIN_V1}|${alphabetizeStringify(parseAuthorizedOperationReplayResponse(response))}`)));
|
|
8854
|
+
}
|
|
8855
|
+
function parseHostedWalletSeamsSessionExchangeCode(value) {
|
|
8856
|
+
return parseAuthorizationDomainId(value, "hostedWalletSessionExchangeCode");
|
|
8857
|
+
}
|
|
8858
|
+
function parseVerifiedOwnerProofId(value) {
|
|
8859
|
+
return parseAuthorizationDomainId(value, "verifiedOwnerProofId");
|
|
8860
|
+
}
|
|
8861
|
+
function parseHostedWalletSeamsSessionExchangeNonce(value) {
|
|
8862
|
+
return parseAuthorizationDomainId(value, "hostedWalletSessionExchangeNonce");
|
|
8863
|
+
}
|
|
8864
|
+
function parseHostedWalletSessionCredentialId(value) {
|
|
8865
|
+
return parseAuthorizationDomainId(value, "hostedWalletSessionCredentialId");
|
|
8866
|
+
}
|
|
8867
|
+
function parsePrimaryWalletSessionOperationCredentialToken(value) {
|
|
8868
|
+
if (typeof value !== "string" || !/^wst_[A-Za-z0-9_-]{43}$/.test(value)) throw new Error("primary Wallet Session credential is invalid");
|
|
8869
|
+
return parseAuthorizationDomainId(value, "primaryWalletSessionOperationCredentialToken");
|
|
8870
|
+
}
|
|
8871
|
+
function parseHostedWalletSessionOperationCredentialToken(value) {
|
|
8872
|
+
if (typeof value !== "string" || !/^wsh_[A-Za-z0-9_-]{43}$/.test(value)) throw new Error("HostedWalletSessionOperationCredentialV1.token is invalid");
|
|
8873
|
+
return parseAuthorizationDomainId(value, "hostedWalletSessionOperationCredentialToken");
|
|
8874
|
+
}
|
|
8875
|
+
function parseSessionOrigin(value) {
|
|
8876
|
+
if (typeof value !== "string" || value.trim() !== value) throw new Error("session origin must be a canonical HTTP origin");
|
|
8877
|
+
let url;
|
|
8878
|
+
try {
|
|
8879
|
+
url = new URL(value);
|
|
8880
|
+
} catch {
|
|
8881
|
+
throw new Error("session origin must be a canonical HTTP origin");
|
|
8882
|
+
}
|
|
8883
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin !== value || url.pathname !== "/" || url.search || url.hash) throw new Error("session origin must be a canonical HTTP origin");
|
|
8884
|
+
return value;
|
|
8885
|
+
}
|
|
8886
|
+
function buildActiveWalletSessionQuota(fields) {
|
|
8887
|
+
requirePositiveCount(fields.remainingUses, "Wallet Session quota remaining uses");
|
|
8888
|
+
requirePositiveTime(fields.expiresAtMs, "Wallet Session quota expiry");
|
|
8889
|
+
return {
|
|
8890
|
+
kind: "active_wallet_session_quota",
|
|
8891
|
+
tenantId: fields.tenantId,
|
|
8892
|
+
principalId: fields.principalId,
|
|
8893
|
+
walletSessionId: fields.walletSessionId,
|
|
8894
|
+
quotaId: fields.quotaId,
|
|
8895
|
+
remainingUses: fields.remainingUses,
|
|
8896
|
+
expiresAtMs: fields.expiresAtMs
|
|
8897
|
+
};
|
|
8898
|
+
}
|
|
8899
|
+
function parseAuthorizationDomainId(value, fieldName) {
|
|
8900
|
+
if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.length > 512 || /[\s\u0000-\u001f\u007f]/.test(value)) throw new Error(`${fieldName} must be a compact opaque identifier`);
|
|
8901
|
+
return value;
|
|
8902
|
+
}
|
|
8903
|
+
function parseTenantIdRequired(value) {
|
|
8904
|
+
const parsed = parseTenantId(value);
|
|
8905
|
+
if (!parsed.ok) throw new Error(`linked-device authorization tenantId: ${parsed.error.message}`);
|
|
8906
|
+
return parsed.value;
|
|
8907
|
+
}
|
|
8908
|
+
function parsePrincipalIdRequired(value) {
|
|
8909
|
+
const parsed = parsePrincipalId(value);
|
|
8910
|
+
if (!parsed.ok) throw new Error(`linked-device authorization principalId: ${parsed.error.message}`);
|
|
8911
|
+
return parsed.value;
|
|
8912
|
+
}
|
|
8913
|
+
function parseWalletIdRequired(value) {
|
|
8914
|
+
const parsed = parseWalletId(value);
|
|
8915
|
+
if (!parsed.ok) throw new Error(`linked-device authorization walletId: ${parsed.error.message}`);
|
|
8916
|
+
return parsed.value;
|
|
8917
|
+
}
|
|
8918
|
+
function parseWalletSessionIdRequired(value) {
|
|
8919
|
+
const parsed = parseWalletSessionId(value);
|
|
8920
|
+
if (!parsed.ok) throw new Error(`linked-device authorization walletSessionId: ${parsed.error.message}`);
|
|
8921
|
+
return parsed.value;
|
|
8922
|
+
}
|
|
8923
|
+
function parseMpcWalletSigningQuotaIdRequired(value) {
|
|
8924
|
+
const parsed = parseMpcWalletSigningQuotaId(value);
|
|
8925
|
+
if (!parsed.ok) throw new Error(`linked-device authorization quotaId: ${parsed.error.message}`);
|
|
8926
|
+
return parsed.value;
|
|
8927
|
+
}
|
|
8928
|
+
function requirePositiveCount(value, label) {
|
|
8929
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`);
|
|
8930
|
+
}
|
|
8931
|
+
function requireNonnegativeInteger(value, label) {
|
|
8932
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a nonnegative safe integer`);
|
|
8933
|
+
return value;
|
|
8934
|
+
}
|
|
8935
|
+
function requirePositiveTimestamp(value, label) {
|
|
8936
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`);
|
|
8937
|
+
return value;
|
|
8938
|
+
}
|
|
8939
|
+
function requirePositiveTime(value, label) {
|
|
8940
|
+
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive timestamp`);
|
|
8941
|
+
}
|
|
8942
|
+
function requireOrderedTimes(createdAtMs, expiresAtMs, label) {
|
|
8943
|
+
requirePositiveTime(createdAtMs, `${label} creation`);
|
|
8944
|
+
requirePositiveTime(expiresAtMs, `${label} expiry`);
|
|
8945
|
+
if (expiresAtMs <= createdAtMs) throw new Error(`${label} expiry must follow creation`);
|
|
8946
|
+
}
|
|
8947
|
+
//#endregion
|
|
8948
|
+
//#region src/authorization/factorEvidence.ts
|
|
8949
|
+
function buildVerifiedWalletOperationPasskeyFactorResult(fields) {
|
|
8950
|
+
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
8951
|
+
return {
|
|
8952
|
+
kind: "verified_wallet_operation_passkey_factor",
|
|
8953
|
+
...fields
|
|
8954
|
+
};
|
|
8955
|
+
}
|
|
8956
|
+
function buildVerifiedWalletOperationEmailOtpFactorResult(fields) {
|
|
8957
|
+
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
8958
|
+
return {
|
|
8959
|
+
kind: "verified_wallet_operation_email_otp_factor",
|
|
8960
|
+
...fields
|
|
8961
|
+
};
|
|
8962
|
+
}
|
|
8963
|
+
function buildVerifiedWalletSessionPasskeyFactorResult(fields) {
|
|
8964
|
+
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
8965
|
+
return {
|
|
8966
|
+
kind: "verified_wallet_session_passkey_factor",
|
|
8967
|
+
tenantId: fields.tenantId,
|
|
8968
|
+
principalId: fields.principalId,
|
|
8969
|
+
walletId: fields.walletId,
|
|
8970
|
+
authorityRef: fields.authorityRef,
|
|
8971
|
+
requestOrigin: fields.requestOrigin,
|
|
8972
|
+
audience: fields.audience,
|
|
8973
|
+
factorId: fields.factorId,
|
|
8974
|
+
credentialIdB64u: fields.credentialIdB64u,
|
|
8975
|
+
assertionDigest: fields.assertionDigest,
|
|
8976
|
+
verifiedAtMs: fields.verifiedAtMs,
|
|
8977
|
+
expiresAtMs: fields.expiresAtMs
|
|
8978
|
+
};
|
|
8979
|
+
}
|
|
8980
|
+
function buildVerifiedWalletSessionEmailOtpFactorResult(fields) {
|
|
8981
|
+
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
8982
|
+
return {
|
|
8983
|
+
kind: "verified_wallet_session_email_otp_factor",
|
|
8984
|
+
tenantId: fields.tenantId,
|
|
8985
|
+
principalId: fields.principalId,
|
|
8986
|
+
walletId: fields.walletId,
|
|
8987
|
+
authorityRef: fields.authorityRef,
|
|
8988
|
+
requestOrigin: fields.requestOrigin,
|
|
8989
|
+
audience: fields.audience,
|
|
8990
|
+
factorId: fields.factorId,
|
|
8991
|
+
challengeId: fields.challengeId,
|
|
8992
|
+
verificationReceiptDigest: fields.verificationReceiptDigest,
|
|
8993
|
+
verifiedAtMs: fields.verifiedAtMs,
|
|
8994
|
+
expiresAtMs: fields.expiresAtMs
|
|
8995
|
+
};
|
|
8996
|
+
}
|
|
8997
|
+
function requireVerificationWindow(verifiedAtMs, expiresAtMs) {
|
|
8998
|
+
if (!Number.isSafeInteger(verifiedAtMs) || !Number.isSafeInteger(expiresAtMs) || verifiedAtMs <= 0 || expiresAtMs <= verifiedAtMs) throw new Error("verified factor expiry must follow verification");
|
|
8999
|
+
}
|
|
9000
|
+
//#endregion
|
|
9001
|
+
//#region ../shared-ts/src/utils/errors.ts
|
|
9002
|
+
/**
|
|
9003
|
+
* Best-effort error message extractor without relying on `any`.
|
|
9004
|
+
* Always returns a string (may be empty when nothing usable can be derived).
|
|
9005
|
+
*/
|
|
9006
|
+
function errorMessage$3(err) {
|
|
9007
|
+
try {
|
|
9008
|
+
if (typeof err === "string") return err;
|
|
9009
|
+
if (err && typeof err.message === "string") return err.message;
|
|
9010
|
+
return String(err ?? "");
|
|
9011
|
+
} catch {
|
|
9012
|
+
return "";
|
|
9013
|
+
}
|
|
9014
|
+
}
|
|
9015
|
+
function b64uField(prefix) {
|
|
9016
|
+
return `${prefix}B64u`;
|
|
9017
|
+
}
|
|
9018
|
+
b64uField("seed"), b64uField("canonicalSeed"), b64uField("xClientBase"), b64uField("clientOutputMask"), b64uField("clientRecoverableSecret"), b64uField("prfFirst"), b64uField("prfSecond"), b64uField("signingShare32"), b64uField("clientNonceHandle");
|
|
9019
|
+
//#endregion
|
|
9020
|
+
//#region ../shared-ts/src/utils/failure.ts
|
|
9021
|
+
function failure$6(code, message) {
|
|
9022
|
+
return {
|
|
9023
|
+
ok: false,
|
|
9024
|
+
code,
|
|
9025
|
+
message
|
|
9026
|
+
};
|
|
9027
|
+
}
|
|
9028
|
+
function failedVerification(code, message) {
|
|
9029
|
+
return {
|
|
9030
|
+
ok: false,
|
|
9031
|
+
verified: false,
|
|
9032
|
+
code,
|
|
9033
|
+
message
|
|
9034
|
+
};
|
|
9035
|
+
}
|
|
9036
|
+
//#endregion
|
|
9037
|
+
//#region src/router/auth/webAuthnCredentialCodecs.ts
|
|
9038
|
+
function containsWebAuthnExtensionOutput(value) {
|
|
9039
|
+
if (value == null) return false;
|
|
9040
|
+
if (!isPlainObject(value)) return true;
|
|
9041
|
+
for (const nested of Object.values(value)) if (containsWebAuthnExtensionOutput(nested)) return true;
|
|
9042
|
+
return false;
|
|
9043
|
+
}
|
|
9044
|
+
function unredactedWebAuthnExtensionField(input) {
|
|
9045
|
+
if (!isPlainObject(input)) return null;
|
|
9046
|
+
if (containsWebAuthnExtensionOutput(input.clientExtensionResults)) return "clientExtensionResults";
|
|
9047
|
+
const response = input.response;
|
|
9048
|
+
if (!isPlainObject(response)) return null;
|
|
9049
|
+
if (containsWebAuthnExtensionOutput(response.clientExtensionResults)) return "response.clientExtensionResults";
|
|
9050
|
+
return null;
|
|
9051
|
+
}
|
|
9052
|
+
function decodeWebAuthnBase64UrlOrBase64(input, fieldName) {
|
|
9053
|
+
try {
|
|
9054
|
+
return base64UrlDecode(input);
|
|
9055
|
+
} catch {
|
|
9056
|
+
try {
|
|
9057
|
+
return base64Decode(input);
|
|
9058
|
+
} catch (error) {
|
|
9059
|
+
throw new Error(`Invalid ${fieldName}: expected base64url/base64 string (${errorMessage$3(error) || "decode failed"})`);
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
function parseWebAuthnClientDataJsonBase64url(clientDataJSONB64u) {
|
|
9064
|
+
const bytes = decodeWebAuthnBase64UrlOrBase64(clientDataJSONB64u, "webauthn_authentication.response.clientDataJSON");
|
|
9065
|
+
const json = new TextDecoder().decode(bytes);
|
|
9066
|
+
let parsed;
|
|
9067
|
+
try {
|
|
9068
|
+
parsed = JSON.parse(json);
|
|
9069
|
+
} catch {
|
|
9070
|
+
throw new Error("Invalid clientDataJSON: expected object");
|
|
9071
|
+
}
|
|
9072
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid clientDataJSON: expected object");
|
|
9073
|
+
const challenge = normalizeOptionalTrimmedString(Reflect.get(parsed, "challenge"));
|
|
9074
|
+
const origin = normalizeOptionalTrimmedString(Reflect.get(parsed, "origin"));
|
|
9075
|
+
const type = normalizeOptionalTrimmedString(Reflect.get(parsed, "type"));
|
|
9076
|
+
if (!challenge) throw new Error("Invalid clientDataJSON.challenge");
|
|
9077
|
+
if (!origin) throw new Error("Invalid clientDataJSON.origin");
|
|
9078
|
+
if (!type) throw new Error("Invalid clientDataJSON.type");
|
|
9079
|
+
return {
|
|
9080
|
+
challenge,
|
|
9081
|
+
origin,
|
|
9082
|
+
type
|
|
9083
|
+
};
|
|
9084
|
+
}
|
|
9085
|
+
function webAuthnOriginHostnameOrEmpty(origin) {
|
|
9086
|
+
try {
|
|
9087
|
+
return new URL(origin).hostname.toLowerCase();
|
|
9088
|
+
} catch {
|
|
9089
|
+
return "";
|
|
9090
|
+
}
|
|
9091
|
+
}
|
|
9092
|
+
function webAuthnCredentialIdB64uFromCredential(input) {
|
|
9093
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return failure$6("invalid_body", "Missing webauthn_authentication.id/rawId");
|
|
9094
|
+
const rawId = normalizeOptionalTrimmedString(Reflect.get(input, "rawId"));
|
|
9095
|
+
const id = normalizeOptionalTrimmedString(Reflect.get(input, "id"));
|
|
9096
|
+
const selected = rawId || id;
|
|
9097
|
+
if (!selected) return failure$6("invalid_body", "Missing webauthn_authentication.id/rawId");
|
|
9098
|
+
try {
|
|
9099
|
+
return {
|
|
9100
|
+
ok: true,
|
|
9101
|
+
credentialIdB64u: base64UrlEncode(decodeWebAuthnBase64UrlOrBase64(selected, "webauthn_authentication.rawId"))
|
|
9102
|
+
};
|
|
9103
|
+
} catch (error) {
|
|
9104
|
+
return failure$6("invalid_body", errorMessage$3(error) || "Invalid credential rawId");
|
|
9105
|
+
}
|
|
9106
|
+
}
|
|
9107
|
+
function parseWebAuthnAuthenticationCredential$2(input) {
|
|
9108
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return null;
|
|
9109
|
+
const responseValue = Reflect.get(input, "response");
|
|
9110
|
+
if (responseValue === null || typeof responseValue !== "object" || Array.isArray(responseValue)) return null;
|
|
9111
|
+
const id = normalizeOptionalTrimmedString(Reflect.get(input, "id"));
|
|
9112
|
+
const rawId = normalizeOptionalTrimmedString(Reflect.get(input, "rawId"));
|
|
9113
|
+
const type = normalizeOptionalTrimmedString(Reflect.get(input, "type"));
|
|
9114
|
+
const clientDataJSON = normalizeOptionalTrimmedString(Reflect.get(responseValue, "clientDataJSON"));
|
|
9115
|
+
const authenticatorData = normalizeOptionalTrimmedString(Reflect.get(responseValue, "authenticatorData"));
|
|
9116
|
+
const signature = normalizeOptionalTrimmedString(Reflect.get(responseValue, "signature"));
|
|
9117
|
+
const rawUserHandle = Reflect.get(responseValue, "userHandle");
|
|
9118
|
+
const userHandle = rawUserHandle === null ? null : normalizeOptionalTrimmedString(rawUserHandle) || null;
|
|
9119
|
+
const rawAuthenticatorAttachment = Reflect.get(input, "authenticatorAttachment");
|
|
9120
|
+
const authenticatorAttachment = rawAuthenticatorAttachment === null ? null : normalizeOptionalTrimmedString(rawAuthenticatorAttachment) || null;
|
|
9121
|
+
if (!id || !rawId || type !== "public-key") return null;
|
|
9122
|
+
if (!clientDataJSON || !authenticatorData || !signature) return null;
|
|
9123
|
+
return {
|
|
9124
|
+
id,
|
|
9125
|
+
rawId,
|
|
9126
|
+
type,
|
|
9127
|
+
authenticatorAttachment,
|
|
9128
|
+
response: {
|
|
9129
|
+
clientDataJSON,
|
|
9130
|
+
authenticatorData,
|
|
9131
|
+
signature,
|
|
9132
|
+
userHandle
|
|
9133
|
+
},
|
|
9134
|
+
clientExtensionResults: Reflect.get(input, "clientExtensionResults") ?? null
|
|
9135
|
+
};
|
|
9136
|
+
}
|
|
9137
|
+
//#endregion
|
|
9138
|
+
//#region src/router/domains/emailOtp/emailOtpSessionRouteHelpers.ts
|
|
9139
|
+
function emailOtpStatusCode(code) {
|
|
9140
|
+
if (code === "internal") return 500;
|
|
9141
|
+
if (code === "not_configured") return 503;
|
|
9142
|
+
if (code === "not_found") return 404;
|
|
9143
|
+
if (code === "rate_limited") return 429;
|
|
9144
|
+
if (code === "stronger_auth_required") return 403;
|
|
9145
|
+
if (code === "reenrollment_required" || code === "registration_attempt_missing" || code === "registration_attempt_expired") return 409;
|
|
9146
|
+
if (code === "challenge_id_mismatch" || code === "challenge_purpose_mismatch" || code === "challenge_subject_mismatch" || code === "challenge_email_mismatch" || code === "challenge_wallet_mismatch" || code === "challenge_session_mismatch" || code === "challenge_org_mismatch" || code === "challenge_channel_mismatch" || code === "registration_reroll_disallowed" || code === "challenge_expired_or_invalid" || code === "invalid_otp") return 401;
|
|
9147
|
+
return 400;
|
|
9148
|
+
}
|
|
9149
|
+
function emailOtpFailureAuditPayload(input) {
|
|
9150
|
+
return {
|
|
9151
|
+
source: input.source,
|
|
9152
|
+
code: input.code,
|
|
9153
|
+
message: input.message,
|
|
9154
|
+
...input.challengeId ? { challengeId: input.challengeId } : {},
|
|
9155
|
+
...input.otpChannel ? { otpChannel: input.otpChannel } : {},
|
|
9156
|
+
...input.operation ? { operation: input.operation } : {},
|
|
9157
|
+
...typeof input.lockedUntilMs === "number" ? { lockedUntilMs: input.lockedUntilMs } : {}
|
|
9158
|
+
};
|
|
9159
|
+
}
|
|
9160
|
+
function shouldEmitEmailOtpLockedWebhook(code) {
|
|
9161
|
+
return code === "otp_locked_out" || code === "otp_attempts_exhausted";
|
|
9162
|
+
}
|
|
9163
|
+
function emailOtpFailureWebhookEventDescriptors(input) {
|
|
9164
|
+
const payload = emailOtpFailureAuditPayload(input);
|
|
9165
|
+
const event = {
|
|
9166
|
+
eventType: "wallet.email_otp.failed",
|
|
9167
|
+
...input.challengeId ? { eventId: input.challengeId } : {},
|
|
9168
|
+
payload
|
|
9169
|
+
};
|
|
9170
|
+
if (!shouldEmitEmailOtpLockedWebhook(input.code)) return [event];
|
|
9171
|
+
return [event, {
|
|
9172
|
+
eventType: "wallet.email_otp.locked",
|
|
9173
|
+
...input.challengeId ? { eventId: input.challengeId } : {},
|
|
9174
|
+
payload
|
|
9175
|
+
}];
|
|
9176
|
+
}
|
|
9177
|
+
function emailOtpLoggedInWebhookEventDescriptor(input) {
|
|
9178
|
+
return {
|
|
9179
|
+
eventType: "wallet.email_otp.logged_in",
|
|
9180
|
+
eventId: input.challengeId,
|
|
9181
|
+
payload: {
|
|
9182
|
+
otpChannel: input.otpChannel,
|
|
9183
|
+
unlockBackend: input.unlockBackend,
|
|
9184
|
+
challengeId: input.challengeId
|
|
9185
|
+
}
|
|
9186
|
+
};
|
|
9187
|
+
}
|
|
9188
|
+
async function hashEmailOtpOperationBinding(input) {
|
|
9189
|
+
return base64UrlEncode(await sha256BytesUtf8(alphabetizeStringify({
|
|
9190
|
+
walletId: input.walletId,
|
|
9191
|
+
providerUserId: input.providerUserId,
|
|
9192
|
+
orgId: input.orgId,
|
|
9193
|
+
operation: input.operation,
|
|
9194
|
+
requestOrigin: input.requestOrigin || "",
|
|
9195
|
+
audience: input.audience || "",
|
|
9196
|
+
...input.authorityRef !== void 0 ? { authorityRef: input.authorityRef } : {},
|
|
9197
|
+
...input.operationFingerprintDigest ? { operationFingerprintDigest: input.operationFingerprintDigest } : {}
|
|
9198
|
+
})));
|
|
9199
|
+
}
|
|
9200
|
+
//#endregion
|
|
9201
|
+
//#region src/router/framework/routeResponses.ts
|
|
9202
|
+
function routeJson(status, body, options) {
|
|
9203
|
+
return {
|
|
9204
|
+
status,
|
|
9205
|
+
body,
|
|
9206
|
+
...options?.headers ? { headers: options.headers } : {},
|
|
9207
|
+
...options?.usage ? { usage: options.usage } : {}
|
|
9208
|
+
};
|
|
9209
|
+
}
|
|
9210
|
+
function routeError(status, code, message) {
|
|
9211
|
+
return routeJson(status, {
|
|
9212
|
+
ok: false,
|
|
9213
|
+
code,
|
|
9214
|
+
message
|
|
9215
|
+
});
|
|
9216
|
+
}
|
|
9217
|
+
function toFetchRouteResponse(response) {
|
|
9218
|
+
return new Response(JSON.stringify(response.body), {
|
|
9219
|
+
status: response.status,
|
|
9220
|
+
headers: {
|
|
9221
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
9222
|
+
...response.headers || {}
|
|
9223
|
+
}
|
|
9224
|
+
});
|
|
9225
|
+
}
|
|
9226
|
+
//#endregion
|
|
9227
|
+
//#region src/router/transport/fetch/routes/walletCustodyRequestBoundary.ts
|
|
9228
|
+
function requireExactObjectFields(value, fields, label) {
|
|
9229
|
+
const keys = Object.keys(value).sort();
|
|
9230
|
+
const expected = [...fields].sort();
|
|
9231
|
+
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new Error(`${label} contains unsupported fields`);
|
|
9232
|
+
}
|
|
9233
|
+
function parseCustodyOwnerProof(value) {
|
|
9234
|
+
if (!isPlainObject(value)) throw new Error("factorProof is required");
|
|
9235
|
+
const kind = normalizeOptionalTrimmedString(value.kind);
|
|
9236
|
+
if (kind === "passkey") {
|
|
9237
|
+
requireExactObjectFields(value, [
|
|
9238
|
+
"kind",
|
|
9239
|
+
"walletId",
|
|
9240
|
+
"rpId",
|
|
9241
|
+
"credentialIdB64u",
|
|
9242
|
+
"challenge_digest",
|
|
9243
|
+
"webauthn_authentication"
|
|
9244
|
+
], "factorProof");
|
|
9245
|
+
const extensionField = unredactedWebAuthnExtensionField(value.webauthn_authentication);
|
|
9246
|
+
if (extensionField) throw new Error(`factorProof.webauthn_authentication.${extensionField} must be redacted`);
|
|
9247
|
+
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$2(value.webauthn_authentication);
|
|
9248
|
+
if (!webauthnAuthentication) throw new Error("factorProof.webauthn_authentication is invalid");
|
|
9249
|
+
return {
|
|
9250
|
+
kind,
|
|
9251
|
+
walletId: requireTrimmedString(value.walletId, "factorProof.walletId"),
|
|
9252
|
+
rpId: requireTrimmedString(value.rpId, "factorProof.rpId"),
|
|
9253
|
+
credentialIdB64u: requireTrimmedString(value.credentialIdB64u, "factorProof.credentialIdB64u"),
|
|
9254
|
+
challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest"),
|
|
9255
|
+
webauthn_authentication: webauthnAuthentication
|
|
9256
|
+
};
|
|
9257
|
+
}
|
|
9258
|
+
if (kind === "email_otp") {
|
|
9259
|
+
requireExactObjectFields(value, [
|
|
9260
|
+
"kind",
|
|
9261
|
+
"provider_subject_id",
|
|
9262
|
+
"challenge_id",
|
|
9263
|
+
"otp_code",
|
|
9264
|
+
"challenge_digest"
|
|
9265
|
+
], "factorProof");
|
|
9266
|
+
return {
|
|
9267
|
+
kind,
|
|
9268
|
+
provider_subject_id: requireTrimmedString(value.provider_subject_id, "factorProof.provider_subject_id"),
|
|
9269
|
+
challenge_id: requireTrimmedString(value.challenge_id, "factorProof.challenge_id"),
|
|
9270
|
+
otp_code: requireTrimmedString(value.otp_code, "factorProof.otp_code"),
|
|
9271
|
+
challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest")
|
|
9272
|
+
};
|
|
9273
|
+
}
|
|
9274
|
+
throw new Error("factorProof.kind must be passkey or email_otp");
|
|
9275
|
+
}
|
|
9276
|
+
function parseRecoveryBackupAcknowledgementRequest(body) {
|
|
9277
|
+
if (!isPlainObject(body)) throw new Error("acknowledgement body is required");
|
|
9278
|
+
requireExactObjectFields(body, [
|
|
9279
|
+
"expectedIssuedAtMs",
|
|
9280
|
+
"factorProof",
|
|
9281
|
+
"requestId",
|
|
9282
|
+
"walletId"
|
|
9283
|
+
], "recovery backup acknowledgement");
|
|
9284
|
+
return {
|
|
9285
|
+
walletId: requireTrimmedString(body.walletId, "walletId"),
|
|
9286
|
+
requestId: requireTrimmedString(body.requestId, "requestId"),
|
|
9287
|
+
expectedIssuedAtMs: parseUnixMs$1(body.expectedIssuedAtMs, "expectedIssuedAtMs"),
|
|
9288
|
+
factorProof: body.factorProof
|
|
9289
|
+
};
|
|
9290
|
+
}
|
|
9291
|
+
function parseRecoveryStatusRequest(body) {
|
|
9292
|
+
if (!isPlainObject(body)) throw new Error("status body is required");
|
|
9293
|
+
requireExactObjectFields(body, ["factorProof", "requestId"], "recovery status");
|
|
9294
|
+
return {
|
|
9295
|
+
requestId: requireTrimmedString(body.requestId, "requestId"),
|
|
9296
|
+
factorProof: body.factorProof
|
|
9297
|
+
};
|
|
9298
|
+
}
|
|
9299
|
+
//#endregion
|
|
9300
|
+
//#region src/router/transport/fetch/walletOwnerOperationAuthorization.ts
|
|
9301
|
+
const CUSTODY_OPERATION_CAPABILITY_ID = "wallet-custody-admin";
|
|
9302
|
+
const CUSTODY_PROOF_TTL_MS = 6e4;
|
|
9303
|
+
function walletOwnerOperationAuthorizationId(operation, challenge) {
|
|
9304
|
+
return requireParsed$4(parseAuthorizedOperationId(`wallet-custody:authorized:wallet-custody:${operation}:${challenge}`), "owner operation");
|
|
9305
|
+
}
|
|
9306
|
+
function hasFullOwnerAuthority$1(authority) {
|
|
9307
|
+
return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
|
|
9308
|
+
}
|
|
9309
|
+
async function custodyDigest(domain, value) {
|
|
9310
|
+
return sha256Utf8DigestB64u(`${domain}|${alphabetizeStringify(value)}`);
|
|
9311
|
+
}
|
|
9312
|
+
async function buildWalletCustodyOperation(input) {
|
|
9313
|
+
const tenantId = requireParsed$4(parseTenantId(input.tenantId), "authorization");
|
|
9314
|
+
const principalId = requireParsed$4(parsePrincipalId(input.principalId), "authorization");
|
|
9315
|
+
const capabilityId = requireParsed$4(parseCapabilityId(CUSTODY_OPERATION_CAPABILITY_ID), "authorization");
|
|
9316
|
+
const operationRef = buildVaultOperationRef("vault.reveal");
|
|
9317
|
+
const laneDigest = await custodyDigest("seams:wallet-custody:lane:v1", {
|
|
9318
|
+
capabilityId,
|
|
9319
|
+
operation: input.operation,
|
|
9320
|
+
operationRef
|
|
9321
|
+
});
|
|
9322
|
+
const challengeDigest = await computeWalletOwnerAdminChallengeDigest({
|
|
9323
|
+
walletId: input.walletId,
|
|
9324
|
+
operation: input.operation,
|
|
9325
|
+
payload: input.payload,
|
|
9326
|
+
requestOrigin: input.requestOrigin
|
|
9327
|
+
});
|
|
9328
|
+
const intentDigest = challengeDigest;
|
|
9329
|
+
const displayDigest = await custodyDigest("seams:wallet-custody:display:v1", {
|
|
9330
|
+
walletId: input.walletId,
|
|
9331
|
+
operation: input.operation
|
|
9332
|
+
});
|
|
9333
|
+
return {
|
|
9334
|
+
operation: buildCapabilityOperationEnvelope({
|
|
9335
|
+
tenantId,
|
|
9336
|
+
principalId,
|
|
9337
|
+
capabilityId,
|
|
9338
|
+
operationId: requireParsed$4(parseCapabilityOperationId(`wallet-custody:${input.operation}:${intentDigest}`), "authorization"),
|
|
9339
|
+
operation: operationRef,
|
|
9340
|
+
digests: {
|
|
9341
|
+
laneDigest,
|
|
9342
|
+
intentDigest,
|
|
9343
|
+
displayDigest
|
|
9344
|
+
}
|
|
9345
|
+
}),
|
|
9346
|
+
challengeDigest
|
|
9347
|
+
};
|
|
9348
|
+
}
|
|
9349
|
+
async function authorizeWalletOwnerOperation(input) {
|
|
9350
|
+
const requestOrigin = normalizeOptionalTrimmedString(input.ctx.request.headers.get("origin"));
|
|
9351
|
+
if (!requestOrigin) return toCustodyAuthorizationFailure(400, "invalid_origin", "request Origin is required");
|
|
9352
|
+
let origin;
|
|
9353
|
+
let proof;
|
|
9354
|
+
let walletId;
|
|
9355
|
+
try {
|
|
9356
|
+
origin = parseSessionOrigin(requestOrigin);
|
|
9357
|
+
walletId = requireParsed$4(parseWalletId(input.walletId), "authorization");
|
|
9358
|
+
proof = parseCustodyOwnerProof(input.factorProof);
|
|
9359
|
+
} catch (error) {
|
|
9360
|
+
return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "owner proof is invalid");
|
|
9361
|
+
}
|
|
9362
|
+
if (proof.kind === "passkey" && proof.walletId !== input.walletId) return toCustodyAuthorizationFailure(403, "scope_mismatch", "owner proof wallet differs");
|
|
9363
|
+
let principalId;
|
|
9364
|
+
let factor;
|
|
9365
|
+
let ownerAuthorityRef;
|
|
9366
|
+
let ownerAuthority;
|
|
9367
|
+
let operation;
|
|
9368
|
+
let challengeDigest;
|
|
9369
|
+
try {
|
|
9370
|
+
principalId = proof.kind === "passkey" ? input.walletId : proof.provider_subject_id;
|
|
9371
|
+
const built = await buildWalletCustodyOperation({
|
|
9372
|
+
tenantId: input.ctx.service.authorizedOperations.tenantId,
|
|
9373
|
+
principalId,
|
|
9374
|
+
walletId: input.walletId,
|
|
9375
|
+
operation: input.operation,
|
|
9376
|
+
payload: input.payload,
|
|
9377
|
+
requestOrigin
|
|
9378
|
+
});
|
|
9379
|
+
operation = built.operation;
|
|
9380
|
+
challengeDigest = built.challengeDigest;
|
|
9381
|
+
} catch (error) {
|
|
9382
|
+
return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "owner proof binding is invalid");
|
|
9383
|
+
}
|
|
9384
|
+
const operationFingerprintDigest = await computeCapabilityOperationFingerprintDigest(operation);
|
|
9385
|
+
const nowMs = Date.now();
|
|
9386
|
+
if (proof.challenge_digest !== String(challengeDigest)) return toCustodyAuthorizationFailure(403, "scope_mismatch", "factor proof challenge does not match the requested operation");
|
|
9387
|
+
if (proof.kind === "passkey") {
|
|
9388
|
+
let authority;
|
|
9389
|
+
try {
|
|
9390
|
+
authority = buildPasskeyWalletAuthAuthority({
|
|
9391
|
+
walletId,
|
|
9392
|
+
rpId: proof.rpId,
|
|
9393
|
+
credentialIdB64u: proof.credentialIdB64u
|
|
9394
|
+
});
|
|
9395
|
+
} catch (error) {
|
|
9396
|
+
return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "passkey authority is invalid");
|
|
9397
|
+
}
|
|
9398
|
+
const active = await input.ctx.service.walletAuthMethods.resolveActivePasskeyAuthorityForVerifiedCredential({
|
|
9399
|
+
walletId,
|
|
9400
|
+
rpId: authority.verifier.rpId,
|
|
9401
|
+
credentialIdB64u: authority.factor.credentialIdB64u
|
|
9402
|
+
});
|
|
9403
|
+
if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
|
|
9404
|
+
if (!hasFullOwnerAuthority$1(active.walletAuthority)) return toCustodyAuthorizationFailure(403, "forbidden", "Full owner authority is required");
|
|
9405
|
+
ownerAuthority = active.walletAuthority;
|
|
9406
|
+
const credential = parseWebAuthnAuthenticationCredential$2(proof.webauthn_authentication);
|
|
9407
|
+
if (!credential) return toCustodyAuthorizationFailure(400, "invalid_owner_proof", "passkey assertion is invalid");
|
|
9408
|
+
if (String(credential.rawId || credential.id).trim() !== authority.factor.credentialIdB64u) return toCustodyAuthorizationFailure(401, "unauthorized", "passkey credential changed");
|
|
9409
|
+
const verified = await input.ctx.service.webAuthn.verifyWebAuthnAuthenticationLite({
|
|
9410
|
+
userId: input.walletId,
|
|
9411
|
+
rpId: authority.verifier.rpId,
|
|
9412
|
+
expectedChallenge: challengeDigest,
|
|
9413
|
+
expected_origin: requestOrigin,
|
|
9414
|
+
webauthn_authentication: credential
|
|
9415
|
+
});
|
|
9416
|
+
if (!verified.success || !verified.verified) return toCustodyAuthorizationFailure(401, verified.code || "not_verified", verified.message || "WebAuthn authentication verification failed");
|
|
9417
|
+
ownerAuthorityRef = await walletAuthAuthorityRef({ authority: active.authority });
|
|
9418
|
+
factor = buildVerifiedWalletOperationPasskeyFactorResult({
|
|
9419
|
+
tenantId: operation.tenantId,
|
|
9420
|
+
principalId: operation.principalId,
|
|
9421
|
+
walletId,
|
|
9422
|
+
authorityRef: ownerAuthorityRef,
|
|
9423
|
+
requestOrigin: origin,
|
|
9424
|
+
audience: origin,
|
|
9425
|
+
factorId: requireParsed$4(parseAuthFactorId(`passkey:${authority.factor.credentialIdB64u}`), "authorization"),
|
|
9426
|
+
credentialIdB64u: requireParsed$4(parseWebAuthnCredentialIdB64u(authority.factor.credentialIdB64u), "authorization"),
|
|
9427
|
+
assertionDigest: await custodyDigest("seams:wallet-custody:assertion:v1", { credential }),
|
|
9428
|
+
operation,
|
|
9429
|
+
verifiedAtMs: nowMs,
|
|
9430
|
+
expiresAtMs: nowMs + CUSTODY_PROOF_TTL_MS
|
|
9431
|
+
});
|
|
9432
|
+
} else {
|
|
9433
|
+
const providerSubject = requireParsed$4(parseProviderSubject(proof.provider_subject_id), "authorization");
|
|
9434
|
+
const orgId = requireParsed$4(parseOrgId(input.ctx.service.authorizedOperations.tenantId), "authorization");
|
|
9435
|
+
const active = await input.ctx.service.walletAuthMethods.resolveActiveEmailOtpAuthorityForVerifiedSubject({
|
|
9436
|
+
walletId: input.walletId,
|
|
9437
|
+
providerUserId: proof.provider_subject_id
|
|
9438
|
+
});
|
|
9439
|
+
if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
|
|
9440
|
+
const activeAuthorityRef = await walletAuthAuthorityRef({ authority: active.authority });
|
|
9441
|
+
ownerAuthorityRef = activeAuthorityRef;
|
|
9442
|
+
ownerAuthority = active.walletAuthority;
|
|
9443
|
+
const ownerProofBindingDigest = await hashEmailOtpOperationBinding({
|
|
9444
|
+
walletId: input.walletId,
|
|
9445
|
+
providerUserId: proof.provider_subject_id,
|
|
9446
|
+
orgId,
|
|
9447
|
+
operation: `wallet_custody:${input.operation}`,
|
|
9448
|
+
requestOrigin,
|
|
9449
|
+
audience: requestOrigin,
|
|
9450
|
+
authorityRef: activeAuthorityRef,
|
|
9451
|
+
operationFingerprintDigest: challengeDigest
|
|
9452
|
+
});
|
|
9453
|
+
const verified = await input.ctx.service.emailOtp.verifyEmailOtpChallenge({
|
|
9454
|
+
userId: proof.provider_subject_id,
|
|
9455
|
+
walletId: input.walletId,
|
|
9456
|
+
orgId,
|
|
9457
|
+
challengeId: proof.challenge_id,
|
|
9458
|
+
otpCode: proof.otp_code,
|
|
9459
|
+
otpChannel: EMAIL_OTP_CHANNEL,
|
|
9460
|
+
ownerProofBindingDigest,
|
|
9461
|
+
operation: WALLET_EMAIL_OTP_UNLOCK_OPERATION
|
|
9462
|
+
});
|
|
9463
|
+
if (!verified.ok) return toCustodyAuthorizationFailure(verified.code === "invalid_body" ? 400 : 401, verified.code, verified.message);
|
|
9464
|
+
const consumed = await input.ctx.service.emailOtp.consumeEmailOtpGrant({
|
|
9465
|
+
subject: {
|
|
9466
|
+
kind: "provider_identity",
|
|
9467
|
+
orgId,
|
|
9468
|
+
providerSubject,
|
|
9469
|
+
walletId
|
|
9470
|
+
},
|
|
9471
|
+
loginGrant: verified.loginGrant,
|
|
9472
|
+
otpChannel: EMAIL_OTP_CHANNEL
|
|
9473
|
+
});
|
|
9474
|
+
if (!consumed.ok) return toCustodyAuthorizationFailure(emailOtpStatusCode(consumed.code), consumed.code, consumed.message);
|
|
9475
|
+
factor = buildVerifiedWalletOperationEmailOtpFactorResult({
|
|
9476
|
+
tenantId: operation.tenantId,
|
|
9477
|
+
principalId: operation.principalId,
|
|
9478
|
+
walletId,
|
|
9479
|
+
authorityRef: activeAuthorityRef,
|
|
9480
|
+
requestOrigin: origin,
|
|
9481
|
+
audience: origin,
|
|
9482
|
+
factorId: requireParsed$4(parseAuthFactorId(`email_otp:${active.authority.factor.provider}:${proof.provider_subject_id}`), "authorization"),
|
|
9483
|
+
operation,
|
|
9484
|
+
challengeId: requireParsed$4(parseEmailOtpChallengeId(consumed.challengeId), "authorization"),
|
|
9485
|
+
verificationReceiptDigest: await custodyDigest("seams:wallet-custody:otp-receipt:v1", {
|
|
9486
|
+
challengeId: consumed.challengeId,
|
|
9487
|
+
operationFingerprintDigest
|
|
9488
|
+
}),
|
|
9489
|
+
verifiedAtMs: nowMs,
|
|
9490
|
+
expiresAtMs: Math.min(nowMs + CUSTODY_PROOF_TTL_MS, verified.grantExpiresAtMs)
|
|
9491
|
+
});
|
|
9492
|
+
}
|
|
9493
|
+
const ownerProof = await input.ctx.service.authorizedOperations.buildVerifiedOwnerProof({
|
|
9494
|
+
purpose: "operation",
|
|
9495
|
+
proofId: parseVerifiedOwnerProofId(`wallet-custody:${operation.operationId}`),
|
|
9496
|
+
factor
|
|
9497
|
+
});
|
|
9498
|
+
if (ownerProof.purpose !== "operation" || ownerProof.operation.operationFingerprintDigest !== operationFingerprintDigest) return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "owner proof binding is invalid");
|
|
9499
|
+
const existing = await input.ctx.service.authorizedOperations.readAuthorizedOperation({
|
|
9500
|
+
tenantId: operation.tenantId,
|
|
9501
|
+
operationFingerprintDigest
|
|
9502
|
+
});
|
|
9503
|
+
if (existing) {
|
|
9504
|
+
if (existing.authorization.kind !== "verified_step_up" || existing.quota.kind !== "quota_neutral") return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "Owner operation authorization differs");
|
|
9505
|
+
return {
|
|
9506
|
+
ok: false,
|
|
9507
|
+
response: authorizedOperationReplay(existing)
|
|
9508
|
+
};
|
|
9509
|
+
}
|
|
9510
|
+
const evidenceSet = await input.ctx.service.authorizedOperations.recordVerifiedWalletOperationFactorEvidenceSet({
|
|
9511
|
+
operation,
|
|
9512
|
+
evidenceId: requireParsed$4(parseAuthorizationEvidenceId(`wallet-custody:evidence:${operation.operationId}`), "authorization"),
|
|
9513
|
+
evidenceSetId: requireParsed$4(parseAuthorizationEvidenceSetId(`wallet-custody:evidence-set:${operation.operationId}`), "authorization"),
|
|
9514
|
+
factor
|
|
9515
|
+
});
|
|
9516
|
+
const admission = await input.ctx.service.authorizedOperations.admitAuthorizedOperation({ operation: {
|
|
9517
|
+
tenantId: operation.tenantId,
|
|
9518
|
+
authorizedOperationId: walletOwnerOperationAuthorizationId(input.operation, challengeDigest),
|
|
9519
|
+
auditEventId: requireParsed$4(parseAuthorizationAuditEventId(`wallet-custody:audit:${operation.operationId}`), "authorization"),
|
|
9520
|
+
operation,
|
|
9521
|
+
authorization: {
|
|
9522
|
+
kind: "verified_step_up",
|
|
9523
|
+
evidenceSetDigest: evidenceSet.evidenceSetDigest
|
|
9524
|
+
},
|
|
9525
|
+
quota: { kind: "quota_neutral" },
|
|
9526
|
+
claimedAtMs: nowMs
|
|
9527
|
+
} });
|
|
9528
|
+
switch (admission.kind) {
|
|
9529
|
+
case "claimed": return {
|
|
9530
|
+
ok: true,
|
|
9531
|
+
operation: admission.operation,
|
|
9532
|
+
ownerAuthority
|
|
9533
|
+
};
|
|
9534
|
+
case "replayed":
|
|
9535
|
+
case "operation_in_progress": return {
|
|
9536
|
+
ok: false,
|
|
9537
|
+
response: authorizedOperationReplay(admission.operation)
|
|
9538
|
+
};
|
|
9539
|
+
case "authorization_grant_rejected":
|
|
9540
|
+
case "verified_step_up_rejected":
|
|
9541
|
+
case "wallet_session_quota_exhausted":
|
|
9542
|
+
case "material_mismatch": return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "owner proof was rejected");
|
|
9543
|
+
}
|
|
9544
|
+
}
|
|
9545
|
+
function toCustodyAuthorizationFailure(status, code, message) {
|
|
9546
|
+
return {
|
|
9547
|
+
ok: false,
|
|
9548
|
+
response: routeErrorResponse$1(status, code, message)
|
|
9549
|
+
};
|
|
9550
|
+
}
|
|
9551
|
+
function routeErrorResponse$1(status, code, message) {
|
|
9552
|
+
return toFetchRouteResponse({
|
|
9553
|
+
status,
|
|
9554
|
+
body: {
|
|
9555
|
+
ok: false,
|
|
9556
|
+
code,
|
|
9557
|
+
message
|
|
9558
|
+
}
|
|
9559
|
+
});
|
|
9560
|
+
}
|
|
9561
|
+
function authorizedOperationReplay(operation) {
|
|
9562
|
+
if (operation.lifecycle !== "completed") return routeErrorResponse$1(409, "recovery_in_progress", "wallet recovery is still in progress");
|
|
9563
|
+
return new Response(operation.response.bodyText, {
|
|
9564
|
+
status: operation.response.status,
|
|
9565
|
+
headers: { "content-type": operation.response.contentType }
|
|
9566
|
+
});
|
|
9567
|
+
}
|
|
9568
|
+
async function completeWalletOwnerOperation(ctx, operation, status, body, result = "succeeded") {
|
|
9569
|
+
const bodyText = JSON.stringify(body);
|
|
9570
|
+
await ctx.service.authorizedOperations.completeAuthorizedOperation({
|
|
9571
|
+
operation,
|
|
9572
|
+
result,
|
|
9573
|
+
response: {
|
|
9574
|
+
status,
|
|
9575
|
+
contentType: "application/json",
|
|
9576
|
+
bodyText
|
|
9577
|
+
},
|
|
9578
|
+
completedAtMs: Date.now()
|
|
9579
|
+
});
|
|
9580
|
+
return toFetchRouteResponse({
|
|
9581
|
+
status,
|
|
9582
|
+
body
|
|
9583
|
+
});
|
|
9584
|
+
}
|
|
9585
|
+
//#endregion
|
|
9586
|
+
//#region src/router/auth/routerApiKeyAuth.ts
|
|
9587
|
+
function readHeader(headers, name) {
|
|
9588
|
+
if (headers instanceof Headers) return String(headers.get(name) || "").trim();
|
|
9589
|
+
const direct = headers[name];
|
|
9590
|
+
const lower = headers[name.toLowerCase()];
|
|
9591
|
+
const upper = headers[name.toUpperCase()];
|
|
9592
|
+
const value = direct ?? lower ?? upper;
|
|
9593
|
+
if (typeof value === "string") return value.trim();
|
|
9594
|
+
if (Array.isArray(value)) return String(value[0] || "").trim();
|
|
9595
|
+
return "";
|
|
9596
|
+
}
|
|
9597
|
+
function extractBearerCredential(headers) {
|
|
9598
|
+
const authHeader = readHeader(headers, "authorization");
|
|
9599
|
+
if (authHeader) {
|
|
9600
|
+
if (authHeader.toLowerCase().startsWith("bearer ")) {
|
|
9601
|
+
const bearerValue = authHeader.slice(7).trim();
|
|
9602
|
+
if (bearerValue) return bearerValue;
|
|
9603
|
+
}
|
|
9604
|
+
}
|
|
9605
|
+
return null;
|
|
9606
|
+
}
|
|
9607
|
+
function normalizeSourceIp(raw) {
|
|
9608
|
+
const value = String(raw || "").trim();
|
|
9609
|
+
if (!value) return null;
|
|
9610
|
+
const maybeForwarded = value.includes(",") ? value.split(",")[0] : value;
|
|
9611
|
+
return String(maybeForwarded || "").trim() || null;
|
|
9612
|
+
}
|
|
9613
|
+
function extractRouterApiEnvironmentId(headers) {
|
|
9614
|
+
const preferred = readHeader(headers, "x-seams-environment-id");
|
|
9615
|
+
if (preferred) return preferred;
|
|
9616
|
+
const fallback = readHeader(headers, "x-environment-id");
|
|
9617
|
+
if (fallback) return fallback;
|
|
9618
|
+
return null;
|
|
9619
|
+
}
|
|
9620
|
+
function resolveSourceIpFromFetchHeaders(headers) {
|
|
9621
|
+
const cfConnecting = readHeader(headers, "cf-connecting-ip");
|
|
9622
|
+
const forwarded = readHeader(headers, "x-forwarded-for");
|
|
9623
|
+
const realIp = readHeader(headers, "x-real-ip");
|
|
9624
|
+
return normalizeSourceIp(cfConnecting || forwarded || realIp);
|
|
9625
|
+
}
|
|
9626
|
+
//#endregion
|
|
9627
|
+
//#region src/core/SessionService.ts
|
|
9628
|
+
function normalizeParsedCorsOrigin(url) {
|
|
9629
|
+
const host = url.hostname.toLowerCase();
|
|
9630
|
+
const proto = url.protocol === "http:" || url.protocol === "https:" ? url.protocol : "https:";
|
|
9631
|
+
const rawPort = String(url.port || "").trim();
|
|
9632
|
+
return `${proto}//${host}${rawPort && !(proto === "https:" && rawPort === "443") && !(proto === "http:" && rawPort === "80") ? `:${rawPort}` : ""}`;
|
|
9633
|
+
}
|
|
9634
|
+
function parseCsvList(input) {
|
|
9635
|
+
const out = /* @__PURE__ */ new Set();
|
|
9636
|
+
for (const raw of String(input || "").split(",")) {
|
|
9637
|
+
const s = raw.trim();
|
|
9638
|
+
if (!s) continue;
|
|
9639
|
+
try {
|
|
9640
|
+
const u = new URL(s);
|
|
9641
|
+
out.add(normalizeParsedCorsOrigin(u));
|
|
9642
|
+
} catch {
|
|
9643
|
+
const stripped = s.replace(/\/$/, "");
|
|
9644
|
+
if (stripped) out.add(stripped);
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
return Array.from(out);
|
|
9648
|
+
}
|
|
9649
|
+
/**
|
|
9650
|
+
* Normalize a single origin for CORS comparisons.
|
|
9651
|
+
* Returns null when the input is empty or not URL-like.
|
|
9652
|
+
*/
|
|
9653
|
+
function normalizeCorsOrigin(input) {
|
|
9654
|
+
const raw = String(input || "").trim();
|
|
9655
|
+
if (!raw) return null;
|
|
9656
|
+
try {
|
|
9657
|
+
return normalizeParsedCorsOrigin(new URL(raw));
|
|
9658
|
+
} catch {
|
|
9659
|
+
return null;
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
function buildCorsOrigins(...inputs) {
|
|
9663
|
+
const merged = /* @__PURE__ */ new Set();
|
|
9664
|
+
for (const input of inputs) for (const origin of parseCsvList(input)) merged.add(origin);
|
|
9665
|
+
const list = Array.from(merged);
|
|
9666
|
+
return list.length > 0 ? list : "*";
|
|
9667
|
+
}
|
|
9668
|
+
//#endregion
|
|
9669
|
+
//#region ../shared-ts/src/utils/routerAbTraceContext.ts
|
|
9670
|
+
const ROUTER_AB_TRACE_ID_HEADER_V1 = "x-seams-trace-id";
|
|
9671
|
+
const TRACE_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
9672
|
+
function parseRouterAbTraceContextV1(raw) {
|
|
9673
|
+
if (raw === null || raw === void 0 || raw === "") return {
|
|
9674
|
+
ok: false,
|
|
9675
|
+
reason: "missing",
|
|
9676
|
+
message: "Router trace ID is missing"
|
|
9677
|
+
};
|
|
9678
|
+
if (typeof raw !== "string" || !TRACE_ID_PATTERN.test(raw)) return {
|
|
9679
|
+
ok: false,
|
|
9680
|
+
reason: "invalid",
|
|
9681
|
+
message: "Router trace ID must be 32 lowercase hexadecimal characters"
|
|
9682
|
+
};
|
|
9683
|
+
return {
|
|
9684
|
+
ok: true,
|
|
9685
|
+
value: {
|
|
9686
|
+
kind: "router_ab_trace_context_v1",
|
|
9687
|
+
value: raw
|
|
9688
|
+
}
|
|
9689
|
+
};
|
|
9690
|
+
}
|
|
9691
|
+
function createRouterAbTraceContextV1() {
|
|
9692
|
+
const bytes = new Uint8Array(16);
|
|
9693
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
9694
|
+
let value = "";
|
|
9695
|
+
for (const byte of bytes) value += byte.toString(16).padStart(2, "0");
|
|
9696
|
+
return {
|
|
9697
|
+
kind: "router_ab_trace_context_v1",
|
|
9698
|
+
value
|
|
9699
|
+
};
|
|
9700
|
+
}
|
|
9701
|
+
//#endregion
|
|
9702
|
+
//#region ../shared-ts/src/utils/walletManagementProtocol.ts
|
|
9703
|
+
const WALLET_MANAGEMENT_PROTOCOL_HEADER = "X-Seams-Wallet-Protocol";
|
|
9704
|
+
//#endregion
|
|
9705
|
+
//#region src/router/framework/http.ts
|
|
9706
|
+
const CORS_ALLOW_METHODS = "GET,POST,PUT,PATCH,DELETE,OPTIONS";
|
|
9707
|
+
const CORS_PREFLIGHT_MAX_AGE_SECONDS = 7200;
|
|
9708
|
+
const CORS_ALLOW_HEADERS = [
|
|
9709
|
+
"Content-Type",
|
|
9710
|
+
WALLET_MANAGEMENT_PROTOCOL_HEADER,
|
|
9711
|
+
"Authorization",
|
|
9712
|
+
"X-Seams-Wallet-Region",
|
|
9713
|
+
"X-Seams-Benchmark-Diagnostics",
|
|
9714
|
+
LINKED_DEVICE_REQUEST_PROOF_HEADER_V1,
|
|
9715
|
+
ROUTER_AB_ED25519_YAO_RECOVERY_CHALLENGE_ID_HEADER_V1,
|
|
9716
|
+
ROUTER_AB_TRACE_ID_HEADER_V1,
|
|
9717
|
+
"X-Seams-Environment-Id",
|
|
9718
|
+
"X-Environment-Id"
|
|
9719
|
+
].join(",");
|
|
9720
|
+
function json$3(body, init, extraHeaders) {
|
|
9721
|
+
const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });
|
|
9722
|
+
const initHeaders = init?.headers;
|
|
9723
|
+
if (initHeaders) try {
|
|
9724
|
+
new Headers(initHeaders).forEach((v, k) => headers.set(k, v));
|
|
9725
|
+
} catch {}
|
|
9726
|
+
if (extraHeaders) for (const [k, v] of Object.entries(extraHeaders)) headers.set(k, v);
|
|
9727
|
+
const { headers: _omit, ...rest } = init || {};
|
|
9728
|
+
return new Response(JSON.stringify(body), {
|
|
9729
|
+
status: 200,
|
|
9730
|
+
...rest,
|
|
9731
|
+
headers
|
|
9732
|
+
});
|
|
9733
|
+
}
|
|
9734
|
+
function jsonFailure(status, code, message) {
|
|
9735
|
+
return json$3(failure$6(code, message), { status });
|
|
9736
|
+
}
|
|
9737
|
+
function withCors(headers, opts, request) {
|
|
9738
|
+
const pathname = request ? new URL(request.url).pathname : "";
|
|
9739
|
+
if (pathname === "/healthz" || pathname === "/readyz") {
|
|
9740
|
+
headers.set("Access-Control-Allow-Origin", "*");
|
|
9741
|
+
headers.delete("Access-Control-Allow-Credentials");
|
|
9742
|
+
headers.set("Access-Control-Allow-Methods", CORS_ALLOW_METHODS);
|
|
9743
|
+
headers.set("Access-Control-Allow-Headers", CORS_ALLOW_HEADERS);
|
|
9744
|
+
if (request?.method === "OPTIONS") headers.set("Access-Control-Max-Age", String(CORS_PREFLIGHT_MAX_AGE_SECONDS));
|
|
9745
|
+
return;
|
|
9746
|
+
}
|
|
9747
|
+
if (!opts?.corsOrigins) return;
|
|
9748
|
+
let allowedOrigin;
|
|
9749
|
+
const normalized = buildCorsOrigins(...opts.corsOrigins || []);
|
|
9750
|
+
if (normalized === "*") {
|
|
9751
|
+
allowedOrigin = "*";
|
|
9752
|
+
headers.set("Access-Control-Allow-Origin", "*");
|
|
9753
|
+
} else if (Array.isArray(normalized)) {
|
|
9754
|
+
const originRaw = String(request?.headers.get("Origin") || "").trim();
|
|
9755
|
+
const originNormalized = normalizeCorsOrigin(originRaw);
|
|
9756
|
+
if (originRaw && originNormalized && normalized.includes(originNormalized)) {
|
|
9757
|
+
allowedOrigin = originRaw;
|
|
9758
|
+
headers.set("Access-Control-Allow-Origin", originRaw);
|
|
9759
|
+
headers.append("Vary", "Origin");
|
|
9760
|
+
}
|
|
9761
|
+
}
|
|
9762
|
+
headers.set("Access-Control-Allow-Methods", CORS_ALLOW_METHODS);
|
|
9763
|
+
headers.set("Access-Control-Allow-Headers", CORS_ALLOW_HEADERS);
|
|
9764
|
+
if (allowedOrigin && request?.method === "OPTIONS") headers.set("Access-Control-Max-Age", String(CORS_PREFLIGHT_MAX_AGE_SECONDS));
|
|
9765
|
+
headers.set("Access-Control-Expose-Headers", "Server-Timing, X-Seams-Wallet-Region");
|
|
9766
|
+
if (allowedOrigin && allowedOrigin !== "*") headers.set("Access-Control-Allow-Credentials", "true");
|
|
9767
|
+
}
|
|
9768
|
+
async function readJson(request) {
|
|
9769
|
+
try {
|
|
9770
|
+
return await request.json();
|
|
9771
|
+
} catch {
|
|
9772
|
+
return null;
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
function headersToRecord(headers) {
|
|
9776
|
+
const out = {};
|
|
9777
|
+
headers.forEach((v, k) => {
|
|
9778
|
+
out[k] = v;
|
|
9779
|
+
});
|
|
9780
|
+
return out;
|
|
9781
|
+
}
|
|
9782
|
+
//#endregion
|
|
9783
|
+
//#region src/router/transport/fetch/routes/externalAuth.ts
|
|
9784
|
+
const ACTIONS = [
|
|
9785
|
+
"associate",
|
|
9786
|
+
"unlink",
|
|
9787
|
+
"prepare",
|
|
9788
|
+
"prepare-association",
|
|
9789
|
+
"prepare-unlink",
|
|
9790
|
+
"owner-challenge",
|
|
9791
|
+
"exchange",
|
|
9792
|
+
"backend-exchange",
|
|
9793
|
+
"redeem",
|
|
9794
|
+
"settings",
|
|
9795
|
+
"logout"
|
|
9796
|
+
];
|
|
9797
|
+
function parseIdentityProof(value) {
|
|
9798
|
+
const proof = requireRecord$1(value, "external identity proof");
|
|
9799
|
+
switch (proof.kind) {
|
|
9800
|
+
case "google": return {
|
|
9801
|
+
kind: "google",
|
|
9802
|
+
idToken: requireCanonicalString(proof.idToken, "idToken")
|
|
9803
|
+
};
|
|
9804
|
+
case "application": return {
|
|
9805
|
+
kind: "application",
|
|
9806
|
+
assertion: requireCanonicalString(proof.assertion, "assertion")
|
|
9807
|
+
};
|
|
9808
|
+
default: throw new Error("Unsupported external identity proof");
|
|
9809
|
+
}
|
|
9810
|
+
}
|
|
9811
|
+
function parseCommand(action, body) {
|
|
9812
|
+
const wallet = parseWalletId(body.walletId);
|
|
9813
|
+
if (!wallet.ok) throw new Error("Wallet ID is invalid");
|
|
9814
|
+
const walletId = wallet.value;
|
|
9815
|
+
if (action === "backend-exchange") return {
|
|
9816
|
+
action,
|
|
9817
|
+
walletId,
|
|
9818
|
+
attemptId: requireCanonicalString(body.attemptId, "attemptId"),
|
|
9819
|
+
proof: parseIdentityProof(body.proof)
|
|
9820
|
+
};
|
|
9821
|
+
const appOrigin = requireCanonicalString(body.appOrigin, "appOrigin");
|
|
9822
|
+
if (new URL(appOrigin).origin !== appOrigin) throw new Error("Application origin is invalid");
|
|
9823
|
+
switch (action) {
|
|
9824
|
+
case "associate":
|
|
9825
|
+
case "unlink": return {
|
|
9826
|
+
action,
|
|
9827
|
+
walletId,
|
|
9828
|
+
appOrigin,
|
|
9829
|
+
attemptId: requireCanonicalString(body.attemptId, "attemptId"),
|
|
9830
|
+
binding: secret(body.binding, "binding"),
|
|
9831
|
+
sessionToken: secret(body.sessionToken, "sessionToken"),
|
|
9832
|
+
code: secret(body.code, "code"),
|
|
9833
|
+
factorProof: body.factorProof
|
|
9834
|
+
};
|
|
9835
|
+
case "owner-challenge": return {
|
|
9836
|
+
action,
|
|
9837
|
+
walletId,
|
|
9838
|
+
appOrigin,
|
|
9839
|
+
attemptId: requireCanonicalString(body.attemptId, "attemptId"),
|
|
9840
|
+
binding: secret(body.binding, "binding"),
|
|
9841
|
+
sessionToken: secret(body.sessionToken, "sessionToken"),
|
|
9842
|
+
code: secret(body.code, "code")
|
|
9843
|
+
};
|
|
9844
|
+
case "prepare-unlink": return {
|
|
9845
|
+
action,
|
|
9846
|
+
walletId,
|
|
9847
|
+
appOrigin,
|
|
9848
|
+
associationId: requireCanonicalString(body.associationId, "associationId"),
|
|
9849
|
+
binding: secret(body.binding, "binding"),
|
|
9850
|
+
sessionToken: secret(body.sessionToken, "sessionToken")
|
|
9851
|
+
};
|
|
9852
|
+
case "prepare":
|
|
9853
|
+
case "prepare-association": return {
|
|
9854
|
+
action,
|
|
9855
|
+
walletId,
|
|
9856
|
+
appOrigin,
|
|
9857
|
+
binding: secret(body.binding, "binding"),
|
|
9858
|
+
sessionToken: secret(body.sessionToken, "sessionToken")
|
|
9859
|
+
};
|
|
9860
|
+
case "exchange": return {
|
|
9861
|
+
action,
|
|
9862
|
+
walletId,
|
|
9863
|
+
appOrigin,
|
|
9864
|
+
attemptId: requireCanonicalString(body.attemptId, "attemptId"),
|
|
8586
9865
|
idToken: requireCanonicalString(body.idToken, "idToken")
|
|
8587
9866
|
};
|
|
8588
9867
|
case "redeem": return {
|
|
@@ -8671,11 +9950,10 @@ async function resolveApplicationTrust(ctx, service, command) {
|
|
|
8671
9950
|
actorUserId: result.principal.apiKeyId,
|
|
8672
9951
|
roles: []
|
|
8673
9952
|
}, { status: "ACTIVE" }))?.find(matchesEnvironment.bind(null, scope.envId));
|
|
8674
|
-
|
|
9953
|
+
return parseExternalAuthConfiguration(environment?.externalAuth ?? {
|
|
8675
9954
|
kind: "disabled",
|
|
8676
9955
|
revision: 0
|
|
8677
9956
|
});
|
|
8678
|
-
return configuration.kind === "google" ? configuration : null;
|
|
8679
9957
|
}
|
|
8680
9958
|
function matchesEnvironment(environmentId, environment) {
|
|
8681
9959
|
return environment.id === environmentId;
|
|
@@ -8702,43 +9980,118 @@ async function handleExternalAuth(ctx) {
|
|
|
8702
9980
|
}
|
|
8703
9981
|
async function dispatch(ctx, service, trust, command) {
|
|
8704
9982
|
const walletId = command.walletId;
|
|
8705
|
-
|
|
8706
|
-
if (command.action === "prepare" || command.action === "exchange" || command.action === "backend-exchange" || command.action === "associate") {
|
|
9983
|
+
if (command.action === "prepare" || command.action === "prepare-association" || command.action === "prepare-unlink" || command.action === "exchange" || command.action === "backend-exchange" || command.action === "associate") {
|
|
8707
9984
|
const rateKey = await sha256HexUtf8(resolveSourceIpFromFetchHeaders(ctx.request.headers) || "unknown");
|
|
8708
9985
|
if (!await service.store.consumeAuthenticationAllowance(rateKey, Date.now())) return failure$5(429, "rate_limited");
|
|
8709
9986
|
}
|
|
8710
|
-
if (command.action === "
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
9987
|
+
if (command.action === "backend-exchange") {
|
|
9988
|
+
if (trust.kind === "disabled") return failure$5(403, "access_denied");
|
|
9989
|
+
return json$3(await service.exchange({
|
|
9990
|
+
walletId,
|
|
9991
|
+
attemptId: command.attemptId,
|
|
9992
|
+
proof: command.proof,
|
|
9993
|
+
trust
|
|
9994
|
+
}));
|
|
9995
|
+
}
|
|
9996
|
+
const originBinding = JSON.stringify([ctx.request.headers.get("origin") || "", command.appOrigin]);
|
|
9997
|
+
if (command.action === "prepare-unlink") return json$3(await service.prepareUnlink({
|
|
9998
|
+
walletId,
|
|
9999
|
+
associationId: command.associationId,
|
|
10000
|
+
originBinding,
|
|
10001
|
+
binding: command.binding,
|
|
10002
|
+
sessionToken: command.sessionToken,
|
|
10003
|
+
trustRevision: trust.revision
|
|
10004
|
+
}));
|
|
10005
|
+
if (command.action === "owner-challenge" || command.action === "associate" || command.action === "unlink") {
|
|
10006
|
+
const binding = {
|
|
10007
|
+
walletId,
|
|
10008
|
+
attemptId: command.attemptId,
|
|
10009
|
+
originBinding,
|
|
10010
|
+
bindingHash: await sha256HexUtf8(command.binding),
|
|
10011
|
+
sessionTokenHash: await sha256HexUtf8(command.sessionToken),
|
|
10012
|
+
codeHash: await sha256HexUtf8(command.code),
|
|
10013
|
+
trustRevision: trust.revision,
|
|
8715
10014
|
nowMs: Date.now()
|
|
10015
|
+
};
|
|
10016
|
+
const result = await readExternalOwnerOperation({
|
|
10017
|
+
store: service.store,
|
|
10018
|
+
binding,
|
|
10019
|
+
requestOrigin: ctx.request.headers.get("origin") || "",
|
|
10020
|
+
phase: command.action === "owner-challenge" ? "prepare" : "complete"
|
|
8716
10021
|
});
|
|
8717
|
-
if (!
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
10022
|
+
if (!result.ok) return json$3(result, { status: 403 });
|
|
10023
|
+
const ownerOperation = result.value;
|
|
10024
|
+
if (command.action === "owner-challenge") return json$3({
|
|
10025
|
+
ok: true,
|
|
10026
|
+
value: externalOwnerChallenge(ownerOperation)
|
|
10027
|
+
});
|
|
10028
|
+
if (command.action !== ownerOperation.attempt.purpose) return failure$5(403, "access_denied");
|
|
10029
|
+
if (ownerOperation.attempt.status === "consumed") {
|
|
10030
|
+
const stored = await ctx.service.authorizedOperations.readAuthorizedOperationById({
|
|
10031
|
+
tenantId: ctx.service.authorizedOperations.tenantId,
|
|
10032
|
+
authorizedOperationId: walletOwnerOperationAuthorizationId(ownerOperation.operation, ownerOperation.challengeB64u)
|
|
10033
|
+
});
|
|
10034
|
+
if (!stored || stored.authorization.kind !== "verified_step_up" || stored.quota.kind !== "quota_neutral" || stored.lifecycle !== "completed") return failure$5(503, "unavailable");
|
|
10035
|
+
return new Response(stored.response.bodyText, {
|
|
10036
|
+
status: stored.response.status,
|
|
10037
|
+
headers: {
|
|
10038
|
+
"Content-Type": stored.response.contentType,
|
|
10039
|
+
"Cache-Control": "no-store"
|
|
10040
|
+
}
|
|
10041
|
+
});
|
|
8721
10042
|
}
|
|
8722
|
-
const
|
|
8723
|
-
|
|
8724
|
-
|
|
10043
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
10044
|
+
ctx,
|
|
10045
|
+
walletId,
|
|
10046
|
+
operation: ownerOperation.operation,
|
|
10047
|
+
payload: ownerOperation.payload,
|
|
10048
|
+
factorProof: command.factorProof
|
|
10049
|
+
});
|
|
10050
|
+
if (!authorized.ok) return authorized.response;
|
|
10051
|
+
if (!await service.store.consumeOwnerAttempt({
|
|
10052
|
+
walletId,
|
|
10053
|
+
attemptId: binding.attemptId,
|
|
10054
|
+
originBinding,
|
|
10055
|
+
bindingHash: binding.bindingHash,
|
|
10056
|
+
sessionTokenHash: binding.sessionTokenHash,
|
|
10057
|
+
codeHash: binding.codeHash,
|
|
10058
|
+
trustRevision: binding.trustRevision,
|
|
10059
|
+
nowMs: Date.now()
|
|
10060
|
+
}, authorized.ownerAuthority)) return completeWalletOwnerOperation(ctx, authorized.operation, 403, externalAuthFailure("access_denied"), "failed_before_side_effect");
|
|
10061
|
+
const attempt = ownerOperation.attempt;
|
|
10062
|
+
if (attempt.purpose === "associate") {
|
|
10063
|
+
if (!await service.store.associate({
|
|
10064
|
+
walletId,
|
|
10065
|
+
identity: attempt.identity,
|
|
10066
|
+
authorityId: authorized.ownerAuthority.authorityId,
|
|
10067
|
+
authorityEpoch: authorized.ownerAuthority.revocationEpoch
|
|
10068
|
+
})) return completeWalletOwnerOperation(ctx, authorized.operation, 403, externalAuthFailure("access_denied"), "failed_before_side_effect");
|
|
10069
|
+
} else await service.store.revokeAssociation({
|
|
8725
10070
|
walletId,
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
10071
|
+
associationId: attempt.associationId,
|
|
10072
|
+
owner: authorized.ownerAuthority
|
|
10073
|
+
});
|
|
10074
|
+
return completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
10075
|
+
ok: true,
|
|
10076
|
+
value: null
|
|
10077
|
+
});
|
|
8730
10078
|
}
|
|
8731
|
-
if (
|
|
10079
|
+
if (trust.kind === "disabled") return failure$5(403, "access_denied");
|
|
10080
|
+
if (command.action === "prepare" || command.action === "prepare-association") return json$3(await service.prepare({
|
|
10081
|
+
purpose: command.action === "prepare" ? "authenticate" : "associate",
|
|
8732
10082
|
walletId,
|
|
8733
10083
|
originBinding,
|
|
8734
10084
|
binding: command.binding,
|
|
8735
10085
|
sessionToken: command.sessionToken
|
|
8736
10086
|
}));
|
|
8737
|
-
if (command.action === "exchange"
|
|
10087
|
+
if (command.action === "exchange") return json$3(await service.exchange({
|
|
8738
10088
|
trust,
|
|
8739
10089
|
walletId,
|
|
8740
10090
|
attemptId: command.attemptId,
|
|
8741
|
-
|
|
10091
|
+
proof: {
|
|
10092
|
+
kind: "google",
|
|
10093
|
+
idToken: command.idToken
|
|
10094
|
+
}
|
|
8742
10095
|
}));
|
|
8743
10096
|
const tokenHash = await sha256HexUtf8(command.sessionToken);
|
|
8744
10097
|
if (command.action === "logout") {
|
|
@@ -10517,482 +11870,6 @@ async function computeOwnerLaneParticipantBindingDigestV1(input) {
|
|
|
10517
11870
|
return requireParsed$4(parseLaneParticipantBindingDigestB64u(base64UrlEncode(await sha256Bytes(ownerLaneParticipantContinuityCanonicalBytesV1(input)))), "owner participant digest");
|
|
10518
11871
|
}
|
|
10519
11872
|
//#endregion
|
|
10520
|
-
//#region ../shared-ts/src/authorization/operationFingerprint.ts
|
|
10521
|
-
const CAPABILITY_OPERATION_FINGERPRINT_DOMAIN_V1 = "seams:authorization:capability-operation-fingerprint:v1";
|
|
10522
|
-
var CapabilityOperationEnvelopeProof = class {
|
|
10523
|
-
tenantId;
|
|
10524
|
-
principalId;
|
|
10525
|
-
capabilityId;
|
|
10526
|
-
operationId;
|
|
10527
|
-
operation;
|
|
10528
|
-
digests;
|
|
10529
|
-
retainProof() {
|
|
10530
|
-
return true;
|
|
10531
|
-
}
|
|
10532
|
-
constructor(fields) {
|
|
10533
|
-
this.retainProof();
|
|
10534
|
-
this.tenantId = fields.tenantId;
|
|
10535
|
-
this.principalId = fields.principalId;
|
|
10536
|
-
this.capabilityId = fields.capabilityId;
|
|
10537
|
-
this.operationId = fields.operationId;
|
|
10538
|
-
this.operation = fields.operation;
|
|
10539
|
-
this.digests = fields.digests;
|
|
10540
|
-
}
|
|
10541
|
-
};
|
|
10542
|
-
function buildCapabilityOperationEnvelope(fields) {
|
|
10543
|
-
return new CapabilityOperationEnvelopeProof({
|
|
10544
|
-
tenantId: fields.tenantId,
|
|
10545
|
-
principalId: fields.principalId,
|
|
10546
|
-
capabilityId: fields.capabilityId,
|
|
10547
|
-
operationId: fields.operationId,
|
|
10548
|
-
operation: fields.operation,
|
|
10549
|
-
digests: {
|
|
10550
|
-
laneDigest: fields.digests.laneDigest,
|
|
10551
|
-
intentDigest: fields.digests.intentDigest,
|
|
10552
|
-
displayDigest: fields.digests.displayDigest
|
|
10553
|
-
}
|
|
10554
|
-
});
|
|
10555
|
-
}
|
|
10556
|
-
function canonicalCapabilityOperationFingerprintPreimageV1(envelope) {
|
|
10557
|
-
return `${CAPABILITY_OPERATION_FINGERPRINT_DOMAIN_V1}|${alphabetizeStringify({
|
|
10558
|
-
tenantId: envelope.tenantId,
|
|
10559
|
-
principalId: envelope.principalId,
|
|
10560
|
-
capabilityId: envelope.capabilityId,
|
|
10561
|
-
operationId: envelope.operationId,
|
|
10562
|
-
operation: envelope.operation,
|
|
10563
|
-
digests: envelope.digests
|
|
10564
|
-
})}`;
|
|
10565
|
-
}
|
|
10566
|
-
async function computeCapabilityOperationFingerprintDigest(envelope) {
|
|
10567
|
-
return parseCapabilityOperationFingerprintDigest(base64UrlEncode(await sha256BytesUtf8(canonicalCapabilityOperationFingerprintPreimageV1(envelope))));
|
|
10568
|
-
}
|
|
10569
|
-
function parseCapabilityOperationFingerprintDigest(value) {
|
|
10570
|
-
return parseDigestB64u(value);
|
|
10571
|
-
}
|
|
10572
|
-
function parseSigningOperationFingerprintDigest(value) {
|
|
10573
|
-
if (typeof value !== "string" || !value.startsWith("sha256:")) throw new Error("signing operation fingerprint must use the sha256: prefix");
|
|
10574
|
-
return parseDigestB64u(value.slice(7));
|
|
10575
|
-
}
|
|
10576
|
-
//#endregion
|
|
10577
|
-
//#region ../shared-ts/src/utils/base58.ts
|
|
10578
|
-
/**
|
|
10579
|
-
* Encodes binary data to base58 string using Bitcoin's base58 alphabet.
|
|
10580
|
-
*
|
|
10581
|
-
* @param data - Binary data to encode (Uint8Array, ArrayBuffer, or number[])
|
|
10582
|
-
* @returns Base58-encoded string
|
|
10583
|
-
*/
|
|
10584
|
-
const base58Encode = (data) => {
|
|
10585
|
-
if (data instanceof ArrayBuffer) return bs58.encode(new Uint8Array(data));
|
|
10586
|
-
if (Array.isArray(data)) return bs58.encode(new Uint8Array(data));
|
|
10587
|
-
return bs58.encode(data);
|
|
10588
|
-
};
|
|
10589
|
-
/**
|
|
10590
|
-
* Decodes a base58 string to binary data using Bitcoin's base58 alphabet.
|
|
10591
|
-
* Returns a Uint8Array containing the decoded bytes.
|
|
10592
|
-
*
|
|
10593
|
-
* @param base58String - Base58-encoded string to decode
|
|
10594
|
-
* @returns Uint8Array containing the decoded bytes
|
|
10595
|
-
* @throws Error if input contains invalid base58 characters
|
|
10596
|
-
*/
|
|
10597
|
-
const base58Decode = (base58String) => {
|
|
10598
|
-
return bs58.decode(base58String);
|
|
10599
|
-
};
|
|
10600
|
-
//#endregion
|
|
10601
|
-
//#region src/authorization/domain.ts
|
|
10602
|
-
function buildExactWalletSessionQuotaProjectionV1(fields) {
|
|
10603
|
-
requireNonnegativeInteger(fields.remainingUses, "Wallet Session quota remaining uses");
|
|
10604
|
-
requirePositiveTime(fields.expiresAtMs, "Wallet Session quota expiry");
|
|
10605
|
-
if (fields.lifecycle === "exhausted" !== (fields.remainingUses === 0)) throw new Error("Wallet Session quota lifecycle disagrees with its remaining uses");
|
|
10606
|
-
return {
|
|
10607
|
-
kind: "exact_wallet_session_quota_projection_v1",
|
|
10608
|
-
lifecycle: fields.lifecycle,
|
|
10609
|
-
tenantId: fields.tenantId,
|
|
10610
|
-
principalId: fields.principalId,
|
|
10611
|
-
walletSessionId: fields.walletSessionId,
|
|
10612
|
-
quotaId: fields.quotaId,
|
|
10613
|
-
remainingUses: fields.remainingUses,
|
|
10614
|
-
expiresAtMs: fields.expiresAtMs
|
|
10615
|
-
};
|
|
10616
|
-
}
|
|
10617
|
-
/**
|
|
10618
|
-
* Every capability subject must resolve through the authoritative active
|
|
10619
|
-
* authority that the authorization names. A signing or export subject resolves
|
|
10620
|
-
* only when its key family is activated and names the same material the
|
|
10621
|
-
* authority currently holds; an administration subject resolves only when it
|
|
10622
|
-
* names that same authority. Anything else is a capability the caller can no
|
|
10623
|
-
* longer exercise, not a corrupt row.
|
|
10624
|
-
*/
|
|
10625
|
-
function exactWalletSessionCapabilitySubjectsResolveAuthority(input) {
|
|
10626
|
-
return input.session.capabilitySubjects.every((subject) => {
|
|
10627
|
-
switch (subject.kind) {
|
|
10628
|
-
case "sign":
|
|
10629
|
-
case "export_keys": {
|
|
10630
|
-
const activation = subject.keyFamily === "ed25519" ? input.signerActivations.ed25519 : input.signerActivations.ecdsa;
|
|
10631
|
-
return activation !== void 0 && mpcMaterialActivationRefsEqual(activation.materialActivation, subject.materialActivation);
|
|
10632
|
-
}
|
|
10633
|
-
case "link_devices":
|
|
10634
|
-
case "revoke_devices": return subject.authorityId === input.session.authorityId;
|
|
10635
|
-
default: return false;
|
|
10636
|
-
}
|
|
10637
|
-
});
|
|
10638
|
-
}
|
|
10639
|
-
/**
|
|
10640
|
-
* The digest-free authorization projection published to the browser. It carries
|
|
10641
|
-
* the exact wallet, authority, method, capability, and lifetime identity a
|
|
10642
|
-
* reader needs to reconcile its own record, and never the credential digest.
|
|
10643
|
-
*/
|
|
10644
|
-
function projectExactWalletSessionAuthorizationV1(session) {
|
|
10645
|
-
const capabilitySubjects = session.capabilitySubjects.map((subject) => {
|
|
10646
|
-
switch (subject.kind) {
|
|
10647
|
-
case "sign":
|
|
10648
|
-
case "export_keys": return {
|
|
10649
|
-
kind: subject.kind,
|
|
10650
|
-
keyFamily: subject.keyFamily,
|
|
10651
|
-
materialActivation: subject.materialActivation
|
|
10652
|
-
};
|
|
10653
|
-
case "link_devices":
|
|
10654
|
-
case "revoke_devices": return { kind: subject.kind };
|
|
10655
|
-
default: throw new Error("Issued linked-device Wallet Session subject is invalid");
|
|
10656
|
-
}
|
|
10657
|
-
});
|
|
10658
|
-
const first = capabilitySubjects[0];
|
|
10659
|
-
if (!first) throw new Error("Issued linked-device Wallet Session has no subjects");
|
|
10660
|
-
return parseActiveWalletSessionV1({
|
|
10661
|
-
kind: "active_wallet_session_v1",
|
|
10662
|
-
walletId: session.walletId,
|
|
10663
|
-
authorityId: session.authorityId,
|
|
10664
|
-
authMethodId: session.walletAuthMethodId,
|
|
10665
|
-
authorizationId: session.authorizationId,
|
|
10666
|
-
quotaId: session.quotaId,
|
|
10667
|
-
authorityDigestB64u: session.authorityDigestB64u,
|
|
10668
|
-
authorityRevocationEpoch: session.authorityRevocationEpoch,
|
|
10669
|
-
capabilitySubjects: [first, ...capabilitySubjects.slice(1)],
|
|
10670
|
-
issuedAtMs: session.createdAtMs,
|
|
10671
|
-
expiresAtMs: session.expiresAtMs
|
|
10672
|
-
});
|
|
10673
|
-
}
|
|
10674
|
-
function projectActiveWalletSession(issued) {
|
|
10675
|
-
if (issued.quota.quotaId !== issued.session.quotaId) throw new Error("Issued Wallet Session quota does not belong to its authorization");
|
|
10676
|
-
return projectExactWalletSessionAuthorizationV1(issued.session);
|
|
10677
|
-
}
|
|
10678
|
-
function appendWalletSessionSignerSubjects(subjects, kind, signerActivations) {
|
|
10679
|
-
if (signerActivations.keyFamilies.length === 1 && signerActivations.keyFamilies[0] === "ed25519") {
|
|
10680
|
-
if (!signerActivations.ed25519) throw new Error("Ed25519 signer activation is missing");
|
|
10681
|
-
subjects.push({
|
|
10682
|
-
kind,
|
|
10683
|
-
keyFamily: "ed25519",
|
|
10684
|
-
materialActivation: signerActivations.ed25519.materialActivation
|
|
10685
|
-
});
|
|
10686
|
-
return;
|
|
10687
|
-
}
|
|
10688
|
-
if (signerActivations.keyFamilies.length === 1 && signerActivations.keyFamilies[0] === "ecdsa_secp256k1") {
|
|
10689
|
-
if (!signerActivations.ecdsa) throw new Error("ECDSA signer activation is missing");
|
|
10690
|
-
subjects.push({
|
|
10691
|
-
kind,
|
|
10692
|
-
keyFamily: "ecdsa_secp256k1",
|
|
10693
|
-
materialActivation: signerActivations.ecdsa.materialActivation
|
|
10694
|
-
});
|
|
10695
|
-
return;
|
|
10696
|
-
}
|
|
10697
|
-
if (!signerActivations.ed25519 || !signerActivations.ecdsa) throw new Error("both-family signer activations are incomplete");
|
|
10698
|
-
subjects.push({
|
|
10699
|
-
kind,
|
|
10700
|
-
keyFamily: "ed25519",
|
|
10701
|
-
materialActivation: signerActivations.ed25519.materialActivation
|
|
10702
|
-
}, {
|
|
10703
|
-
kind,
|
|
10704
|
-
keyFamily: "ecdsa_secp256k1",
|
|
10705
|
-
materialActivation: signerActivations.ecdsa.materialActivation
|
|
10706
|
-
});
|
|
10707
|
-
}
|
|
10708
|
-
function buildWalletSessionCapabilitySubjectsV1(authority) {
|
|
10709
|
-
const subjects = [];
|
|
10710
|
-
if (authority.permissions.includes("sign")) appendWalletSessionSignerSubjects(subjects, "sign", authority.signerActivations);
|
|
10711
|
-
if (authority.permissions.includes("export_keys")) appendWalletSessionSignerSubjects(subjects, "export_keys", authority.signerActivations);
|
|
10712
|
-
if (authority.permissions.includes("link_devices")) subjects.push({
|
|
10713
|
-
kind: "link_devices",
|
|
10714
|
-
authorityId: authority.authorityId
|
|
10715
|
-
});
|
|
10716
|
-
if (authority.permissions.includes("revoke_devices")) subjects.push({
|
|
10717
|
-
kind: "revoke_devices",
|
|
10718
|
-
authorityId: authority.authorityId
|
|
10719
|
-
});
|
|
10720
|
-
const [firstSubject, ...remainingSubjects] = subjects;
|
|
10721
|
-
if (!firstSubject) throw new Error("active wallet authority must produce capability subjects");
|
|
10722
|
-
return [firstSubject, ...remainingSubjects];
|
|
10723
|
-
}
|
|
10724
|
-
function buildWalletSessionAuthorizationV2(fields) {
|
|
10725
|
-
requireNonnegativeInteger(fields.authorityRevocationEpoch, "Wallet Session authority revocation epoch");
|
|
10726
|
-
parseDigestB64u(fields.authorityDigestB64u);
|
|
10727
|
-
if (fields.capabilitySubjects.length === 0) throw new Error("Wallet Session capability subjects are required");
|
|
10728
|
-
requireOrderedTimes(fields.createdAtMs, fields.expiresAtMs, "Wallet Session authorization");
|
|
10729
|
-
const identityValues = [
|
|
10730
|
-
String(fields.authorizationId),
|
|
10731
|
-
String(fields.walletSessionId),
|
|
10732
|
-
String(fields.quotaId)
|
|
10733
|
-
];
|
|
10734
|
-
if (new Set(identityValues).size !== identityValues.length) throw new Error("Wallet Session authorization identities must be pairwise distinct");
|
|
10735
|
-
return {
|
|
10736
|
-
kind: "wallet_session_authorization_v2",
|
|
10737
|
-
tenantId: fields.tenantId,
|
|
10738
|
-
principalId: fields.principalId,
|
|
10739
|
-
walletId: fields.walletId,
|
|
10740
|
-
authorityId: fields.authorityId,
|
|
10741
|
-
walletAuthMethodId: fields.walletAuthMethodId,
|
|
10742
|
-
authorityDigestB64u: fields.authorityDigestB64u,
|
|
10743
|
-
authorityRevocationEpoch: fields.authorityRevocationEpoch,
|
|
10744
|
-
mintId: fields.mintId,
|
|
10745
|
-
authorizationId: fields.authorizationId,
|
|
10746
|
-
walletSessionId: fields.walletSessionId,
|
|
10747
|
-
quotaId: fields.quotaId,
|
|
10748
|
-
capabilitySubjects: fields.capabilitySubjects,
|
|
10749
|
-
createdAtMs: fields.createdAtMs,
|
|
10750
|
-
expiresAtMs: fields.expiresAtMs
|
|
10751
|
-
};
|
|
10752
|
-
}
|
|
10753
|
-
function walletSessionCapabilitySubjectsV1Equal(left, right) {
|
|
10754
|
-
if (left.length !== right.length) return false;
|
|
10755
|
-
for (let index = 0; index < left.length; index += 1) {
|
|
10756
|
-
const leftSubject = left[index];
|
|
10757
|
-
const rightSubject = right[index];
|
|
10758
|
-
if (!leftSubject || !rightSubject || leftSubject.kind !== rightSubject.kind) return false;
|
|
10759
|
-
switch (leftSubject.kind) {
|
|
10760
|
-
case "sign":
|
|
10761
|
-
case "export_keys": break;
|
|
10762
|
-
case "link_devices":
|
|
10763
|
-
case "revoke_devices":
|
|
10764
|
-
if (rightSubject.kind !== leftSubject.kind || leftSubject.authorityId !== rightSubject.authorityId) return false;
|
|
10765
|
-
break;
|
|
10766
|
-
}
|
|
10767
|
-
if (leftSubject.kind === "sign" || leftSubject.kind === "export_keys") {
|
|
10768
|
-
if (rightSubject.kind !== leftSubject.kind || leftSubject.keyFamily !== rightSubject.keyFamily || !mpcMaterialActivationRefsEqual(leftSubject.materialActivation, rightSubject.materialActivation)) return false;
|
|
10769
|
-
}
|
|
10770
|
-
}
|
|
10771
|
-
return true;
|
|
10772
|
-
}
|
|
10773
|
-
function walletSessionAuthorizationV2RecordsEqual(left, right) {
|
|
10774
|
-
return left.kind === right.kind && left.tenantId === right.tenantId && left.principalId === right.principalId && left.walletId === right.walletId && left.authorityId === right.authorityId && left.walletAuthMethodId === right.walletAuthMethodId && left.authorityDigestB64u === right.authorityDigestB64u && left.authorityRevocationEpoch === right.authorityRevocationEpoch && left.mintId === right.mintId && left.authorizationId === right.authorizationId && left.walletSessionId === right.walletSessionId && left.quotaId === right.quotaId && walletSessionCapabilitySubjectsV1Equal(left.capabilitySubjects, right.capabilitySubjects) && left.createdAtMs === right.createdAtMs && left.expiresAtMs === right.expiresAtMs;
|
|
10775
|
-
}
|
|
10776
|
-
function parseMpcMaterialActivationRefRequired(value) {
|
|
10777
|
-
const parsed = parseMpcMaterialActivationRef(value);
|
|
10778
|
-
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
10779
|
-
return parsed.value;
|
|
10780
|
-
}
|
|
10781
|
-
function parseWalletAuthorityIdRequired(value) {
|
|
10782
|
-
const parsed = parseWalletAuthorityId(value);
|
|
10783
|
-
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
10784
|
-
return parsed.value;
|
|
10785
|
-
}
|
|
10786
|
-
function parseWalletAuthMethodIdRequired(value) {
|
|
10787
|
-
const parsed = parseWalletAuthMethodId(value);
|
|
10788
|
-
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
10789
|
-
return parsed.value;
|
|
10790
|
-
}
|
|
10791
|
-
function parseWalletSessionCapabilitySubject(value) {
|
|
10792
|
-
if (!isWalletSessionCapabilitySubjectRecordV1(value) || typeof value.kind !== "string") throw new Error("Wallet Session capability subject must be an object");
|
|
10793
|
-
switch (value.kind) {
|
|
10794
|
-
case "sign":
|
|
10795
|
-
case "export_keys":
|
|
10796
|
-
if (Object.keys(value).sort().join("|") !== "keyFamily|kind|materialActivation") throw new Error("Wallet Session signer capability subject contains unexpected fields");
|
|
10797
|
-
if (value.keyFamily !== "ed25519" && value.keyFamily !== "ecdsa_secp256k1") throw new Error("Wallet Session signer capability subject key family is invalid");
|
|
10798
|
-
return {
|
|
10799
|
-
kind: value.kind,
|
|
10800
|
-
keyFamily: value.keyFamily,
|
|
10801
|
-
materialActivation: parseMpcMaterialActivationRefRequired(value.materialActivation)
|
|
10802
|
-
};
|
|
10803
|
-
case "link_devices":
|
|
10804
|
-
case "revoke_devices":
|
|
10805
|
-
if (Object.keys(value).sort().join("|") !== "authorityId|kind") throw new Error("Wallet Session administration capability subject contains unexpected fields");
|
|
10806
|
-
return {
|
|
10807
|
-
kind: value.kind,
|
|
10808
|
-
authorityId: parseWalletAuthorityIdRequired(value.authorityId)
|
|
10809
|
-
};
|
|
10810
|
-
default: throw new Error("Wallet Session capability subject kind is unsupported");
|
|
10811
|
-
}
|
|
10812
|
-
}
|
|
10813
|
-
function isWalletSessionCapabilitySubjectRecordV1(value) {
|
|
10814
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10815
|
-
}
|
|
10816
|
-
function parseWalletSessionCapabilitySubjects(value) {
|
|
10817
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error("Wallet Session capability subjects must be non-empty");
|
|
10818
|
-
const subjects = [];
|
|
10819
|
-
for (const subject of value) subjects.push(parseWalletSessionCapabilitySubject(subject));
|
|
10820
|
-
const [firstSubject, ...remainingSubjects] = subjects;
|
|
10821
|
-
if (!firstSubject) throw new Error("Wallet Session capability subjects must be non-empty");
|
|
10822
|
-
return [firstSubject, ...remainingSubjects];
|
|
10823
|
-
}
|
|
10824
|
-
function parseWalletSessionAuthorizationV2(value) {
|
|
10825
|
-
if (!isWalletSessionAuthorizationRecordV2(value)) throw new Error("Wallet Session authorization V2 contains unexpected fields");
|
|
10826
|
-
if (value.kind !== "wallet_session_authorization_v2") throw new Error("Wallet Session authorization V2 kind is invalid");
|
|
10827
|
-
const tenantId = parseTenantIdRequired(value.tenantId);
|
|
10828
|
-
const principalId = parsePrincipalIdRequired(value.principalId);
|
|
10829
|
-
const walletId = parseWalletIdRequired(value.walletId);
|
|
10830
|
-
const authorityId = parseWalletAuthorityIdRequired(value.authorityId);
|
|
10831
|
-
const walletAuthMethodId = parseWalletAuthMethodIdRequired(value.walletAuthMethodId);
|
|
10832
|
-
const authorityDigestB64u = parseDigestB64u(value.authorityDigestB64u);
|
|
10833
|
-
const mintIdResult = parseWalletSessionMintId(value.mintId);
|
|
10834
|
-
if (!mintIdResult.ok) throw new Error(mintIdResult.error.message);
|
|
10835
|
-
const authorizationIdResult = parseWalletSessionAuthorizationId(value.authorizationId);
|
|
10836
|
-
if (!authorizationIdResult.ok) throw new Error(authorizationIdResult.error.message);
|
|
10837
|
-
const walletSessionId = parseWalletSessionIdRequired(value.walletSessionId);
|
|
10838
|
-
const quotaId = parseMpcWalletSigningQuotaIdRequired(value.quotaId);
|
|
10839
|
-
return buildWalletSessionAuthorizationV2({
|
|
10840
|
-
tenantId,
|
|
10841
|
-
principalId,
|
|
10842
|
-
walletId,
|
|
10843
|
-
authorityId,
|
|
10844
|
-
walletAuthMethodId,
|
|
10845
|
-
authorityDigestB64u,
|
|
10846
|
-
authorityRevocationEpoch: requireNonnegativeInteger(value.authorityRevocationEpoch, "Wallet Session authority revocation epoch"),
|
|
10847
|
-
mintId: mintIdResult.value,
|
|
10848
|
-
authorizationId: authorizationIdResult.value,
|
|
10849
|
-
walletSessionId,
|
|
10850
|
-
quotaId,
|
|
10851
|
-
capabilitySubjects: parseWalletSessionCapabilitySubjects(value.capabilitySubjects),
|
|
10852
|
-
createdAtMs: requirePositiveTimestamp(value.createdAtMs, "Wallet Session createdAtMs"),
|
|
10853
|
-
expiresAtMs: requirePositiveTimestamp(value.expiresAtMs, "Wallet Session expiresAtMs")
|
|
10854
|
-
});
|
|
10855
|
-
}
|
|
10856
|
-
function isWalletSessionAuthorizationRecordV2(value) {
|
|
10857
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10858
|
-
return Object.keys(value).sort().join("|") === "authorityDigestB64u|authorityId|authorityRevocationEpoch|authorizationId|capabilitySubjects|createdAtMs|expiresAtMs|kind|mintId|principalId|quotaId|tenantId|walletAuthMethodId|walletId|walletSessionId";
|
|
10859
|
-
}
|
|
10860
|
-
async function buildAuthorizedOperation(input) {
|
|
10861
|
-
if (input.tenantId !== input.operation.tenantId) throw new Error("authorized operation tenant must match its operation envelope");
|
|
10862
|
-
const operationFingerprintDigest = await computeCapabilityOperationFingerprintDigest(input.operation);
|
|
10863
|
-
return {
|
|
10864
|
-
...input,
|
|
10865
|
-
kind: "authorized_operation",
|
|
10866
|
-
operationFingerprintDigest,
|
|
10867
|
-
lifecycle: "claimed"
|
|
10868
|
-
};
|
|
10869
|
-
}
|
|
10870
|
-
const AUTHORIZED_OPERATION_RESULT_DIGEST_DOMAIN_V1 = "seams:authorization:authorized-operation-result:v1";
|
|
10871
|
-
function parseAuthorizedOperationReplayResponse(value) {
|
|
10872
|
-
if (!isAuthorizedOperationReplayResponseRecordV1(value)) throw new Error("authorized operation replay response must be an object");
|
|
10873
|
-
const keys = Object.keys(value).sort();
|
|
10874
|
-
if (keys.length !== 3 || keys.join("|") !== "bodyText|contentType|status") throw new Error("authorized operation replay response must contain only status, contentType, and bodyText");
|
|
10875
|
-
const status = value.status;
|
|
10876
|
-
if (typeof status !== "number" || !Number.isSafeInteger(status) || status < 200 || status > 599) throw new Error("authorized operation replay response status must be an HTTP status");
|
|
10877
|
-
const contentType = value.contentType;
|
|
10878
|
-
if (typeof contentType !== "string" || contentType.length === 0 || contentType.trim() !== contentType || contentType.length > 255 || /[\u0000-\u001f\u007f]/.test(contentType)) throw new Error("authorized operation replay response contentType is invalid");
|
|
10879
|
-
const bodyText = value.bodyText;
|
|
10880
|
-
if (typeof bodyText !== "string") throw new Error("authorized operation replay response bodyText must be text");
|
|
10881
|
-
if (new TextEncoder().encode(bodyText).byteLength > 65536) throw new Error("authorized operation replay response bodyText is too large");
|
|
10882
|
-
if (responseStatusHasNoBody(status) && bodyText.length !== 0) throw new Error("authorized operation replay response status cannot carry a body");
|
|
10883
|
-
return {
|
|
10884
|
-
status,
|
|
10885
|
-
contentType,
|
|
10886
|
-
bodyText
|
|
10887
|
-
};
|
|
10888
|
-
}
|
|
10889
|
-
function authorizedOperationReplayBodyInit(response) {
|
|
10890
|
-
const parsed = parseAuthorizedOperationReplayResponse(response);
|
|
10891
|
-
return responseStatusHasNoBody(parsed.status) ? null : parsed.bodyText;
|
|
10892
|
-
}
|
|
10893
|
-
function responseStatusHasNoBody(status) {
|
|
10894
|
-
return status === 204 || status === 205 || status === 304;
|
|
10895
|
-
}
|
|
10896
|
-
function isAuthorizedOperationReplayResponseRecordV1(value) {
|
|
10897
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
10898
|
-
return Object.keys(value).sort().join("|") === "bodyText|contentType|status";
|
|
10899
|
-
}
|
|
10900
|
-
async function computeAuthorizedOperationResultDigest(response) {
|
|
10901
|
-
return parseDigestB64u(base64UrlEncode(await sha256BytesUtf8(`${AUTHORIZED_OPERATION_RESULT_DIGEST_DOMAIN_V1}|${alphabetizeStringify(parseAuthorizedOperationReplayResponse(response))}`)));
|
|
10902
|
-
}
|
|
10903
|
-
function parseHostedWalletSeamsSessionExchangeCode(value) {
|
|
10904
|
-
return parseAuthorizationDomainId(value, "hostedWalletSessionExchangeCode");
|
|
10905
|
-
}
|
|
10906
|
-
function parseVerifiedOwnerProofId(value) {
|
|
10907
|
-
return parseAuthorizationDomainId(value, "verifiedOwnerProofId");
|
|
10908
|
-
}
|
|
10909
|
-
function parseHostedWalletSeamsSessionExchangeNonce(value) {
|
|
10910
|
-
return parseAuthorizationDomainId(value, "hostedWalletSessionExchangeNonce");
|
|
10911
|
-
}
|
|
10912
|
-
function parseHostedWalletSessionCredentialId(value) {
|
|
10913
|
-
return parseAuthorizationDomainId(value, "hostedWalletSessionCredentialId");
|
|
10914
|
-
}
|
|
10915
|
-
function parsePrimaryWalletSessionOperationCredentialToken(value) {
|
|
10916
|
-
if (typeof value !== "string" || !/^wst_[A-Za-z0-9_-]{43}$/.test(value)) throw new Error("primary Wallet Session credential is invalid");
|
|
10917
|
-
return parseAuthorizationDomainId(value, "primaryWalletSessionOperationCredentialToken");
|
|
10918
|
-
}
|
|
10919
|
-
function parseHostedWalletSessionOperationCredentialToken(value) {
|
|
10920
|
-
if (typeof value !== "string" || !/^wsh_[A-Za-z0-9_-]{43}$/.test(value)) throw new Error("HostedWalletSessionOperationCredentialV1.token is invalid");
|
|
10921
|
-
return parseAuthorizationDomainId(value, "hostedWalletSessionOperationCredentialToken");
|
|
10922
|
-
}
|
|
10923
|
-
function parseSessionOrigin(value) {
|
|
10924
|
-
if (typeof value !== "string" || value.trim() !== value) throw new Error("session origin must be a canonical HTTP origin");
|
|
10925
|
-
let url;
|
|
10926
|
-
try {
|
|
10927
|
-
url = new URL(value);
|
|
10928
|
-
} catch {
|
|
10929
|
-
throw new Error("session origin must be a canonical HTTP origin");
|
|
10930
|
-
}
|
|
10931
|
-
if (url.protocol !== "https:" && url.protocol !== "http:" || url.origin !== value || url.pathname !== "/" || url.search || url.hash) throw new Error("session origin must be a canonical HTTP origin");
|
|
10932
|
-
return value;
|
|
10933
|
-
}
|
|
10934
|
-
function buildActiveWalletSessionQuota(fields) {
|
|
10935
|
-
requirePositiveCount(fields.remainingUses, "Wallet Session quota remaining uses");
|
|
10936
|
-
requirePositiveTime(fields.expiresAtMs, "Wallet Session quota expiry");
|
|
10937
|
-
return {
|
|
10938
|
-
kind: "active_wallet_session_quota",
|
|
10939
|
-
tenantId: fields.tenantId,
|
|
10940
|
-
principalId: fields.principalId,
|
|
10941
|
-
walletSessionId: fields.walletSessionId,
|
|
10942
|
-
quotaId: fields.quotaId,
|
|
10943
|
-
remainingUses: fields.remainingUses,
|
|
10944
|
-
expiresAtMs: fields.expiresAtMs
|
|
10945
|
-
};
|
|
10946
|
-
}
|
|
10947
|
-
function parseAuthorizationDomainId(value, fieldName) {
|
|
10948
|
-
if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.length > 512 || /[\s\u0000-\u001f\u007f]/.test(value)) throw new Error(`${fieldName} must be a compact opaque identifier`);
|
|
10949
|
-
return value;
|
|
10950
|
-
}
|
|
10951
|
-
function parseTenantIdRequired(value) {
|
|
10952
|
-
const parsed = parseTenantId(value);
|
|
10953
|
-
if (!parsed.ok) throw new Error(`linked-device authorization tenantId: ${parsed.error.message}`);
|
|
10954
|
-
return parsed.value;
|
|
10955
|
-
}
|
|
10956
|
-
function parsePrincipalIdRequired(value) {
|
|
10957
|
-
const parsed = parsePrincipalId(value);
|
|
10958
|
-
if (!parsed.ok) throw new Error(`linked-device authorization principalId: ${parsed.error.message}`);
|
|
10959
|
-
return parsed.value;
|
|
10960
|
-
}
|
|
10961
|
-
function parseWalletIdRequired(value) {
|
|
10962
|
-
const parsed = parseWalletId(value);
|
|
10963
|
-
if (!parsed.ok) throw new Error(`linked-device authorization walletId: ${parsed.error.message}`);
|
|
10964
|
-
return parsed.value;
|
|
10965
|
-
}
|
|
10966
|
-
function parseWalletSessionIdRequired(value) {
|
|
10967
|
-
const parsed = parseWalletSessionId(value);
|
|
10968
|
-
if (!parsed.ok) throw new Error(`linked-device authorization walletSessionId: ${parsed.error.message}`);
|
|
10969
|
-
return parsed.value;
|
|
10970
|
-
}
|
|
10971
|
-
function parseMpcWalletSigningQuotaIdRequired(value) {
|
|
10972
|
-
const parsed = parseMpcWalletSigningQuotaId(value);
|
|
10973
|
-
if (!parsed.ok) throw new Error(`linked-device authorization quotaId: ${parsed.error.message}`);
|
|
10974
|
-
return parsed.value;
|
|
10975
|
-
}
|
|
10976
|
-
function requirePositiveCount(value, label) {
|
|
10977
|
-
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`);
|
|
10978
|
-
}
|
|
10979
|
-
function requireNonnegativeInteger(value, label) {
|
|
10980
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a nonnegative safe integer`);
|
|
10981
|
-
return value;
|
|
10982
|
-
}
|
|
10983
|
-
function requirePositiveTimestamp(value, label) {
|
|
10984
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`);
|
|
10985
|
-
return value;
|
|
10986
|
-
}
|
|
10987
|
-
function requirePositiveTime(value, label) {
|
|
10988
|
-
if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive timestamp`);
|
|
10989
|
-
}
|
|
10990
|
-
function requireOrderedTimes(createdAtMs, expiresAtMs, label) {
|
|
10991
|
-
requirePositiveTime(createdAtMs, `${label} creation`);
|
|
10992
|
-
requirePositiveTime(expiresAtMs, `${label} expiry`);
|
|
10993
|
-
if (expiresAtMs <= createdAtMs) throw new Error(`${label} expiry must follow creation`);
|
|
10994
|
-
}
|
|
10995
|
-
//#endregion
|
|
10996
11873
|
//#region src/router/domains/signingOperations/walletExecutionAdmission.ts
|
|
10997
11874
|
function validateWalletSessionAuthorizationV2Context(input) {
|
|
10998
11875
|
if (!isPositiveSafeInteger(input.nowMs)) return "invalid_time";
|
|
@@ -11785,32 +12662,6 @@ function parseRegistrationPreparedSessionSeal(raw, service) {
|
|
|
11785
12662
|
};
|
|
11786
12663
|
}
|
|
11787
12664
|
//#endregion
|
|
11788
|
-
//#region src/router/framework/routeResponses.ts
|
|
11789
|
-
function routeJson(status, body, options) {
|
|
11790
|
-
return {
|
|
11791
|
-
status,
|
|
11792
|
-
body,
|
|
11793
|
-
...options?.headers ? { headers: options.headers } : {},
|
|
11794
|
-
...options?.usage ? { usage: options.usage } : {}
|
|
11795
|
-
};
|
|
11796
|
-
}
|
|
11797
|
-
function routeError(status, code, message) {
|
|
11798
|
-
return routeJson(status, {
|
|
11799
|
-
ok: false,
|
|
11800
|
-
code,
|
|
11801
|
-
message
|
|
11802
|
-
});
|
|
11803
|
-
}
|
|
11804
|
-
function toFetchRouteResponse(response) {
|
|
11805
|
-
return new Response(JSON.stringify(response.body), {
|
|
11806
|
-
status: response.status,
|
|
11807
|
-
headers: {
|
|
11808
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
11809
|
-
...response.headers || {}
|
|
11810
|
-
}
|
|
11811
|
-
});
|
|
11812
|
-
}
|
|
11813
|
-
//#endregion
|
|
11814
12665
|
//#region ../shared-ts/src/utils/registrationIds.ts
|
|
11815
12666
|
function walletIdFromString(value) {
|
|
11816
12667
|
const parsed = parseWalletId(value);
|
|
@@ -13246,12 +14097,19 @@ function parseWalletRegistrationSetupRequest(body) {
|
|
|
13246
14097
|
code: "invalid_body",
|
|
13247
14098
|
message: "authMethod is required"
|
|
13248
14099
|
};
|
|
14100
|
+
const completion = body.completion ?? "wallet_session";
|
|
14101
|
+
if (completion !== "wallet_session" && completion !== "owner_operations") return {
|
|
14102
|
+
ok: false,
|
|
14103
|
+
code: "invalid_body",
|
|
14104
|
+
message: "Invalid registration completion"
|
|
14105
|
+
};
|
|
13249
14106
|
const wallet = parseWalletRegistrationSetupWallet(body.wallet);
|
|
13250
14107
|
if (!wallet.ok) return wallet;
|
|
13251
14108
|
return {
|
|
13252
14109
|
ok: true,
|
|
13253
14110
|
value: {
|
|
13254
14111
|
registrationOperationId,
|
|
14112
|
+
completion,
|
|
13255
14113
|
...wallet.value ? { wallet: wallet.value } : {},
|
|
13256
14114
|
signerSelection: signerSelection.value.selection,
|
|
13257
14115
|
authMethod
|
|
@@ -13321,7 +14179,7 @@ function parseInventoryKeyTargets(raw) {
|
|
|
13321
14179
|
value: targets
|
|
13322
14180
|
};
|
|
13323
14181
|
}
|
|
13324
|
-
function parseWebAuthnAuthenticationCredential$
|
|
14182
|
+
function parseWebAuthnAuthenticationCredential$1(raw) {
|
|
13325
14183
|
if (!isPlainObject(raw)) return {
|
|
13326
14184
|
ok: false,
|
|
13327
14185
|
code: "invalid_body",
|
|
@@ -13400,7 +14258,7 @@ async function parseWalletEcdsaInventoryBody(body, walletId) {
|
|
|
13400
14258
|
message: "auth is required"
|
|
13401
14259
|
};
|
|
13402
14260
|
if (auth.kind === "webauthn_assertion") {
|
|
13403
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
14261
|
+
const credential = parseWebAuthnAuthenticationCredential$1(auth.credential);
|
|
13404
14262
|
if (!credential.ok) return credential;
|
|
13405
14263
|
const expectedChallengeDigestB64u = typeof auth.expectedChallengeDigestB64u === "string" ? auth.expectedChallengeDigestB64u.trim() : "";
|
|
13406
14264
|
const serverNonceB64u = typeof auth.serverNonceB64u === "string" ? auth.serverNonceB64u.trim() : "";
|
|
@@ -13634,7 +14492,7 @@ async function parseWalletAddSignerStartBody(body, walletId) {
|
|
|
13634
14492
|
code: "invalid_body",
|
|
13635
14493
|
message: authRpId.error.message
|
|
13636
14494
|
};
|
|
13637
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
14495
|
+
const credential = parseWebAuthnAuthenticationCredential$1(auth.credential);
|
|
13638
14496
|
if (!credential.ok) return credential;
|
|
13639
14497
|
const expectedChallengeDigestB64u = typeof auth.expectedChallengeDigestB64u === "string" ? auth.expectedChallengeDigestB64u.trim() : "";
|
|
13640
14498
|
if (expectedChallengeDigestB64u !== expectedDigest) return {
|
|
@@ -13745,7 +14603,7 @@ async function parseWalletAddAuthMethodStartBody(body, walletId) {
|
|
|
13745
14603
|
code: "invalid_body",
|
|
13746
14604
|
message: authRpId.error.message
|
|
13747
14605
|
};
|
|
13748
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
14606
|
+
const credential = parseWebAuthnAuthenticationCredential$1(auth.credential);
|
|
13749
14607
|
if (!credential.ok) return credential;
|
|
13750
14608
|
const expectedChallengeDigestB64u = typeof auth.expectedChallengeDigestB64u === "string" ? auth.expectedChallengeDigestB64u.trim() : "";
|
|
13751
14609
|
if (expectedChallengeDigestB64u !== expectedDigest) return {
|
|
@@ -14349,7 +15207,7 @@ function parseWalletRevokeAuthMethodSourceProof(raw) {
|
|
|
14349
15207
|
code: "invalid_body",
|
|
14350
15208
|
message: rpId.error.message
|
|
14351
15209
|
};
|
|
14352
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
15210
|
+
const credential = parseWebAuthnAuthenticationCredential$1(raw.credential);
|
|
14353
15211
|
if (!credential.ok) return credential;
|
|
14354
15212
|
const expectedChallengeDigestB64u = typeof raw.expectedChallengeDigestB64u === "string" ? raw.expectedChallengeDigestB64u.trim() : "";
|
|
14355
15213
|
if (!expectedChallengeDigestB64u) return {
|
|
@@ -14831,6 +15689,7 @@ async function handleRouterApiWalletRegistrationNearProvisioning(input) {
|
|
|
14831
15689
|
if (sessionSeal.value) {
|
|
14832
15690
|
if (!isPasskeyWalletRegistrationNearProvisioningSuccessV2(result)) return routeError(400, "invalid_body", "Registration session sealing requires a passkey authority");
|
|
14833
15691
|
const sealStartedAt = Date.now();
|
|
15692
|
+
if (result.registrationEstablishedSession.kind === "owner_operations") return routeError(400, "invalid_body", "Owner registration has no session seal");
|
|
14834
15693
|
const sealed = await sessionSeal.value.finish(result.registrationEstablishedSession.session);
|
|
14835
15694
|
serverTiming.push(["near_finalize_session_seal", Math.max(0, Date.now() - sealStartedAt)]);
|
|
14836
15695
|
if (!sealed.ok) return routeJson(503, {
|
|
@@ -15230,7 +16089,7 @@ function resolveWalletRegistrationRoute(ctx) {
|
|
|
15230
16089
|
async function handleWalletRegistration(ctx) {
|
|
15231
16090
|
const route = resolveWalletRegistrationRoute(ctx);
|
|
15232
16091
|
if (!route) return null;
|
|
15233
|
-
if (ctx.request.headers.get("X-Seams-Wallet-Protocol") !== "
|
|
16092
|
+
if (ctx.request.headers.get("X-Seams-Wallet-Protocol") !== "5") return jsonFailure(409, "wallet_protocol_mismatch", "Your wallet application needs an update. Reload this page and try again. If the problem continues, ask the application developer to upgrade the Wallet SDK.");
|
|
15234
16093
|
const common = {
|
|
15235
16094
|
body: await readJson(ctx.request),
|
|
15236
16095
|
headers: Object.fromEntries(ctx.request.headers.entries()),
|
|
@@ -15597,215 +16456,19 @@ async function handleVoiceIdVerifyRequest(request, deps) {
|
|
|
15597
16456
|
}, { status: 503 });
|
|
15598
16457
|
}
|
|
15599
16458
|
}
|
|
16459
|
+
Object.freeze(Array.from("0123456789ABCDEFGHJKMNPQRSTVWXYZ").reduce((acc, char, index) => {
|
|
16460
|
+
acc[char] = index;
|
|
16461
|
+
return acc;
|
|
16462
|
+
}, {}));
|
|
15600
16463
|
//#endregion
|
|
15601
|
-
//#region ../shared-ts/src/
|
|
15602
|
-
|
|
15603
|
-
* Best-effort error message extractor without relying on `any`.
|
|
15604
|
-
* Always returns a string (may be empty when nothing usable can be derived).
|
|
15605
|
-
*/
|
|
15606
|
-
function errorMessage$3(err) {
|
|
15607
|
-
try {
|
|
15608
|
-
if (typeof err === "string") return err;
|
|
15609
|
-
if (err && typeof err.message === "string") return err.message;
|
|
15610
|
-
return String(err ?? "");
|
|
15611
|
-
} catch {
|
|
15612
|
-
return "";
|
|
15613
|
-
}
|
|
15614
|
-
}
|
|
15615
|
-
function b64uField(prefix) {
|
|
15616
|
-
return `${prefix}B64u`;
|
|
15617
|
-
}
|
|
15618
|
-
b64uField("seed"), b64uField("canonicalSeed"), b64uField("xClientBase"), b64uField("clientOutputMask"), b64uField("clientRecoverableSecret"), b64uField("prfFirst"), b64uField("prfSecond"), b64uField("signingShare32"), b64uField("clientNonceHandle");
|
|
15619
|
-
//#endregion
|
|
15620
|
-
//#region src/router/auth/webAuthnCredentialCodecs.ts
|
|
15621
|
-
function containsWebAuthnExtensionOutput(value) {
|
|
15622
|
-
if (value == null) return false;
|
|
15623
|
-
if (!isPlainObject(value)) return true;
|
|
15624
|
-
for (const nested of Object.values(value)) if (containsWebAuthnExtensionOutput(nested)) return true;
|
|
15625
|
-
return false;
|
|
15626
|
-
}
|
|
15627
|
-
function unredactedWebAuthnExtensionField(input) {
|
|
15628
|
-
if (!isPlainObject(input)) return null;
|
|
15629
|
-
if (containsWebAuthnExtensionOutput(input.clientExtensionResults)) return "clientExtensionResults";
|
|
15630
|
-
const response = input.response;
|
|
15631
|
-
if (!isPlainObject(response)) return null;
|
|
15632
|
-
if (containsWebAuthnExtensionOutput(response.clientExtensionResults)) return "response.clientExtensionResults";
|
|
15633
|
-
return null;
|
|
15634
|
-
}
|
|
15635
|
-
function decodeWebAuthnBase64UrlOrBase64(input, fieldName) {
|
|
15636
|
-
try {
|
|
15637
|
-
return base64UrlDecode(input);
|
|
15638
|
-
} catch {
|
|
15639
|
-
try {
|
|
15640
|
-
return base64Decode(input);
|
|
15641
|
-
} catch (error) {
|
|
15642
|
-
throw new Error(`Invalid ${fieldName}: expected base64url/base64 string (${errorMessage$3(error) || "decode failed"})`);
|
|
15643
|
-
}
|
|
15644
|
-
}
|
|
15645
|
-
}
|
|
15646
|
-
function parseWebAuthnClientDataJsonBase64url(clientDataJSONB64u) {
|
|
15647
|
-
const bytes = decodeWebAuthnBase64UrlOrBase64(clientDataJSONB64u, "webauthn_authentication.response.clientDataJSON");
|
|
15648
|
-
const json = new TextDecoder().decode(bytes);
|
|
15649
|
-
let parsed;
|
|
15650
|
-
try {
|
|
15651
|
-
parsed = JSON.parse(json);
|
|
15652
|
-
} catch {
|
|
15653
|
-
throw new Error("Invalid clientDataJSON: expected object");
|
|
15654
|
-
}
|
|
15655
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid clientDataJSON: expected object");
|
|
15656
|
-
const challenge = normalizeOptionalTrimmedString(Reflect.get(parsed, "challenge"));
|
|
15657
|
-
const origin = normalizeOptionalTrimmedString(Reflect.get(parsed, "origin"));
|
|
15658
|
-
const type = normalizeOptionalTrimmedString(Reflect.get(parsed, "type"));
|
|
15659
|
-
if (!challenge) throw new Error("Invalid clientDataJSON.challenge");
|
|
15660
|
-
if (!origin) throw new Error("Invalid clientDataJSON.origin");
|
|
15661
|
-
if (!type) throw new Error("Invalid clientDataJSON.type");
|
|
15662
|
-
return {
|
|
15663
|
-
challenge,
|
|
15664
|
-
origin,
|
|
15665
|
-
type
|
|
15666
|
-
};
|
|
15667
|
-
}
|
|
15668
|
-
function webAuthnOriginHostnameOrEmpty(origin) {
|
|
15669
|
-
try {
|
|
15670
|
-
return new URL(origin).hostname.toLowerCase();
|
|
15671
|
-
} catch {
|
|
15672
|
-
return "";
|
|
15673
|
-
}
|
|
15674
|
-
}
|
|
15675
|
-
function webAuthnCredentialIdB64uFromCredential(input) {
|
|
15676
|
-
if (input === null || typeof input !== "object" || Array.isArray(input)) return failure$6("invalid_body", "Missing webauthn_authentication.id/rawId");
|
|
15677
|
-
const rawId = normalizeOptionalTrimmedString(Reflect.get(input, "rawId"));
|
|
15678
|
-
const id = normalizeOptionalTrimmedString(Reflect.get(input, "id"));
|
|
15679
|
-
const selected = rawId || id;
|
|
15680
|
-
if (!selected) return failure$6("invalid_body", "Missing webauthn_authentication.id/rawId");
|
|
16464
|
+
//#region ../shared-ts/src/wallet-recovery/recoveryCodeLocator.ts
|
|
16465
|
+
function parseRecoveryCodeLocatorV1(value, label = "recoveryCodeLocator") {
|
|
15681
16466
|
try {
|
|
15682
|
-
return
|
|
15683
|
-
ok: true,
|
|
15684
|
-
credentialIdB64u: base64UrlEncode(decodeWebAuthnBase64UrlOrBase64(selected, "webauthn_authentication.rawId"))
|
|
15685
|
-
};
|
|
16467
|
+
return parseDigestB64u(value);
|
|
15686
16468
|
} catch (error) {
|
|
15687
|
-
|
|
15688
|
-
}
|
|
15689
|
-
}
|
|
15690
|
-
function parseWebAuthnAuthenticationCredential$1(input) {
|
|
15691
|
-
if (input === null || typeof input !== "object" || Array.isArray(input)) return null;
|
|
15692
|
-
const responseValue = Reflect.get(input, "response");
|
|
15693
|
-
if (responseValue === null || typeof responseValue !== "object" || Array.isArray(responseValue)) return null;
|
|
15694
|
-
const id = normalizeOptionalTrimmedString(Reflect.get(input, "id"));
|
|
15695
|
-
const rawId = normalizeOptionalTrimmedString(Reflect.get(input, "rawId"));
|
|
15696
|
-
const type = normalizeOptionalTrimmedString(Reflect.get(input, "type"));
|
|
15697
|
-
const clientDataJSON = normalizeOptionalTrimmedString(Reflect.get(responseValue, "clientDataJSON"));
|
|
15698
|
-
const authenticatorData = normalizeOptionalTrimmedString(Reflect.get(responseValue, "authenticatorData"));
|
|
15699
|
-
const signature = normalizeOptionalTrimmedString(Reflect.get(responseValue, "signature"));
|
|
15700
|
-
const rawUserHandle = Reflect.get(responseValue, "userHandle");
|
|
15701
|
-
const userHandle = rawUserHandle === null ? null : normalizeOptionalTrimmedString(rawUserHandle) || null;
|
|
15702
|
-
const rawAuthenticatorAttachment = Reflect.get(input, "authenticatorAttachment");
|
|
15703
|
-
const authenticatorAttachment = rawAuthenticatorAttachment === null ? null : normalizeOptionalTrimmedString(rawAuthenticatorAttachment) || null;
|
|
15704
|
-
if (!id || !rawId || type !== "public-key") return null;
|
|
15705
|
-
if (!clientDataJSON || !authenticatorData || !signature) return null;
|
|
15706
|
-
return {
|
|
15707
|
-
id,
|
|
15708
|
-
rawId,
|
|
15709
|
-
type,
|
|
15710
|
-
authenticatorAttachment,
|
|
15711
|
-
response: {
|
|
15712
|
-
clientDataJSON,
|
|
15713
|
-
authenticatorData,
|
|
15714
|
-
signature,
|
|
15715
|
-
userHandle
|
|
15716
|
-
},
|
|
15717
|
-
clientExtensionResults: Reflect.get(input, "clientExtensionResults") ?? null
|
|
15718
|
-
};
|
|
15719
|
-
}
|
|
15720
|
-
//#endregion
|
|
15721
|
-
//#region src/router/transport/fetch/routes/walletCustodyRequestBoundary.ts
|
|
15722
|
-
function requireExactObjectFields(value, fields, label) {
|
|
15723
|
-
const keys = Object.keys(value).sort();
|
|
15724
|
-
const expected = [...fields].sort();
|
|
15725
|
-
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new Error(`${label} contains unsupported fields`);
|
|
15726
|
-
}
|
|
15727
|
-
function parseCustodyOwnerProof(value) {
|
|
15728
|
-
if (!isPlainObject(value)) throw new Error("factorProof is required");
|
|
15729
|
-
const kind = normalizeOptionalTrimmedString(value.kind);
|
|
15730
|
-
if (kind === "passkey") {
|
|
15731
|
-
requireExactObjectFields(value, [
|
|
15732
|
-
"kind",
|
|
15733
|
-
"walletId",
|
|
15734
|
-
"rpId",
|
|
15735
|
-
"credentialIdB64u",
|
|
15736
|
-
"challenge_digest",
|
|
15737
|
-
"webauthn_authentication"
|
|
15738
|
-
], "factorProof");
|
|
15739
|
-
const extensionField = unredactedWebAuthnExtensionField(value.webauthn_authentication);
|
|
15740
|
-
if (extensionField) throw new Error(`factorProof.webauthn_authentication.${extensionField} must be redacted`);
|
|
15741
|
-
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$1(value.webauthn_authentication);
|
|
15742
|
-
if (!webauthnAuthentication) throw new Error("factorProof.webauthn_authentication is invalid");
|
|
15743
|
-
return {
|
|
15744
|
-
kind,
|
|
15745
|
-
walletId: requireTrimmedString(value.walletId, "factorProof.walletId"),
|
|
15746
|
-
rpId: requireTrimmedString(value.rpId, "factorProof.rpId"),
|
|
15747
|
-
credentialIdB64u: requireTrimmedString(value.credentialIdB64u, "factorProof.credentialIdB64u"),
|
|
15748
|
-
challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest"),
|
|
15749
|
-
webauthn_authentication: webauthnAuthentication
|
|
15750
|
-
};
|
|
15751
|
-
}
|
|
15752
|
-
if (kind === "email_otp") {
|
|
15753
|
-
requireExactObjectFields(value, [
|
|
15754
|
-
"kind",
|
|
15755
|
-
"provider_subject_id",
|
|
15756
|
-
"challenge_id",
|
|
15757
|
-
"otp_code",
|
|
15758
|
-
"challenge_digest"
|
|
15759
|
-
], "factorProof");
|
|
15760
|
-
return {
|
|
15761
|
-
kind,
|
|
15762
|
-
provider_subject_id: requireTrimmedString(value.provider_subject_id, "factorProof.provider_subject_id"),
|
|
15763
|
-
challenge_id: requireTrimmedString(value.challenge_id, "factorProof.challenge_id"),
|
|
15764
|
-
otp_code: requireTrimmedString(value.otp_code, "factorProof.otp_code"),
|
|
15765
|
-
challenge_digest: requireTrimmedString(value.challenge_digest, "factorProof.challenge_digest")
|
|
15766
|
-
};
|
|
15767
|
-
}
|
|
15768
|
-
throw new Error("factorProof.kind must be passkey or email_otp");
|
|
15769
|
-
}
|
|
15770
|
-
function parseRecoveryBackupAcknowledgementRequest(body) {
|
|
15771
|
-
if (!isPlainObject(body)) throw new Error("acknowledgement body is required");
|
|
15772
|
-
requireExactObjectFields(body, [
|
|
15773
|
-
"expectedIssuedAtMs",
|
|
15774
|
-
"factorProof",
|
|
15775
|
-
"requestId",
|
|
15776
|
-
"walletId"
|
|
15777
|
-
], "recovery backup acknowledgement");
|
|
15778
|
-
return {
|
|
15779
|
-
walletId: requireTrimmedString(body.walletId, "walletId"),
|
|
15780
|
-
requestId: requireTrimmedString(body.requestId, "requestId"),
|
|
15781
|
-
expectedIssuedAtMs: parseUnixMs$1(body.expectedIssuedAtMs, "expectedIssuedAtMs"),
|
|
15782
|
-
factorProof: body.factorProof
|
|
15783
|
-
};
|
|
15784
|
-
}
|
|
15785
|
-
function parseRecoveryStatusRequest(body) {
|
|
15786
|
-
if (!isPlainObject(body)) throw new Error("status body is required");
|
|
15787
|
-
requireExactObjectFields(body, ["factorProof", "requestId"], "recovery status");
|
|
15788
|
-
return {
|
|
15789
|
-
requestId: requireTrimmedString(body.requestId, "requestId"),
|
|
15790
|
-
factorProof: body.factorProof
|
|
15791
|
-
};
|
|
15792
|
-
}
|
|
15793
|
-
//#endregion
|
|
15794
|
-
//#region src/core/authService/webauthnOidcHelpers.ts
|
|
15795
|
-
function originHostnameOrEmpty(origin) {
|
|
15796
|
-
try {
|
|
15797
|
-
return new URL(origin).hostname.toLowerCase();
|
|
15798
|
-
} catch {
|
|
15799
|
-
return "";
|
|
16469
|
+
throw new Error(`${label} ${error instanceof Error ? error.message : "must be a canonical digest"}`);
|
|
15800
16470
|
}
|
|
15801
16471
|
}
|
|
15802
|
-
function isHostWithinRpId(host, rpId) {
|
|
15803
|
-
const h = (host || "").toLowerCase();
|
|
15804
|
-
const r = String(rpId).toLowerCase();
|
|
15805
|
-
if (!h || !r) return false;
|
|
15806
|
-
if ((process.env.NO_CADDY === "1" || process.env.VITE_NO_CADDY === "1") && (h === "localhost" || h === "127.0.0.1") && r.endsWith(".localhost")) return true;
|
|
15807
|
-
return h === r || h.endsWith(`.${r}`);
|
|
15808
|
-
}
|
|
15809
16472
|
//#endregion
|
|
15810
16473
|
//#region ../shared-ts/src/wallet-recovery/recoveryCodeReservation.ts
|
|
15811
16474
|
function parseRecoveryCodeReservationId(value) {
|
|
@@ -15813,182 +16476,6 @@ function parseRecoveryCodeReservationId(value) {
|
|
|
15813
16476
|
return value;
|
|
15814
16477
|
}
|
|
15815
16478
|
//#endregion
|
|
15816
|
-
//#region ../shared-ts/src/authorization/walletCustodyOperation.ts
|
|
15817
|
-
/** Public challenge digest for a single wallet-administration factor proof. */
|
|
15818
|
-
async function computeWalletCustodyAdminChallengeDigest(input) {
|
|
15819
|
-
return sha256Utf8DigestB64u(`seams:wallet-custody:challenge:v1|${alphabetizeStringify({
|
|
15820
|
-
walletId: input.walletId,
|
|
15821
|
-
operation: input.operation,
|
|
15822
|
-
payload: input.payload,
|
|
15823
|
-
requestOrigin: input.requestOrigin
|
|
15824
|
-
})}`);
|
|
15825
|
-
}
|
|
15826
|
-
//#endregion
|
|
15827
|
-
//#region ../shared-ts/src/wallet-recovery/walletRecoveryTarget.ts
|
|
15828
|
-
function parseWalletRecoveryTargetV1(raw) {
|
|
15829
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("wallet recovery target must be an object");
|
|
15830
|
-
const record = raw;
|
|
15831
|
-
const fields = Object.keys(record);
|
|
15832
|
-
switch (record.kind) {
|
|
15833
|
-
case "passkey": {
|
|
15834
|
-
if (fields.length !== 2 || !fields.includes("rpId")) throw new Error("Passkey recovery target fields are invalid");
|
|
15835
|
-
const rpId = parseWebAuthnRpId(record.rpId);
|
|
15836
|
-
if (!rpId.ok) throw new Error("Passkey recovery target RP ID is invalid");
|
|
15837
|
-
return {
|
|
15838
|
-
kind: "passkey",
|
|
15839
|
-
rpId: rpId.value
|
|
15840
|
-
};
|
|
15841
|
-
}
|
|
15842
|
-
case "google_email_otp":
|
|
15843
|
-
if (fields.length !== 2 || !fields.includes("googleProvider") || record.googleProvider !== "google") throw new Error("Google Email OTP recovery target fields are invalid");
|
|
15844
|
-
return {
|
|
15845
|
-
kind: "google_email_otp",
|
|
15846
|
-
googleProvider: "google"
|
|
15847
|
-
};
|
|
15848
|
-
default: throw new Error("wallet recovery target kind is unsupported");
|
|
15849
|
-
}
|
|
15850
|
-
}
|
|
15851
|
-
//#endregion
|
|
15852
|
-
//#region src/router/domains/emailOtp/emailOtpSessionRouteHelpers.ts
|
|
15853
|
-
function emailOtpStatusCode(code) {
|
|
15854
|
-
if (code === "internal") return 500;
|
|
15855
|
-
if (code === "not_configured") return 503;
|
|
15856
|
-
if (code === "not_found") return 404;
|
|
15857
|
-
if (code === "rate_limited") return 429;
|
|
15858
|
-
if (code === "stronger_auth_required") return 403;
|
|
15859
|
-
if (code === "reenrollment_required" || code === "registration_attempt_missing" || code === "registration_attempt_expired") return 409;
|
|
15860
|
-
if (code === "challenge_id_mismatch" || code === "challenge_purpose_mismatch" || code === "challenge_subject_mismatch" || code === "challenge_email_mismatch" || code === "challenge_wallet_mismatch" || code === "challenge_session_mismatch" || code === "challenge_org_mismatch" || code === "challenge_channel_mismatch" || code === "registration_reroll_disallowed" || code === "challenge_expired_or_invalid" || code === "invalid_otp") return 401;
|
|
15861
|
-
return 400;
|
|
15862
|
-
}
|
|
15863
|
-
function emailOtpFailureAuditPayload(input) {
|
|
15864
|
-
return {
|
|
15865
|
-
source: input.source,
|
|
15866
|
-
code: input.code,
|
|
15867
|
-
message: input.message,
|
|
15868
|
-
...input.challengeId ? { challengeId: input.challengeId } : {},
|
|
15869
|
-
...input.otpChannel ? { otpChannel: input.otpChannel } : {},
|
|
15870
|
-
...input.operation ? { operation: input.operation } : {},
|
|
15871
|
-
...typeof input.lockedUntilMs === "number" ? { lockedUntilMs: input.lockedUntilMs } : {}
|
|
15872
|
-
};
|
|
15873
|
-
}
|
|
15874
|
-
function shouldEmitEmailOtpLockedWebhook(code) {
|
|
15875
|
-
return code === "otp_locked_out" || code === "otp_attempts_exhausted";
|
|
15876
|
-
}
|
|
15877
|
-
function emailOtpFailureWebhookEventDescriptors(input) {
|
|
15878
|
-
const payload = emailOtpFailureAuditPayload(input);
|
|
15879
|
-
const event = {
|
|
15880
|
-
eventType: "wallet.email_otp.failed",
|
|
15881
|
-
...input.challengeId ? { eventId: input.challengeId } : {},
|
|
15882
|
-
payload
|
|
15883
|
-
};
|
|
15884
|
-
if (!shouldEmitEmailOtpLockedWebhook(input.code)) return [event];
|
|
15885
|
-
return [event, {
|
|
15886
|
-
eventType: "wallet.email_otp.locked",
|
|
15887
|
-
...input.challengeId ? { eventId: input.challengeId } : {},
|
|
15888
|
-
payload
|
|
15889
|
-
}];
|
|
15890
|
-
}
|
|
15891
|
-
function emailOtpLoggedInWebhookEventDescriptor(input) {
|
|
15892
|
-
return {
|
|
15893
|
-
eventType: "wallet.email_otp.logged_in",
|
|
15894
|
-
eventId: input.challengeId,
|
|
15895
|
-
payload: {
|
|
15896
|
-
otpChannel: input.otpChannel,
|
|
15897
|
-
unlockBackend: input.unlockBackend,
|
|
15898
|
-
challengeId: input.challengeId
|
|
15899
|
-
}
|
|
15900
|
-
};
|
|
15901
|
-
}
|
|
15902
|
-
async function hashEmailOtpOperationBinding(input) {
|
|
15903
|
-
return base64UrlEncode(await sha256BytesUtf8(alphabetizeStringify({
|
|
15904
|
-
walletId: input.walletId,
|
|
15905
|
-
providerUserId: input.providerUserId,
|
|
15906
|
-
orgId: input.orgId,
|
|
15907
|
-
operation: input.operation,
|
|
15908
|
-
requestOrigin: input.requestOrigin || "",
|
|
15909
|
-
audience: input.audience || "",
|
|
15910
|
-
...input.authorityRef !== void 0 ? { authorityRef: input.authorityRef } : {},
|
|
15911
|
-
...input.operationFingerprintDigest ? { operationFingerprintDigest: input.operationFingerprintDigest } : {}
|
|
15912
|
-
})));
|
|
15913
|
-
}
|
|
15914
|
-
//#endregion
|
|
15915
|
-
//#region src/authorization/factorEvidence.ts
|
|
15916
|
-
function buildVerifiedWalletOperationPasskeyFactorResult(fields) {
|
|
15917
|
-
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
15918
|
-
return {
|
|
15919
|
-
kind: "verified_wallet_operation_passkey_factor",
|
|
15920
|
-
...fields
|
|
15921
|
-
};
|
|
15922
|
-
}
|
|
15923
|
-
function buildVerifiedWalletOperationEmailOtpFactorResult(fields) {
|
|
15924
|
-
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
15925
|
-
return {
|
|
15926
|
-
kind: "verified_wallet_operation_email_otp_factor",
|
|
15927
|
-
...fields
|
|
15928
|
-
};
|
|
15929
|
-
}
|
|
15930
|
-
function buildVerifiedWalletSessionPasskeyFactorResult(fields) {
|
|
15931
|
-
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
15932
|
-
return {
|
|
15933
|
-
kind: "verified_wallet_session_passkey_factor",
|
|
15934
|
-
tenantId: fields.tenantId,
|
|
15935
|
-
principalId: fields.principalId,
|
|
15936
|
-
walletId: fields.walletId,
|
|
15937
|
-
authorityRef: fields.authorityRef,
|
|
15938
|
-
requestOrigin: fields.requestOrigin,
|
|
15939
|
-
audience: fields.audience,
|
|
15940
|
-
factorId: fields.factorId,
|
|
15941
|
-
credentialIdB64u: fields.credentialIdB64u,
|
|
15942
|
-
assertionDigest: fields.assertionDigest,
|
|
15943
|
-
verifiedAtMs: fields.verifiedAtMs,
|
|
15944
|
-
expiresAtMs: fields.expiresAtMs
|
|
15945
|
-
};
|
|
15946
|
-
}
|
|
15947
|
-
function buildVerifiedWalletSessionEmailOtpFactorResult(fields) {
|
|
15948
|
-
requireVerificationWindow(fields.verifiedAtMs, fields.expiresAtMs);
|
|
15949
|
-
return {
|
|
15950
|
-
kind: "verified_wallet_session_email_otp_factor",
|
|
15951
|
-
tenantId: fields.tenantId,
|
|
15952
|
-
principalId: fields.principalId,
|
|
15953
|
-
walletId: fields.walletId,
|
|
15954
|
-
authorityRef: fields.authorityRef,
|
|
15955
|
-
requestOrigin: fields.requestOrigin,
|
|
15956
|
-
audience: fields.audience,
|
|
15957
|
-
factorId: fields.factorId,
|
|
15958
|
-
challengeId: fields.challengeId,
|
|
15959
|
-
verificationReceiptDigest: fields.verificationReceiptDigest,
|
|
15960
|
-
verifiedAtMs: fields.verifiedAtMs,
|
|
15961
|
-
expiresAtMs: fields.expiresAtMs
|
|
15962
|
-
};
|
|
15963
|
-
}
|
|
15964
|
-
function requireVerificationWindow(verifiedAtMs, expiresAtMs) {
|
|
15965
|
-
if (!Number.isSafeInteger(verifiedAtMs) || !Number.isSafeInteger(expiresAtMs) || verifiedAtMs <= 0 || expiresAtMs <= verifiedAtMs) throw new Error("verified factor expiry must follow verification");
|
|
15966
|
-
}
|
|
15967
|
-
//#endregion
|
|
15968
|
-
//#region ../shared-ts/src/utils/emailOtpDomain.ts
|
|
15969
|
-
const EMAIL_OTP_CHANNEL = "email_otp";
|
|
15970
|
-
/** Domain label that opens the AAD binding a released Email OTP factor to its challenge. */
|
|
15971
|
-
const EMAIL_OTP_FACTOR_RELEASE_AAD_DOMAIN_V1 = "seams/email-otp/factor-release/v1";
|
|
15972
|
-
const WALLET_UNLOCK_BACKENDS = ["passkey", EMAIL_OTP_CHANNEL];
|
|
15973
|
-
const WALLET_EMAIL_OTP_LOGIN_OPERATIONS = [
|
|
15974
|
-
"wallet_unlock",
|
|
15975
|
-
"transaction_sign",
|
|
15976
|
-
"export_key"
|
|
15977
|
-
];
|
|
15978
|
-
const WALLET_EMAIL_OTP_EXPORT_OPERATION = "export_key";
|
|
15979
|
-
const WALLET_EMAIL_OTP_UNLOCK_OPERATION = "wallet_unlock";
|
|
15980
|
-
const WALLET_EMAIL_OTP_TRANSACTION_SIGN_OPERATION = "transaction_sign";
|
|
15981
|
-
function isWalletUnlockBackend(value) {
|
|
15982
|
-
return WALLET_UNLOCK_BACKENDS.includes(value);
|
|
15983
|
-
}
|
|
15984
|
-
function isWalletEmailOtpLoginOperation(value) {
|
|
15985
|
-
return WALLET_EMAIL_OTP_LOGIN_OPERATIONS.includes(value);
|
|
15986
|
-
}
|
|
15987
|
-
Object.freeze(Array.from("0123456789ABCDEFGHJKMNPQRSTVWXYZ").reduce((acc, char, index) => {
|
|
15988
|
-
acc[char] = index;
|
|
15989
|
-
return acc;
|
|
15990
|
-
}, {}));
|
|
15991
|
-
//#endregion
|
|
15992
16479
|
//#region ../shared-ts/src/wallet-recovery/recoveryKeyId.ts
|
|
15993
16480
|
/** The prefix plus unpadded base64url over one SHA-256 digest. */
|
|
15994
16481
|
const DERIVED_WALLET_RECOVERY_KEY_ID_PATTERN = /^wallet-rkid-v1-[A-Za-z0-9_-]{43}$/;
|
|
@@ -15997,6 +16484,40 @@ function parseDerivedWalletRecoveryKeyId(value, label = "recoveryKeyId") {
|
|
|
15997
16484
|
return value;
|
|
15998
16485
|
}
|
|
15999
16486
|
//#endregion
|
|
16487
|
+
//#region ../shared-ts/src/wallet-recovery/walletRecoveryEcdsaPossession.ts
|
|
16488
|
+
const POSSESSION_PROOF_SCHEME_V1 = "secp256k1_bip340_sha256_v1";
|
|
16489
|
+
function requireExactKeys$3(record, label, keys) {
|
|
16490
|
+
const allowed = new Set(keys);
|
|
16491
|
+
for (const key of Object.keys(record)) if (!allowed.has(key)) throw new Error(`${label}.${key} is not a supported field`);
|
|
16492
|
+
for (const key of keys) if (!Object.hasOwn(record, key)) throw new Error(`${label}.${key} is required`);
|
|
16493
|
+
}
|
|
16494
|
+
function requireSignature64(value, label) {
|
|
16495
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error(`${label} must be canonical base64url`);
|
|
16496
|
+
let bytes;
|
|
16497
|
+
try {
|
|
16498
|
+
bytes = base64UrlDecode(value);
|
|
16499
|
+
} catch {
|
|
16500
|
+
throw new Error(`${label} must be valid base64url`);
|
|
16501
|
+
}
|
|
16502
|
+
if (bytes.length !== 64 || base64UrlEncode(bytes) !== value) throw new Error(`${label} must be canonical base64url for 64 bytes`);
|
|
16503
|
+
return value;
|
|
16504
|
+
}
|
|
16505
|
+
function parseWalletRecoveryEcdsaPossessionProofV1(value) {
|
|
16506
|
+
const record = requireRecord$1(value, "walletRecoveryEcdsaPossessionProof");
|
|
16507
|
+
requireExactKeys$3(record, "walletRecoveryEcdsaPossessionProof", [
|
|
16508
|
+
"kind",
|
|
16509
|
+
"scheme",
|
|
16510
|
+
"signature64B64u"
|
|
16511
|
+
]);
|
|
16512
|
+
if (record.kind !== "wallet_recovery_ecdsa_possession_proof_v1") throw new Error("walletRecoveryEcdsaPossessionProof.kind is invalid");
|
|
16513
|
+
if (record.scheme !== POSSESSION_PROOF_SCHEME_V1) throw new Error("walletRecoveryEcdsaPossessionProof.scheme is invalid");
|
|
16514
|
+
return {
|
|
16515
|
+
kind: "wallet_recovery_ecdsa_possession_proof_v1",
|
|
16516
|
+
scheme: POSSESSION_PROOF_SCHEME_V1,
|
|
16517
|
+
signature64B64u: requireSignature64(record.signature64B64u, "walletRecoveryEcdsaPossessionProof.signature64B64u")
|
|
16518
|
+
};
|
|
16519
|
+
}
|
|
16520
|
+
//#endregion
|
|
16000
16521
|
//#region ../shared-ts/src/wallet-recovery/walletRecoveryEnvelopeSet.ts
|
|
16001
16522
|
function buildWalletCustodySeedRecoveryEntry(args) {
|
|
16002
16523
|
return {
|
|
@@ -16071,47 +16592,45 @@ function parseWalletRecoveryEnvelopeEntry(raw, label) {
|
|
|
16071
16592
|
});
|
|
16072
16593
|
}
|
|
16073
16594
|
//#endregion
|
|
16074
|
-
//#region ../shared-ts/src/wallet-recovery/
|
|
16075
|
-
function
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
16079
|
-
|
|
16595
|
+
//#region ../shared-ts/src/wallet-recovery/walletRecoveryTarget.ts
|
|
16596
|
+
function parseWalletRecoveryTargetV1(raw) {
|
|
16597
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("wallet recovery target must be an object");
|
|
16598
|
+
const record = raw;
|
|
16599
|
+
const fields = Object.keys(record);
|
|
16600
|
+
switch (record.kind) {
|
|
16601
|
+
case "passkey": {
|
|
16602
|
+
if (fields.length !== 2 || !fields.includes("rpId")) throw new Error("Passkey recovery target fields are invalid");
|
|
16603
|
+
const rpId = parseWebAuthnRpId(record.rpId);
|
|
16604
|
+
if (!rpId.ok) throw new Error("Passkey recovery target RP ID is invalid");
|
|
16605
|
+
return {
|
|
16606
|
+
kind: "passkey",
|
|
16607
|
+
rpId: rpId.value
|
|
16608
|
+
};
|
|
16609
|
+
}
|
|
16610
|
+
case "google_email_otp":
|
|
16611
|
+
if (fields.length !== 2 || !fields.includes("googleProvider") || record.googleProvider !== "google") throw new Error("Google Email OTP recovery target fields are invalid");
|
|
16612
|
+
return {
|
|
16613
|
+
kind: "google_email_otp",
|
|
16614
|
+
googleProvider: "google"
|
|
16615
|
+
};
|
|
16616
|
+
default: throw new Error("wallet recovery target kind is unsupported");
|
|
16080
16617
|
}
|
|
16081
16618
|
}
|
|
16082
16619
|
//#endregion
|
|
16083
|
-
//#region
|
|
16084
|
-
|
|
16085
|
-
function requireExactKeys$3(record, label, keys) {
|
|
16086
|
-
const allowed = new Set(keys);
|
|
16087
|
-
for (const key of Object.keys(record)) if (!allowed.has(key)) throw new Error(`${label}.${key} is not a supported field`);
|
|
16088
|
-
for (const key of keys) if (!Object.hasOwn(record, key)) throw new Error(`${label}.${key} is required`);
|
|
16089
|
-
}
|
|
16090
|
-
function requireSignature64(value, label) {
|
|
16091
|
-
if (typeof value !== "string" || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error(`${label} must be canonical base64url`);
|
|
16092
|
-
let bytes;
|
|
16620
|
+
//#region src/core/authService/webauthnOidcHelpers.ts
|
|
16621
|
+
function originHostnameOrEmpty(origin) {
|
|
16093
16622
|
try {
|
|
16094
|
-
|
|
16623
|
+
return new URL(origin).hostname.toLowerCase();
|
|
16095
16624
|
} catch {
|
|
16096
|
-
|
|
16625
|
+
return "";
|
|
16097
16626
|
}
|
|
16098
|
-
if (bytes.length !== 64 || base64UrlEncode(bytes) !== value) throw new Error(`${label} must be canonical base64url for 64 bytes`);
|
|
16099
|
-
return value;
|
|
16100
16627
|
}
|
|
16101
|
-
function
|
|
16102
|
-
const
|
|
16103
|
-
|
|
16104
|
-
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
]);
|
|
16108
|
-
if (record.kind !== "wallet_recovery_ecdsa_possession_proof_v1") throw new Error("walletRecoveryEcdsaPossessionProof.kind is invalid");
|
|
16109
|
-
if (record.scheme !== POSSESSION_PROOF_SCHEME_V1) throw new Error("walletRecoveryEcdsaPossessionProof.scheme is invalid");
|
|
16110
|
-
return {
|
|
16111
|
-
kind: "wallet_recovery_ecdsa_possession_proof_v1",
|
|
16112
|
-
scheme: POSSESSION_PROOF_SCHEME_V1,
|
|
16113
|
-
signature64B64u: requireSignature64(record.signature64B64u, "walletRecoveryEcdsaPossessionProof.signature64B64u")
|
|
16114
|
-
};
|
|
16628
|
+
function isHostWithinRpId(host, rpId) {
|
|
16629
|
+
const h = (host || "").toLowerCase();
|
|
16630
|
+
const r = String(rpId).toLowerCase();
|
|
16631
|
+
if (!h || !r) return false;
|
|
16632
|
+
if ((process.env.NO_CADDY === "1" || process.env.VITE_NO_CADDY === "1") && (h === "localhost" || h === "127.0.0.1") && r.endsWith(".localhost")) return true;
|
|
16633
|
+
return h === r || h.endsWith(`.${r}`);
|
|
16115
16634
|
}
|
|
16116
16635
|
//#endregion
|
|
16117
16636
|
//#region src/router/transport/fetch/routes/passkeyCustody.ts
|
|
@@ -16139,239 +16658,6 @@ const RECOVERY_READ_ROUTE_ID = "wallet_recovery_codes_read";
|
|
|
16139
16658
|
const RECOVERY_STATUS_ROUTE_ID = "wallet_recovery_status";
|
|
16140
16659
|
const EMAIL_OTP_CHALLENGE_ROUTE_ID = "wallet_custody_email_otp_challenge";
|
|
16141
16660
|
const ENVELOPE_OWNERSHIP_UPGRADE_ROUTE_ID = "wallet_custody_envelope_ownership_upgrade";
|
|
16142
|
-
const CUSTODY_OPERATION_CAPABILITY_ID = "wallet-custody-admin";
|
|
16143
|
-
const CUSTODY_PROOF_TTL_MS = 6e4;
|
|
16144
|
-
function hasFullOwnerAuthority$1(authority) {
|
|
16145
|
-
return sameDelegatedWalletAuthorityV1(buildDelegatedWalletAuthorityV1({ permissions: authority.permissions }), buildDelegatedWalletAuthorityV1({ permissions: buildFullOwnerPermissionsV1() }));
|
|
16146
|
-
}
|
|
16147
|
-
function parseRequiredAuthorizationValue(parsed) {
|
|
16148
|
-
if (!parsed.ok) throw new Error(parsed.error.message);
|
|
16149
|
-
return parsed.value;
|
|
16150
|
-
}
|
|
16151
|
-
async function custodyDigest(domain, value) {
|
|
16152
|
-
return sha256Utf8DigestB64u(`${domain}|${alphabetizeStringify(value)}`);
|
|
16153
|
-
}
|
|
16154
|
-
async function buildWalletCustodyOperation(input) {
|
|
16155
|
-
const tenantId = parseRequiredAuthorizationValue(parseTenantId(input.tenantId));
|
|
16156
|
-
const principalId = parseRequiredAuthorizationValue(parsePrincipalId(input.principalId));
|
|
16157
|
-
const capabilityId = parseRequiredAuthorizationValue(parseCapabilityId(CUSTODY_OPERATION_CAPABILITY_ID));
|
|
16158
|
-
const operationRef = buildVaultOperationRef("vault.reveal");
|
|
16159
|
-
const laneDigest = await custodyDigest("seams:wallet-custody:lane:v1", {
|
|
16160
|
-
capabilityId,
|
|
16161
|
-
operation: input.operation,
|
|
16162
|
-
operationRef
|
|
16163
|
-
});
|
|
16164
|
-
const challengeDigest = await computeWalletCustodyAdminChallengeDigest({
|
|
16165
|
-
walletId: input.walletId,
|
|
16166
|
-
operation: input.operation,
|
|
16167
|
-
payload: input.payload,
|
|
16168
|
-
requestOrigin: input.requestOrigin
|
|
16169
|
-
});
|
|
16170
|
-
const intentDigest = challengeDigest;
|
|
16171
|
-
const displayDigest = await custodyDigest("seams:wallet-custody:display:v1", {
|
|
16172
|
-
walletId: input.walletId,
|
|
16173
|
-
operation: input.operation
|
|
16174
|
-
});
|
|
16175
|
-
return {
|
|
16176
|
-
operation: buildCapabilityOperationEnvelope({
|
|
16177
|
-
tenantId,
|
|
16178
|
-
principalId,
|
|
16179
|
-
capabilityId,
|
|
16180
|
-
operationId: parseRequiredAuthorizationValue(parseCapabilityOperationId(`wallet-custody:${input.operation}:${intentDigest}`)),
|
|
16181
|
-
operation: operationRef,
|
|
16182
|
-
digests: {
|
|
16183
|
-
laneDigest,
|
|
16184
|
-
intentDigest,
|
|
16185
|
-
displayDigest
|
|
16186
|
-
}
|
|
16187
|
-
}),
|
|
16188
|
-
challengeDigest
|
|
16189
|
-
};
|
|
16190
|
-
}
|
|
16191
|
-
async function authorizeWalletCustodyOperation(input) {
|
|
16192
|
-
const requestOrigin = normalizeOptionalTrimmedString(input.ctx.request.headers.get("origin"));
|
|
16193
|
-
if (!requestOrigin) return toCustodyAuthorizationFailure(400, "invalid_origin", "request Origin is required");
|
|
16194
|
-
let origin;
|
|
16195
|
-
let proof;
|
|
16196
|
-
let walletId;
|
|
16197
|
-
try {
|
|
16198
|
-
origin = parseSessionOrigin(requestOrigin);
|
|
16199
|
-
walletId = parseRequiredAuthorizationValue(parseWalletId(input.walletId));
|
|
16200
|
-
proof = parseCustodyOwnerProof(input.factorProof);
|
|
16201
|
-
} catch (error) {
|
|
16202
|
-
return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "owner proof is invalid");
|
|
16203
|
-
}
|
|
16204
|
-
if (proof.kind === "passkey" && proof.walletId !== input.walletId) return toCustodyAuthorizationFailure(403, "scope_mismatch", "owner proof wallet differs");
|
|
16205
|
-
let principalId;
|
|
16206
|
-
let factor;
|
|
16207
|
-
let operation;
|
|
16208
|
-
let challengeDigest;
|
|
16209
|
-
try {
|
|
16210
|
-
principalId = proof.kind === "passkey" ? input.walletId : proof.provider_subject_id;
|
|
16211
|
-
const built = await buildWalletCustodyOperation({
|
|
16212
|
-
tenantId: input.ctx.service.authorizedOperations.tenantId,
|
|
16213
|
-
principalId,
|
|
16214
|
-
walletId: input.walletId,
|
|
16215
|
-
operation: input.operation,
|
|
16216
|
-
payload: input.payload,
|
|
16217
|
-
requestOrigin
|
|
16218
|
-
});
|
|
16219
|
-
operation = built.operation;
|
|
16220
|
-
challengeDigest = built.challengeDigest;
|
|
16221
|
-
} catch (error) {
|
|
16222
|
-
return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "owner proof binding is invalid");
|
|
16223
|
-
}
|
|
16224
|
-
const operationFingerprintDigest = await computeCapabilityOperationFingerprintDigest(operation);
|
|
16225
|
-
const nowMs = Date.now();
|
|
16226
|
-
if (proof.challenge_digest !== String(challengeDigest)) return toCustodyAuthorizationFailure(403, "scope_mismatch", "factor proof challenge does not match the requested operation");
|
|
16227
|
-
if (proof.kind === "passkey") {
|
|
16228
|
-
let authority;
|
|
16229
|
-
try {
|
|
16230
|
-
authority = buildPasskeyWalletAuthAuthority({
|
|
16231
|
-
walletId,
|
|
16232
|
-
rpId: proof.rpId,
|
|
16233
|
-
credentialIdB64u: proof.credentialIdB64u
|
|
16234
|
-
});
|
|
16235
|
-
} catch (error) {
|
|
16236
|
-
return toCustodyAuthorizationFailure(400, "invalid_owner_proof", error instanceof Error ? error.message : "passkey authority is invalid");
|
|
16237
|
-
}
|
|
16238
|
-
const active = await input.ctx.service.walletAuthMethods.resolveActivePasskeyAuthorityForVerifiedCredential({
|
|
16239
|
-
walletId,
|
|
16240
|
-
rpId: authority.verifier.rpId,
|
|
16241
|
-
credentialIdB64u: authority.factor.credentialIdB64u
|
|
16242
|
-
});
|
|
16243
|
-
if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
|
|
16244
|
-
if (!hasFullOwnerAuthority$1(active.walletAuthority)) return toCustodyAuthorizationFailure(403, "forbidden", "Full owner authority is required");
|
|
16245
|
-
const credential = parseWebAuthnAuthenticationCredential$1(proof.webauthn_authentication);
|
|
16246
|
-
if (!credential) return toCustodyAuthorizationFailure(400, "invalid_owner_proof", "passkey assertion is invalid");
|
|
16247
|
-
if (String(credential.rawId || credential.id).trim() !== authority.factor.credentialIdB64u) return toCustodyAuthorizationFailure(401, "unauthorized", "passkey credential changed");
|
|
16248
|
-
const verified = await input.ctx.service.webAuthn.verifyWebAuthnAuthenticationLite({
|
|
16249
|
-
userId: input.walletId,
|
|
16250
|
-
rpId: authority.verifier.rpId,
|
|
16251
|
-
expectedChallenge: challengeDigest,
|
|
16252
|
-
expected_origin: requestOrigin,
|
|
16253
|
-
webauthn_authentication: credential
|
|
16254
|
-
});
|
|
16255
|
-
if (!verified.success || !verified.verified) return toCustodyAuthorizationFailure(401, verified.code || "not_verified", verified.message || "WebAuthn authentication verification failed");
|
|
16256
|
-
factor = buildVerifiedWalletOperationPasskeyFactorResult({
|
|
16257
|
-
tenantId: operation.tenantId,
|
|
16258
|
-
principalId: operation.principalId,
|
|
16259
|
-
walletId,
|
|
16260
|
-
authorityRef: await walletAuthAuthorityRef({ authority: active.authority }),
|
|
16261
|
-
requestOrigin: origin,
|
|
16262
|
-
audience: origin,
|
|
16263
|
-
factorId: parseRequiredAuthorizationValue(parseAuthFactorId(`passkey:${authority.factor.credentialIdB64u}`)),
|
|
16264
|
-
credentialIdB64u: parseRequiredAuthorizationValue(parseWebAuthnCredentialIdB64u(authority.factor.credentialIdB64u)),
|
|
16265
|
-
assertionDigest: await custodyDigest("seams:wallet-custody:assertion:v1", { credential }),
|
|
16266
|
-
operation,
|
|
16267
|
-
verifiedAtMs: nowMs,
|
|
16268
|
-
expiresAtMs: nowMs + CUSTODY_PROOF_TTL_MS
|
|
16269
|
-
});
|
|
16270
|
-
} else {
|
|
16271
|
-
const providerSubject = parseRequiredAuthorizationValue(parseProviderSubject(proof.provider_subject_id));
|
|
16272
|
-
const orgId = parseRequiredAuthorizationValue(parseOrgId(input.ctx.service.authorizedOperations.tenantId));
|
|
16273
|
-
const active = await input.ctx.service.walletAuthMethods.resolveActiveEmailOtpAuthorityForVerifiedSubject({
|
|
16274
|
-
walletId: input.walletId,
|
|
16275
|
-
providerUserId: proof.provider_subject_id
|
|
16276
|
-
});
|
|
16277
|
-
if (!active.ok) return toCustodyAuthorizationFailure(403, active.code, active.message);
|
|
16278
|
-
const activeAuthorityRef = await walletAuthAuthorityRef({ authority: active.authority });
|
|
16279
|
-
const ownerProofBindingDigest = await hashEmailOtpOperationBinding({
|
|
16280
|
-
walletId: input.walletId,
|
|
16281
|
-
providerUserId: proof.provider_subject_id,
|
|
16282
|
-
orgId,
|
|
16283
|
-
operation: `wallet_custody:${input.operation}`,
|
|
16284
|
-
requestOrigin,
|
|
16285
|
-
audience: requestOrigin,
|
|
16286
|
-
authorityRef: activeAuthorityRef,
|
|
16287
|
-
operationFingerprintDigest: challengeDigest
|
|
16288
|
-
});
|
|
16289
|
-
const verified = await input.ctx.service.emailOtp.verifyEmailOtpChallenge({
|
|
16290
|
-
userId: proof.provider_subject_id,
|
|
16291
|
-
walletId: input.walletId,
|
|
16292
|
-
orgId,
|
|
16293
|
-
challengeId: proof.challenge_id,
|
|
16294
|
-
otpCode: proof.otp_code,
|
|
16295
|
-
otpChannel: EMAIL_OTP_CHANNEL,
|
|
16296
|
-
ownerProofBindingDigest,
|
|
16297
|
-
operation: WALLET_EMAIL_OTP_UNLOCK_OPERATION
|
|
16298
|
-
});
|
|
16299
|
-
if (!verified.ok) return toCustodyAuthorizationFailure(verified.code === "invalid_body" ? 400 : 401, verified.code, verified.message);
|
|
16300
|
-
const consumed = await input.ctx.service.emailOtp.consumeEmailOtpGrant({
|
|
16301
|
-
subject: {
|
|
16302
|
-
kind: "provider_identity",
|
|
16303
|
-
orgId,
|
|
16304
|
-
providerSubject,
|
|
16305
|
-
walletId
|
|
16306
|
-
},
|
|
16307
|
-
loginGrant: verified.loginGrant,
|
|
16308
|
-
otpChannel: EMAIL_OTP_CHANNEL
|
|
16309
|
-
});
|
|
16310
|
-
if (!consumed.ok) return toCustodyAuthorizationFailure(emailOtpStatusCode(consumed.code), consumed.code, consumed.message);
|
|
16311
|
-
factor = buildVerifiedWalletOperationEmailOtpFactorResult({
|
|
16312
|
-
tenantId: operation.tenantId,
|
|
16313
|
-
principalId: operation.principalId,
|
|
16314
|
-
walletId,
|
|
16315
|
-
authorityRef: activeAuthorityRef,
|
|
16316
|
-
requestOrigin: origin,
|
|
16317
|
-
audience: origin,
|
|
16318
|
-
factorId: parseRequiredAuthorizationValue(parseAuthFactorId(`email_otp:${active.authority.factor.provider}:${proof.provider_subject_id}`)),
|
|
16319
|
-
operation,
|
|
16320
|
-
challengeId: parseRequiredAuthorizationValue(parseEmailOtpChallengeId(consumed.challengeId)),
|
|
16321
|
-
verificationReceiptDigest: await custodyDigest("seams:wallet-custody:otp-receipt:v1", {
|
|
16322
|
-
challengeId: consumed.challengeId,
|
|
16323
|
-
operationFingerprintDigest
|
|
16324
|
-
}),
|
|
16325
|
-
verifiedAtMs: nowMs,
|
|
16326
|
-
expiresAtMs: Math.min(nowMs + CUSTODY_PROOF_TTL_MS, verified.grantExpiresAtMs)
|
|
16327
|
-
});
|
|
16328
|
-
}
|
|
16329
|
-
const ownerProof = await input.ctx.service.authorizedOperations.buildVerifiedOwnerProof({
|
|
16330
|
-
purpose: "operation",
|
|
16331
|
-
proofId: parseVerifiedOwnerProofId(`wallet-custody:${operation.operationId}`),
|
|
16332
|
-
factor
|
|
16333
|
-
});
|
|
16334
|
-
if (ownerProof.purpose !== "operation" || ownerProof.operation.operationFingerprintDigest !== operationFingerprintDigest) return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "owner proof binding is invalid");
|
|
16335
|
-
const evidenceSet = await input.ctx.service.authorizedOperations.recordVerifiedWalletOperationFactorEvidenceSet({
|
|
16336
|
-
operation,
|
|
16337
|
-
evidenceId: parseRequiredAuthorizationValue(parseAuthorizationEvidenceId(`wallet-custody:evidence:${operation.operationId}`)),
|
|
16338
|
-
evidenceSetId: parseRequiredAuthorizationValue(parseAuthorizationEvidenceSetId(`wallet-custody:evidence-set:${operation.operationId}`)),
|
|
16339
|
-
factor
|
|
16340
|
-
});
|
|
16341
|
-
const admission = await input.ctx.service.authorizedOperations.admitAuthorizedOperation({ operation: {
|
|
16342
|
-
tenantId: operation.tenantId,
|
|
16343
|
-
authorizedOperationId: parseRequiredAuthorizationValue(parseAuthorizedOperationId(`wallet-custody:authorized:${operation.operationId}`)),
|
|
16344
|
-
auditEventId: parseRequiredAuthorizationValue(parseAuthorizationAuditEventId(`wallet-custody:audit:${operation.operationId}`)),
|
|
16345
|
-
operation,
|
|
16346
|
-
authorization: {
|
|
16347
|
-
kind: "verified_step_up",
|
|
16348
|
-
evidenceSetDigest: evidenceSet.evidenceSetDigest
|
|
16349
|
-
},
|
|
16350
|
-
quota: { kind: "quota_neutral" },
|
|
16351
|
-
claimedAtMs: nowMs
|
|
16352
|
-
} });
|
|
16353
|
-
switch (admission.kind) {
|
|
16354
|
-
case "claimed": return {
|
|
16355
|
-
ok: true,
|
|
16356
|
-
operation: admission.operation
|
|
16357
|
-
};
|
|
16358
|
-
case "replayed":
|
|
16359
|
-
case "operation_in_progress": return {
|
|
16360
|
-
ok: false,
|
|
16361
|
-
response: authorizedOperationReplay(admission.operation)
|
|
16362
|
-
};
|
|
16363
|
-
case "authorization_grant_rejected":
|
|
16364
|
-
case "verified_step_up_rejected":
|
|
16365
|
-
case "wallet_session_quota_exhausted":
|
|
16366
|
-
case "material_mismatch": return toCustodyAuthorizationFailure(403, "owner_proof_rejected", "owner proof was rejected");
|
|
16367
|
-
}
|
|
16368
|
-
}
|
|
16369
|
-
function toCustodyAuthorizationFailure(status, code, message) {
|
|
16370
|
-
return {
|
|
16371
|
-
ok: false,
|
|
16372
|
-
response: routeErrorResponse(status, code, message)
|
|
16373
|
-
};
|
|
16374
|
-
}
|
|
16375
16661
|
function routeErrorResponse(status, code, message) {
|
|
16376
16662
|
return toFetchRouteResponse({
|
|
16377
16663
|
status,
|
|
@@ -16387,23 +16673,6 @@ function matchRoute(ctx, routeId) {
|
|
|
16387
16673
|
if (!route) throw new Error(`Missing route definition for ${routeId}`);
|
|
16388
16674
|
return matchesRouteDefinitionRequest(route, ctx.method, ctx.pathname) ? route : null;
|
|
16389
16675
|
}
|
|
16390
|
-
async function completeWalletCustodyOperation(ctx, operation, status, body, result = "succeeded") {
|
|
16391
|
-
const bodyText = JSON.stringify(body);
|
|
16392
|
-
await ctx.service.authorizedOperations.completeAuthorizedOperation({
|
|
16393
|
-
operation,
|
|
16394
|
-
result,
|
|
16395
|
-
response: {
|
|
16396
|
-
status,
|
|
16397
|
-
contentType: "application/json",
|
|
16398
|
-
bodyText
|
|
16399
|
-
},
|
|
16400
|
-
completedAtMs: Date.now()
|
|
16401
|
-
});
|
|
16402
|
-
return toFetchRouteResponse({
|
|
16403
|
-
status,
|
|
16404
|
-
body
|
|
16405
|
-
});
|
|
16406
|
-
}
|
|
16407
16676
|
async function handlePasskeyCustody(ctx) {
|
|
16408
16677
|
if (!matchRoute(ctx, ROUTE_ID)) return null;
|
|
16409
16678
|
const body = await readJsonObject(ctx.request);
|
|
@@ -16431,8 +16700,8 @@ function parseWalletCustodyEmailOtpChallengeRequest(value, actualOrigin) {
|
|
|
16431
16700
|
];
|
|
16432
16701
|
const withoutOptional = keys.filter((key) => key !== "requestOrigin");
|
|
16433
16702
|
if (withoutOptional.length !== required.length || withoutOptional.some((key, index) => key !== required.slice().sort()[index]) || keys.includes("requestOrigin") && keys.length !== expected.length) throw new Error("Email OTP custody challenge body contains unsupported fields");
|
|
16434
|
-
const walletId =
|
|
16435
|
-
const providerSubjectId =
|
|
16703
|
+
const walletId = requireParsed$4(parseWalletId(value.walletId), "authorization");
|
|
16704
|
+
const providerSubjectId = requireParsed$4(parseProviderSubject(value.providerSubjectId), "authorization");
|
|
16436
16705
|
const operation = value.operation;
|
|
16437
16706
|
if (operation !== "credentials_list" && operation !== "credential_label" && operation !== "recovery_rotate" && operation !== "recovery_read" && operation !== "recovery_status" && operation !== "recovery_backup_acknowledge") throw new Error("Email OTP custody challenge operation is invalid");
|
|
16438
16707
|
if (!isPlainObject(value.payload)) throw new Error("Email OTP custody challenge payload is invalid");
|
|
@@ -16461,8 +16730,8 @@ async function handleWalletCustodyEmailOtpChallenge(ctx) {
|
|
|
16461
16730
|
providerUserId: request.providerSubjectId
|
|
16462
16731
|
});
|
|
16463
16732
|
if (!active.ok) return routeErrorResponse(403, active.code, active.message);
|
|
16464
|
-
const authoritativeOrgId =
|
|
16465
|
-
const challengeDigest = await
|
|
16733
|
+
const authoritativeOrgId = requireParsed$4(parseOrgId(ctx.service.authorizedOperations.tenantId), "authorization");
|
|
16734
|
+
const challengeDigest = await computeWalletOwnerAdminChallengeDigest(request);
|
|
16466
16735
|
const ownerProofBindingDigest = await hashEmailOtpOperationBinding({
|
|
16467
16736
|
walletId: request.walletId,
|
|
16468
16737
|
providerUserId: request.providerSubjectId,
|
|
@@ -16510,7 +16779,7 @@ async function handleWalletCustodyCredentialsList(ctx) {
|
|
|
16510
16779
|
const walletId = walletIdFromPath(route.path, ctx.pathname);
|
|
16511
16780
|
if (!walletId) return routeErrorResponse(400, "invalid_request", "credential list needs a wallet");
|
|
16512
16781
|
const body = await readJsonObject(ctx.request);
|
|
16513
|
-
const authorized = await
|
|
16782
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
16514
16783
|
ctx,
|
|
16515
16784
|
walletId,
|
|
16516
16785
|
operation: "credentials_list",
|
|
@@ -16521,7 +16790,7 @@ async function handleWalletCustodyCredentialsList(ctx) {
|
|
|
16521
16790
|
const parsedWalletId = parseWalletId(walletId);
|
|
16522
16791
|
if (!parsedWalletId.ok) return routeErrorResponse(400, "invalid_request", "wallet id is invalid");
|
|
16523
16792
|
const result = await ctx.service.passkeyCustody.listWalletCredentials({ walletId: parsedWalletId.value });
|
|
16524
|
-
return await
|
|
16793
|
+
return await completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
16525
16794
|
ok: true,
|
|
16526
16795
|
credentials: result
|
|
16527
16796
|
});
|
|
@@ -16589,7 +16858,7 @@ async function handleWalletCustodyCredentialLabel(ctx) {
|
|
|
16589
16858
|
const envelopeId = normalizeOptionalTrimmedString(body?.envelopeId);
|
|
16590
16859
|
const label = body?.label;
|
|
16591
16860
|
if (!walletId || !envelopeId || label !== void 0 && typeof label !== "string") return routeErrorResponse(400, "invalid_request", "credential rename needs a wallet, envelope, and optional label");
|
|
16592
|
-
const authorized = await
|
|
16861
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
16593
16862
|
ctx,
|
|
16594
16863
|
walletId,
|
|
16595
16864
|
operation: "credential_label",
|
|
@@ -16609,26 +16878,26 @@ async function handleWalletCustodyCredentialLabel(ctx) {
|
|
|
16609
16878
|
...label === void 0 ? {} : { label }
|
|
16610
16879
|
});
|
|
16611
16880
|
switch (result.kind) {
|
|
16612
|
-
case "updated": return await
|
|
16881
|
+
case "updated": return await completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
16613
16882
|
ok: true,
|
|
16614
16883
|
credential: result.projection
|
|
16615
16884
|
});
|
|
16616
|
-
case "missing": return await
|
|
16885
|
+
case "missing": return await completeWalletOwnerOperation(ctx, authorized.operation, 404, {
|
|
16617
16886
|
ok: false,
|
|
16618
16887
|
code: "credential_not_found",
|
|
16619
16888
|
message: "credential not found"
|
|
16620
16889
|
}, "failed_before_side_effect");
|
|
16621
|
-
case "conflict": return await
|
|
16890
|
+
case "conflict": return await completeWalletOwnerOperation(ctx, authorized.operation, 409, {
|
|
16622
16891
|
ok: false,
|
|
16623
16892
|
code: "credential_activity_conflict",
|
|
16624
16893
|
message: "credential changed; retry"
|
|
16625
16894
|
}, "failed_before_side_effect");
|
|
16626
|
-
case "invalid_envelope_id": return await
|
|
16895
|
+
case "invalid_envelope_id": return await completeWalletOwnerOperation(ctx, authorized.operation, 400, {
|
|
16627
16896
|
ok: false,
|
|
16628
16897
|
code: "invalid_envelope_id",
|
|
16629
16898
|
message: "credential envelope id is invalid"
|
|
16630
16899
|
}, "failed_before_side_effect");
|
|
16631
|
-
case "invalid_label": return await
|
|
16900
|
+
case "invalid_label": return await completeWalletOwnerOperation(ctx, authorized.operation, 400, {
|
|
16632
16901
|
ok: false,
|
|
16633
16902
|
code: "invalid_label",
|
|
16634
16903
|
message: result.reason
|
|
@@ -16854,7 +17123,7 @@ function parseWalletRecoveryOperationRequest(body, fields, label) {
|
|
|
16854
17123
|
if (!body) throw new Error(`${label} body is required`);
|
|
16855
17124
|
requireExactObjectFields(body, fields, label);
|
|
16856
17125
|
return {
|
|
16857
|
-
recoveryOperationId:
|
|
17126
|
+
recoveryOperationId: requireParsed$4(parseWalletRecoveryOperationId(body.recoveryOperationId), "authorization"),
|
|
16858
17127
|
reservationId: parseRecoveryCodeReservationId(body.reservationId)
|
|
16859
17128
|
};
|
|
16860
17129
|
}
|
|
@@ -16876,7 +17145,7 @@ function parseWalletRecoveryEmailOtpVerifyRequest(body) {
|
|
|
16876
17145
|
"challengeId",
|
|
16877
17146
|
"otpCode"
|
|
16878
17147
|
], "wallet recovery Email OTP verification"),
|
|
16879
|
-
challengeId:
|
|
17148
|
+
challengeId: requireParsed$4(parseEmailOtpChallengeId(body?.challengeId), "authorization"),
|
|
16880
17149
|
otpCode: requireTrimmedString(body?.otpCode, "otpCode")
|
|
16881
17150
|
};
|
|
16882
17151
|
}
|
|
@@ -16997,7 +17266,7 @@ function parseWireRequest(body, originHeader) {
|
|
|
16997
17266
|
if (!body || typeof body !== "object") return null;
|
|
16998
17267
|
const challengeId = normalizeOptionalTrimmedString(body.challengeId);
|
|
16999
17268
|
const locator = parseEnvelopeRetrievalLocator(body.locator);
|
|
17000
|
-
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$
|
|
17269
|
+
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$2(body.webauthnAuthentication);
|
|
17001
17270
|
if (!challengeId || !locator || !webauthnAuthentication) return null;
|
|
17002
17271
|
const expectedOrigin = normalizeOptionalTrimmedString(originHeader);
|
|
17003
17272
|
if (!expectedOrigin) return null;
|
|
@@ -17179,13 +17448,6 @@ function parseEcdsaMaterialPossessionProofs(value) {
|
|
|
17179
17448
|
};
|
|
17180
17449
|
});
|
|
17181
17450
|
}
|
|
17182
|
-
function authorizedOperationReplay(operation) {
|
|
17183
|
-
if (operation.lifecycle !== "completed") return routeErrorResponse(409, "recovery_in_progress", "wallet recovery is still in progress");
|
|
17184
|
-
return new Response(operation.response.bodyText, {
|
|
17185
|
-
status: operation.response.status,
|
|
17186
|
-
headers: { "content-type": operation.response.contentType }
|
|
17187
|
-
});
|
|
17188
|
-
}
|
|
17189
17451
|
async function handleWalletRecoveryBackupAcknowledge(ctx) {
|
|
17190
17452
|
if (!matchRoute(ctx, RECOVERY_ACK_ROUTE_ID)) return null;
|
|
17191
17453
|
let request;
|
|
@@ -17194,7 +17456,7 @@ async function handleWalletRecoveryBackupAcknowledge(ctx) {
|
|
|
17194
17456
|
} catch {
|
|
17195
17457
|
return routeErrorResponse(400, "invalid_request", "acknowledgement body is invalid");
|
|
17196
17458
|
}
|
|
17197
|
-
const authorized = await
|
|
17459
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
17198
17460
|
ctx,
|
|
17199
17461
|
walletId: request.walletId,
|
|
17200
17462
|
operation: "recovery_backup_acknowledge",
|
|
@@ -17210,17 +17472,17 @@ async function handleWalletRecoveryBackupAcknowledge(ctx) {
|
|
|
17210
17472
|
walletId: request.walletId,
|
|
17211
17473
|
expectedIssuedAtMs: request.expectedIssuedAtMs
|
|
17212
17474
|
});
|
|
17213
|
-
if (result.kind === "conflict") return await
|
|
17475
|
+
if (result.kind === "conflict") return await completeWalletOwnerOperation(ctx, authorized.operation, 409, {
|
|
17214
17476
|
ok: false,
|
|
17215
17477
|
code: "recovery_set_conflict",
|
|
17216
17478
|
message: "the recovery set changed"
|
|
17217
17479
|
}, "failed_before_side_effect");
|
|
17218
|
-
if (result.kind === "no_recovery_set") return await
|
|
17480
|
+
if (result.kind === "no_recovery_set") return await completeWalletOwnerOperation(ctx, authorized.operation, 404, {
|
|
17219
17481
|
ok: false,
|
|
17220
17482
|
code: "no_recovery_set",
|
|
17221
17483
|
message: "this wallet has no issued recovery codes to acknowledge"
|
|
17222
17484
|
}, "failed_before_side_effect");
|
|
17223
|
-
return await
|
|
17485
|
+
return await completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
17224
17486
|
ok: true,
|
|
17225
17487
|
issuedAtMs: result.issuedAtMs
|
|
17226
17488
|
});
|
|
@@ -17258,7 +17520,7 @@ async function handleWalletRecoveryRotate(ctx) {
|
|
|
17258
17520
|
} catch (error) {
|
|
17259
17521
|
return routeErrorResponse(400, "invalid_request", error instanceof Error ? error.message : "replacement recovery set is invalid");
|
|
17260
17522
|
}
|
|
17261
|
-
const authorized = await
|
|
17523
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
17262
17524
|
ctx,
|
|
17263
17525
|
walletId,
|
|
17264
17526
|
operation: "recovery_rotate",
|
|
@@ -17279,22 +17541,22 @@ async function handleWalletRecoveryRotate(ctx) {
|
|
|
17279
17541
|
expectedStoreVersion
|
|
17280
17542
|
});
|
|
17281
17543
|
switch (result.kind) {
|
|
17282
|
-
case "rotated": return await
|
|
17544
|
+
case "rotated": return await completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
17283
17545
|
ok: true,
|
|
17284
17546
|
issuedAtMs: result.issuedAtMs,
|
|
17285
17547
|
storeVersion: result.storeVersion
|
|
17286
17548
|
});
|
|
17287
|
-
case "no_recovery_set": return await
|
|
17549
|
+
case "no_recovery_set": return await completeWalletOwnerOperation(ctx, authorized.operation, 404, {
|
|
17288
17550
|
ok: false,
|
|
17289
17551
|
code: "no_recovery_set",
|
|
17290
17552
|
message: "this wallet has no codes to rotate"
|
|
17291
17553
|
}, "failed_before_side_effect");
|
|
17292
|
-
case "conflict": return await
|
|
17554
|
+
case "conflict": return await completeWalletOwnerOperation(ctx, authorized.operation, 409, {
|
|
17293
17555
|
ok: false,
|
|
17294
17556
|
code: "recovery_set_conflict",
|
|
17295
17557
|
message: "the recovery set changed during this rotation; try again"
|
|
17296
17558
|
}, "failed_before_side_effect");
|
|
17297
|
-
case "rejected": return await
|
|
17559
|
+
case "rejected": return await completeWalletOwnerOperation(ctx, authorized.operation, 400, {
|
|
17298
17560
|
ok: false,
|
|
17299
17561
|
code: "rotation_rejected",
|
|
17300
17562
|
message: result.reason
|
|
@@ -17306,7 +17568,7 @@ async function handleWalletRecoveryRead(ctx) {
|
|
|
17306
17568
|
const body = await readJsonObject(ctx.request);
|
|
17307
17569
|
const walletId = normalizeOptionalTrimmedString(body?.walletId);
|
|
17308
17570
|
if (!walletId) return routeErrorResponse(400, "invalid_request", "a read needs a wallet");
|
|
17309
|
-
const authorized = await
|
|
17571
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
17310
17572
|
ctx,
|
|
17311
17573
|
walletId,
|
|
17312
17574
|
operation: "recovery_read",
|
|
@@ -17315,12 +17577,12 @@ async function handleWalletRecoveryRead(ctx) {
|
|
|
17315
17577
|
});
|
|
17316
17578
|
if (!authorized.ok) return authorized.response;
|
|
17317
17579
|
const result = await ctx.service.passkeyCustody.readRecoverySet({ walletId });
|
|
17318
|
-
if (result.kind === "no_recovery_set") return await
|
|
17580
|
+
if (result.kind === "no_recovery_set") return await completeWalletOwnerOperation(ctx, authorized.operation, 404, {
|
|
17319
17581
|
ok: false,
|
|
17320
17582
|
code: "no_recovery_set",
|
|
17321
17583
|
message: "this wallet has no issued recovery codes"
|
|
17322
17584
|
}, "failed_before_side_effect");
|
|
17323
|
-
return await
|
|
17585
|
+
return await completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
17324
17586
|
ok: true,
|
|
17325
17587
|
recoverySet: result.record,
|
|
17326
17588
|
storeVersion: result.storeVersion
|
|
@@ -17338,7 +17600,7 @@ async function handleWalletRecoveryStatus(ctx) {
|
|
|
17338
17600
|
} catch {
|
|
17339
17601
|
return routeErrorResponse(400, "invalid_request", "status body is invalid");
|
|
17340
17602
|
}
|
|
17341
|
-
const authorized = await
|
|
17603
|
+
const authorized = await authorizeWalletOwnerOperation({
|
|
17342
17604
|
ctx,
|
|
17343
17605
|
walletId,
|
|
17344
17606
|
operation: "recovery_status",
|
|
@@ -17350,12 +17612,12 @@ async function handleWalletRecoveryStatus(ctx) {
|
|
|
17350
17612
|
});
|
|
17351
17613
|
if (!authorized.ok) return authorized.response;
|
|
17352
17614
|
const result = await ctx.service.passkeyCustody.readRecoveryStatus({ walletId });
|
|
17353
|
-
if (result.kind === "no_recovery_set") return await
|
|
17615
|
+
if (result.kind === "no_recovery_set") return await completeWalletOwnerOperation(ctx, authorized.operation, 404, {
|
|
17354
17616
|
ok: false,
|
|
17355
17617
|
code: "no_recovery_set",
|
|
17356
17618
|
message: "this wallet has no recovery codes"
|
|
17357
17619
|
}, "failed_before_side_effect");
|
|
17358
|
-
return await
|
|
17620
|
+
return await completeWalletOwnerOperation(ctx, authorized.operation, 200, {
|
|
17359
17621
|
ok: true,
|
|
17360
17622
|
activeCodeCount: result.activeCodeCount,
|
|
17361
17623
|
totalCodeCount: result.totalCodeCount,
|
|
@@ -23789,7 +24051,7 @@ function parseSyncAccountVerifyRequest(input) {
|
|
|
23789
24051
|
if (unexpectedKey) return invalidSyncAccountBody(`Unsupported sync-account verify field: ${unexpectedKey}`);
|
|
23790
24052
|
const challengeId = normalizeOptionalTrimmedString(input.body.challengeId);
|
|
23791
24053
|
if (!challengeId) return invalidSyncAccountBody("challengeId is required");
|
|
23792
|
-
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$
|
|
24054
|
+
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$2(input.body.webauthn_authentication);
|
|
23793
24055
|
if (!webauthnAuthentication) return invalidSyncAccountBody("webauthn_authentication is required");
|
|
23794
24056
|
const expectedOrigin = normalizeOptionalTrimmedString(input.origin);
|
|
23795
24057
|
if (!expectedOrigin) return invalidSyncAccountBody("Origin header is required");
|
|
@@ -24549,6 +24811,40 @@ function isWalletSessionUnavailableStepUpError(result) {
|
|
|
24549
24811
|
return isPlainObject(result.body) && result.body.code === "wallet_session_unavailable";
|
|
24550
24812
|
}
|
|
24551
24813
|
async function authenticateRouterAbWalletOperationStepUp(input) {
|
|
24814
|
+
if (!extractBearerCredential(input.headers) && input.authorizedOperations) {
|
|
24815
|
+
const origin = parseStepUpRequestOrigin(input.headers);
|
|
24816
|
+
if (!origin.ok) return origin;
|
|
24817
|
+
const operationId = requireAuthorizationValue$2(parseAuthorizedOperationId(`normal-signing-operation:${input.scope.request_id}`));
|
|
24818
|
+
const parsedWalletId = parseWalletId(input.scope.account_id);
|
|
24819
|
+
if (!parsedWalletId.ok) return walletSessionScopeInvalidFailure();
|
|
24820
|
+
const walletId = parsedWalletId.value;
|
|
24821
|
+
const recorded = await input.authorizedOperations.resolveRecordedPasskeyOwnerOperation({
|
|
24822
|
+
authorizedOperationId: operationId,
|
|
24823
|
+
walletId,
|
|
24824
|
+
requestOrigin: origin.requestOrigin,
|
|
24825
|
+
nowMs: Date.now()
|
|
24826
|
+
});
|
|
24827
|
+
if (!recorded || input.requestExpiresAtMs > recorded.expiresAtMs) return {
|
|
24828
|
+
ok: false,
|
|
24829
|
+
error: routerAbStepUpError(403, "unauthorized", "Recorded owner approval is unavailable")
|
|
24830
|
+
};
|
|
24831
|
+
const activeMaterial = await input.resolveEd25519MaterialActivation({
|
|
24832
|
+
walletId,
|
|
24833
|
+
materialActivation: input.scope.material_activation
|
|
24834
|
+
});
|
|
24835
|
+
if (!activeMaterial.ok) return walletSessionScopeInvalidFailure();
|
|
24836
|
+
return authenticateRouterAbWalletOperationStepUpIdentity({
|
|
24837
|
+
kind: "verified_owner_proof",
|
|
24838
|
+
headers: input.headers,
|
|
24839
|
+
walletId,
|
|
24840
|
+
materialOwner: input.scope.material_activation.material_owner,
|
|
24841
|
+
operationId: recorded.operation.operation.operationId,
|
|
24842
|
+
authority: recorded.authority,
|
|
24843
|
+
runtimePolicyScope: activeMaterial.runtimePolicyScope,
|
|
24844
|
+
expiresAtMs: recorded.expiresAtMs,
|
|
24845
|
+
authorizedOperations: input.authorizedOperations
|
|
24846
|
+
});
|
|
24847
|
+
}
|
|
24552
24848
|
return authenticateRouterAbWalletOperationStepUpIdentity({
|
|
24553
24849
|
kind: "wallet_session_operation_credential_v1",
|
|
24554
24850
|
headers: input.headers,
|
|
@@ -26694,7 +26990,7 @@ function parsePasskeyLoginVerifyRequest(input) {
|
|
|
26694
26990
|
if (!keys.ok) return keys;
|
|
26695
26991
|
const challengeId = requireTrimmedField(body.request, "challengeId");
|
|
26696
26992
|
if (!challengeId.ok) return challengeId;
|
|
26697
|
-
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$
|
|
26993
|
+
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$2(body.request.webauthn_authentication);
|
|
26698
26994
|
if (!webauthnAuthentication) return invalidAuthBody("webauthn_authentication is required");
|
|
26699
26995
|
const expectedOrigin = normalizeOptionalTrimmedString(input.origin);
|
|
26700
26996
|
if (!expectedOrigin) return invalidAuthBody("Origin header is required");
|
|
@@ -26739,7 +27035,7 @@ function parseGoogleLoginVerifyRequest(raw) {
|
|
|
26739
27035
|
function parsePasskeyStepUpRequest(input) {
|
|
26740
27036
|
const challengeId = requireTrimmedField(input.body, "step_up_challenge_id");
|
|
26741
27037
|
if (!challengeId.ok) return challengeId;
|
|
26742
|
-
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$
|
|
27038
|
+
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$2(input.body.webauthn_authentication);
|
|
26743
27039
|
if (!webauthnAuthentication) return invalidAuthBody("webauthn_authentication is required for step-up");
|
|
26744
27040
|
const expectedOrigin = normalizeOptionalTrimmedString(input.origin);
|
|
26745
27041
|
if (!expectedOrigin) return invalidAuthBody("Origin header is required");
|
|
@@ -32656,7 +32952,7 @@ var CloudflareD1WebAuthnAuthService = class {
|
|
|
32656
32952
|
const rpId = parseWebAuthnRpId(input.rpId);
|
|
32657
32953
|
const expectedChallenge = normalizeOptionalTrimmedString(input.expectedChallenge);
|
|
32658
32954
|
const expectedOrigin = normalizeOptionalTrimmedString(input.expected_origin);
|
|
32659
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
32955
|
+
const credential = parseWebAuthnAuthenticationCredential$2(input.webauthn_authentication);
|
|
32660
32956
|
if (!userId.ok) return {
|
|
32661
32957
|
success: false,
|
|
32662
32958
|
verified: false,
|
|
@@ -32796,7 +33092,7 @@ var CloudflareD1WebAuthnAuthService = class {
|
|
|
32796
33092
|
if (!expectedOrigin) return failedVerification("invalid_body", "expected_origin is required for WebAuthn authentication verification");
|
|
32797
33093
|
const rpId = parseWebAuthnRpId(challenge.rpId);
|
|
32798
33094
|
if (!rpId.ok) return failedVerification("internal", `Stored login challenge rpId is invalid: ${rpId.error.message}`);
|
|
32799
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
33095
|
+
const credential = parseWebAuthnAuthenticationCredential$2(input.webauthn_authentication);
|
|
32800
33096
|
if (!credential) return failedVerification("invalid_body", "Missing webauthn_authentication");
|
|
32801
33097
|
const credentialId = webAuthnCredentialIdB64uFromCredential(credential);
|
|
32802
33098
|
if (!credentialId.ok) return failedVerification(credentialId.code, credentialId.message);
|
|
@@ -32837,7 +33133,7 @@ var CloudflareD1WebAuthnAuthService = class {
|
|
|
32837
33133
|
try {
|
|
32838
33134
|
const challengeId = normalizeOptionalTrimmedString(input.challengeId ?? input.challenge_id);
|
|
32839
33135
|
if (!challengeId) return failure$6("invalid_body", "Missing challengeId");
|
|
32840
|
-
const credential = parseWebAuthnAuthenticationCredential$
|
|
33136
|
+
const credential = parseWebAuthnAuthenticationCredential$2(input.webauthn_authentication);
|
|
32841
33137
|
if (!credential) return failedVerification("invalid_body", "Missing webauthn_authentication");
|
|
32842
33138
|
const credentialId = webAuthnCredentialIdB64uFromCredential(credential);
|
|
32843
33139
|
if (!credentialId.ok) return failedVerification(credentialId.code, credentialId.message);
|
|
@@ -39007,7 +39303,7 @@ function parsePasskeyExportAdmissionAuthorization(authorization) {
|
|
|
39007
39303
|
ok: false,
|
|
39008
39304
|
message: `passkey export authorization has unknown field: ${unexpectedField}`
|
|
39009
39305
|
};
|
|
39010
|
-
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$
|
|
39306
|
+
const webauthnAuthentication = parseWebAuthnAuthenticationCredential$2(authorization.webauthnAuthentication);
|
|
39011
39307
|
if (!webauthnAuthentication) return {
|
|
39012
39308
|
ok: false,
|
|
39013
39309
|
message: "webauthnAuthentication is invalid"
|