@xkova/sdk 0.1.0-alpha.1 → 0.1.0-alpha.10
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 +324 -32
- package/dist/abi.d.ts +40 -0
- package/dist/abi.js +25 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +62 -3
- package/dist/generated/types.d.ts +8629 -5182
- package/dist/held-approval.d.ts +41 -0
- package/dist/held-approval.js +33 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/member-action-intents.js +2 -2
- package/dist/member-bff.d.ts +60 -21
- package/dist/member-bff.js +81 -79
- package/dist/member-resources.d.ts +67 -1
- package/dist/member-resources.js +9 -5
- package/dist/request-signing.js +6 -0
- package/dist/resources/account-holders.d.ts +131 -1
- package/dist/resources/account-holders.js +44 -9
- package/dist/resources/branding.d.ts +10 -0
- package/dist/resources/branding.js +2 -0
- package/dist/resources/contacts.d.ts +2 -2
- package/dist/resources/contacts.js +3 -3
- package/dist/resources/escrows.d.ts +3 -0
- package/dist/resources/payment-requests.d.ts +4 -0
- package/dist/resources/payments.d.ts +13 -0
- package/dist/resources/payments.js +7 -0
- package/dist/resources/ramps.d.ts +73 -64
- package/dist/resources/ramps.js +2 -2
- package/dist/resources/rwa.d.ts +9 -9
- package/dist/resources/tenant.d.ts +15 -0
- package/dist/resources/tenant.js +6 -1
- package/dist/resources/tokenization.d.ts +30 -11
- package/dist/resources/tokenization.js +10 -10
- package/dist/resources/treasury.d.ts +404 -0
- package/dist/resources/treasury.js +115 -0
- package/dist/resources/wallets.d.ts +2 -0
- package/dist/resources/webhooks.d.ts +1 -1
- package/dist/resources/webhooks.js +2 -2
- package/dist/resources/workflows.d.ts +7 -6
- package/dist/resources/workflows.js +87 -24
- package/dist/resources/workforce-wallets.d.ts +34 -0
- package/dist/resources/workforce-wallets.js +19 -0
- package/dist/url.d.ts +1 -0
- package/dist/url.js +4 -0
- package/dist/webhook.js +1 -1
- package/dist/workforce-bff.d.ts +202 -0
- package/dist/workforce-bff.js +462 -0
- package/examples/postgres-checkpoint-store.mjs +116 -0
- package/examples/postgres-checkpoints.sql +9 -0
- package/llms.txt +29 -9
- package/package.json +8 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow a governance hold before reading a mutation's entity result.
|
|
3
|
+
*
|
|
4
|
+
* On a workspace with a seeded/configured governance requirement, a
|
|
5
|
+
* create-shaped mutation (for example, `createTreasuryCounterparty` or
|
|
6
|
+
* treasury signer changes) may not execute: it returns a `HeldApproval`
|
|
7
|
+
* (`{ status: 'pending_approval', pending_approval_request_id }`) instead of
|
|
8
|
+
* the entity. No entity is created. Independently eligible approvers complete
|
|
9
|
+
* the required approval, then the same call is reissued through normal policy.
|
|
10
|
+
*
|
|
11
|
+
* Narrow BEFORE reading entity fields:
|
|
12
|
+
*
|
|
13
|
+
* const res = await client.treasury.createCounterparty(body, opts);
|
|
14
|
+
* if (isHeldApproval(res)) {
|
|
15
|
+
* await waitForApproval(res.pending_approval_request_id); // your flow
|
|
16
|
+
* return client.treasury.createCounterparty(body, opts); // same request
|
|
17
|
+
* }
|
|
18
|
+
* res.id; // safely the entity now
|
|
19
|
+
*
|
|
20
|
+
* This helper grants no approval, signer authority or permission to retry a
|
|
21
|
+
* changed request. Existing entity-shaped responses are not governance holds.
|
|
22
|
+
*/
|
|
23
|
+
import type { components } from './generated/types.js';
|
|
24
|
+
export type HeldApproval = components['schemas']['HeldApproval'];
|
|
25
|
+
/** True when a mutation response is a governance hold rather than the entity. */
|
|
26
|
+
export declare function isHeldApproval(res: unknown): res is HeldApproval;
|
|
27
|
+
/**
|
|
28
|
+
* Thrown by `assertNotHeld` when a mutation was governance-held. Carries the
|
|
29
|
+
* approval request id so a script that does not implement the attest flow
|
|
30
|
+
* fails loudly with the pointer a human needs.
|
|
31
|
+
*/
|
|
32
|
+
export declare class XKOVAHeldApprovalError extends Error {
|
|
33
|
+
readonly pendingApprovalRequestId: string;
|
|
34
|
+
constructor(held: HeldApproval, action?: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Unwrap a `entity | HeldApproval` union for callers that treat a hold as
|
|
38
|
+
* exceptional (scripts, one-shot provisioning). Returns the entity, or throws
|
|
39
|
+
* `XKOVAHeldApprovalError` carrying the approval request id.
|
|
40
|
+
*/
|
|
41
|
+
export declare function assertNotHeld<T>(res: T | HeldApproval, action?: string): T;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** True when a mutation response is a governance hold rather than the entity. */
|
|
2
|
+
export function isHeldApproval(res) {
|
|
3
|
+
return (typeof res === 'object' &&
|
|
4
|
+
res !== null &&
|
|
5
|
+
res.status === 'pending_approval' &&
|
|
6
|
+
typeof res
|
|
7
|
+
.pending_approval_request_id === 'string');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thrown by `assertNotHeld` when a mutation was governance-held. Carries the
|
|
11
|
+
* approval request id so a script that does not implement the attest flow
|
|
12
|
+
* fails loudly with the pointer a human needs.
|
|
13
|
+
*/
|
|
14
|
+
export class XKOVAHeldApprovalError extends Error {
|
|
15
|
+
pendingApprovalRequestId;
|
|
16
|
+
constructor(held, action) {
|
|
17
|
+
super(`${action ?? 'The mutation'} was held for governance approval ` +
|
|
18
|
+
`(approval request ${held.pending_approval_request_id}). Attest the ` +
|
|
19
|
+
`request, then re-issue the same call.`);
|
|
20
|
+
this.name = 'XKOVAHeldApprovalError';
|
|
21
|
+
this.pendingApprovalRequestId = held.pending_approval_request_id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Unwrap a `entity | HeldApproval` union for callers that treat a hold as
|
|
26
|
+
* exceptional (scripts, one-shot provisioning). Returns the entity, or throws
|
|
27
|
+
* `XKOVAHeldApprovalError` carrying the approval request id.
|
|
28
|
+
*/
|
|
29
|
+
export function assertNotHeld(res, action) {
|
|
30
|
+
if (isHeldApproval(res))
|
|
31
|
+
throw new XKOVAHeldApprovalError(res, action);
|
|
32
|
+
return res;
|
|
33
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { generateIdempotencyKey, withIdempotency } from './idempotency.js';
|
|
|
5
5
|
export { withRetry, type RetryPolicy } from './retry.js';
|
|
6
6
|
export { collectCursor, paginateCursor, type CursorPage, type PaginateCursorOptions, } from './cursor.js';
|
|
7
7
|
export { XKOVAApiError, XKOVAUploadError } from './errors.js';
|
|
8
|
+
export { isHeldApproval, assertNotHeld, XKOVAHeldApprovalError, type HeldApproval, } from './held-approval.js';
|
|
8
9
|
export { type PollOptions } from './resources/workflows.js';
|
|
9
10
|
export { type XKOVAMemberSessionContext } from './resources/account-holders.js';
|
|
10
11
|
export { verifyWebhookSignature, type VerifyWebhookSignatureResult } from './webhook.js';
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { generateIdempotencyKey, withIdempotency } from './idempotency.js';
|
|
|
4
4
|
export { withRetry } from './retry.js';
|
|
5
5
|
export { collectCursor, paginateCursor, } from './cursor.js';
|
|
6
6
|
export { XKOVAApiError, XKOVAUploadError } from './errors.js';
|
|
7
|
+
export { isHeldApproval, assertNotHeld, XKOVAHeldApprovalError, } from './held-approval.js';
|
|
7
8
|
export { verifyWebhookSignature } from './webhook.js';
|
|
8
9
|
export { verifyInclusion, verifyConsistency, verifySth, } from './rwa-verify.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isAllowedProtocol } from './url.js';
|
|
2
3
|
export class XKOVAMemberIntentError extends Error {
|
|
3
4
|
code;
|
|
4
5
|
status;
|
|
@@ -33,8 +34,7 @@ export class XKOVAMemberActionIntents {
|
|
|
33
34
|
location.username ||
|
|
34
35
|
location.password ||
|
|
35
36
|
location.hash ||
|
|
36
|
-
(location
|
|
37
|
-
!(location.protocol === 'http:' && location.hostname === 'localhost')))
|
|
37
|
+
!isAllowedProtocol(location))
|
|
38
38
|
throw new TypeError('Action review requires the exact application origin');
|
|
39
39
|
const snapshot = JSON.parse(canonicalJson({
|
|
40
40
|
request: input.request,
|
package/dist/member-bff.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Respo
|
|
|
8
8
|
export type XKOVAMemberProofKind = 'xkova_hosted' | 'oidc' | 'signed_launch';
|
|
9
9
|
export type XKOVAMemberProofBindingMode = 'authorization_nonce' | 'server_assertion_jti';
|
|
10
10
|
export type XKOVAAccountCenterIntent = 'security' | 'sessions' | 'wallet_readiness' | 'action';
|
|
11
|
+
/** Direct Account Center starts do not create resource-bound signing authority. */
|
|
12
|
+
export type XKOVAAccountCenterStartIntent = Exclude<XKOVAAccountCenterIntent, 'action'>;
|
|
11
13
|
export type XKOVAMemberActionResourceType = 'payment' | 'escrow' | 'approval_request';
|
|
12
14
|
export type XKOVAMemberActionName = 'sign' | 'create' | 'cancel' | 'attest';
|
|
13
15
|
export interface XKOVAMemberBffClientOptions {
|
|
@@ -34,6 +36,14 @@ export interface XKOVAMemberSessionMaterial extends XKOVAMemberContext {
|
|
|
34
36
|
/** Confidential refresh credential, BFF storage only. */
|
|
35
37
|
readonly refreshToken: string;
|
|
36
38
|
}
|
|
39
|
+
export interface XKOVAExternalMemberEnrollment {
|
|
40
|
+
readonly accountHolderId: string;
|
|
41
|
+
readonly applicationId: string;
|
|
42
|
+
readonly publishableKey: string;
|
|
43
|
+
readonly applicationConfigVersion: number;
|
|
44
|
+
readonly providerNamespace: string;
|
|
45
|
+
readonly created: boolean;
|
|
46
|
+
}
|
|
37
47
|
export interface XKOVABrowserMemberProjection {
|
|
38
48
|
readonly signedIn: true;
|
|
39
49
|
readonly accountHolderId: string;
|
|
@@ -49,8 +59,12 @@ export interface StoredXKOVAMemberSession<TApplicationData = unknown> extends XK
|
|
|
49
59
|
}
|
|
50
60
|
/**
|
|
51
61
|
* `runExclusive` must serialize refresh and logout for the same reference
|
|
52
|
-
* across all BFF instances. Use a
|
|
53
|
-
* a process-local mutex cannot coordinate multiple instances.
|
|
62
|
+
* across all BFF instances. Use a distributed or session-level database lock;
|
|
63
|
+
* a process-local mutex cannot coordinate multiple instances. This lock is not
|
|
64
|
+
* a surrounding rollback transaction: every awaited put/replace/delete must be
|
|
65
|
+
* durable before resolving and must survive a later error from work(). In
|
|
66
|
+
* particular, a pending refresh-attempt checkpoint cannot roll back after the
|
|
67
|
+
* provider/API has acted. replace is exact-existing-only, never an upsert.
|
|
54
68
|
*/
|
|
55
69
|
export interface XKOVAMemberSessionStore<TApplicationData = unknown> {
|
|
56
70
|
put(reference: string, session: StoredXKOVAMemberSession<TApplicationData>): Promise<void>;
|
|
@@ -116,7 +130,7 @@ export type XKOVAMemberAuthorizationTransaction<TApplicationData = unknown> = {
|
|
|
116
130
|
readonly sessionReference: string;
|
|
117
131
|
readonly intent: XKOVAAccountCenterIntent;
|
|
118
132
|
readonly actionAuthorization?: {
|
|
119
|
-
readonly method: 'hosted_passkey';
|
|
133
|
+
readonly method: 'hosted_passkey' | 'xkova_webauthn';
|
|
120
134
|
} | {
|
|
121
135
|
readonly method: 'external_oidc';
|
|
122
136
|
readonly actionToken: string;
|
|
@@ -169,6 +183,11 @@ export type XKOVAMemberAuthorizationTransaction<TApplicationData = unknown> = {
|
|
|
169
183
|
* every BFF instance. `replace` and `delete` must target that exact state. A
|
|
170
184
|
* production store must encrypt the transaction because it can briefly hold a
|
|
171
185
|
* confidential provider proof while XKOVA session exchange is retried.
|
|
186
|
+
* Every awaited write must commit independently of a later work() exception.
|
|
187
|
+
* Never wrap the entire callback in a rollback transaction: that can erase the
|
|
188
|
+
* staged proof after its one-time provider code has already been consumed.
|
|
189
|
+
* On uncertain write acknowledgement, reconcile the same exact record; never
|
|
190
|
+
* re-exchange the provider code or invent a replacement operation identity.
|
|
172
191
|
*/
|
|
173
192
|
export interface XKOVAMemberAuthorizationTransactionStore<TApplicationData = unknown> {
|
|
174
193
|
put(stateHash: string, transaction: XKOVAMemberAuthorizationTransaction<TApplicationData>): Promise<void>;
|
|
@@ -198,6 +217,10 @@ export type XKOVAMemberProofExchangeTransaction<TApplicationData = unknown> = {
|
|
|
198
217
|
* Durable transaction store for a confidential proof exchange that does not
|
|
199
218
|
* use the browser callback helper. Pending records contain a provider proof
|
|
200
219
|
* and customer application data, so production adapters must encrypt them.
|
|
220
|
+
* Serialize the exact reference across replicas without a surrounding rollback
|
|
221
|
+
* transaction. Awaited put/replace/delete checkpoints must remain committed if
|
|
222
|
+
* the later exchange fails. Reconcile an uncertain acknowledgement against the
|
|
223
|
+
* same reference and attempt identity before retrying.
|
|
201
224
|
*/
|
|
202
225
|
export interface XKOVAMemberProofExchangeTransactionStore<TApplicationData = unknown> {
|
|
203
226
|
put(exchangeHash: string, transaction: XKOVAMemberProofExchangeTransaction<TApplicationData>): Promise<void>;
|
|
@@ -239,7 +262,7 @@ export interface XKOVAAccountCenterHandoffCompletion {
|
|
|
239
262
|
readonly applicationReturnUri?: string;
|
|
240
263
|
}
|
|
241
264
|
export type XKOVAMemberActionAuthorizationStart = {
|
|
242
|
-
readonly authorizationMethod: 'hosted_passkey';
|
|
265
|
+
readonly authorizationMethod: 'hosted_passkey' | 'xkova_webauthn';
|
|
243
266
|
readonly authorizationUrl: string;
|
|
244
267
|
readonly state: string;
|
|
245
268
|
readonly expiresAt: string;
|
|
@@ -321,6 +344,18 @@ export declare class XKOVAMemberBffClient {
|
|
|
321
344
|
exchangeProof(proofToken: string, options: {
|
|
322
345
|
readonly attemptId: string;
|
|
323
346
|
}): Promise<XKOVAMemberSessionMaterial>;
|
|
347
|
+
/**
|
|
348
|
+
* BFF-only membership provisioning after validating the provider identity or
|
|
349
|
+
* resolving it from a trusted directory. This creates no login session or
|
|
350
|
+
* wallet and does not substitute for genuine provider-proof verification.
|
|
351
|
+
* Save one idempotencyKey before the first attempt and retain the same subject,
|
|
352
|
+
* selector and observed configuration version during response-loss recovery.
|
|
353
|
+
*/
|
|
354
|
+
enrollExternalMember(input: {
|
|
355
|
+
readonly externalSub: string;
|
|
356
|
+
readonly applicationConfigVersion: number;
|
|
357
|
+
readonly idempotencyKey: string;
|
|
358
|
+
}): Promise<XKOVAExternalMemberEnrollment>;
|
|
324
359
|
refresh(refreshToken: string, options: {
|
|
325
360
|
readonly attemptId: string;
|
|
326
361
|
}): Promise<XKOVAMemberSessionMaterial>;
|
|
@@ -350,20 +385,6 @@ export declare class XKOVAMemberBffClient {
|
|
|
350
385
|
readonly redirectUri: string;
|
|
351
386
|
readonly transactionStore: XKOVAMemberAuthorizationTransactionStore<TApplicationData>;
|
|
352
387
|
}): Promise<XKOVAHostedMemberAuthorizationStart>;
|
|
353
|
-
/**
|
|
354
|
-
* Available only to sandbox applications enabled for demo sign-in. The stored
|
|
355
|
-
* Hosted transaction determines the demo account and callback. An uncertain
|
|
356
|
-
* response preserves the key so a retry recovers the same reservation.
|
|
357
|
-
* Complete the returned callback using
|
|
358
|
-
* completeHostedAuthorization and the original browser flow cookie.
|
|
359
|
-
*/
|
|
360
|
-
completeSandboxDemoAuthorization<TApplicationData>(input: {
|
|
361
|
-
readonly state: string;
|
|
362
|
-
readonly transactionStore: XKOVAMemberAuthorizationTransactionStore<TApplicationData>;
|
|
363
|
-
}): Promise<{
|
|
364
|
-
readonly callbackUrl: string;
|
|
365
|
-
readonly expiresAt: string;
|
|
366
|
-
}>;
|
|
367
388
|
/**
|
|
368
389
|
* Completes the one-time Hosted Auth result-code exchange and persists the
|
|
369
390
|
* provider-neutral XKOVA member session. The returned projection is safe for
|
|
@@ -385,7 +406,7 @@ export declare class XKOVAMemberBffClient {
|
|
|
385
406
|
* attempt remain in the customer BFF stores.
|
|
386
407
|
*/
|
|
387
408
|
beginAccountCenterHandoff<TApplicationData>(input: {
|
|
388
|
-
readonly intent:
|
|
409
|
+
readonly intent: XKOVAAccountCenterStartIntent;
|
|
389
410
|
readonly browserOrigin: string;
|
|
390
411
|
readonly returnUri: string;
|
|
391
412
|
/** Exact-origin application screen to restore, persisted only by the BFF. */
|
|
@@ -439,7 +460,22 @@ export declare class XKOVAMemberBffClient {
|
|
|
439
460
|
readonly expectedState: string;
|
|
440
461
|
readonly transactionStore: XKOVAMemberAuthorizationTransactionStore<TApplicationData>;
|
|
441
462
|
readonly sessionStore: XKOVAMemberSessionStore<TApplicationData>;
|
|
463
|
+
/**
|
|
464
|
+
* Recover an already consumed provider code durably if this process stops or staging fails.
|
|
465
|
+
* XKOVA stages the returned proof before its own exchange, but cannot atomically commit a provider result.
|
|
466
|
+
*/
|
|
442
467
|
readonly exchangeProviderCode: (input: XKOVAMemberProviderCodeExchangeInput) => XKOVAMemberProviderCodeExchangeResult | Promise<XKOVAMemberProviderCodeExchangeResult>;
|
|
468
|
+
/**
|
|
469
|
+
* Optional OIDC enrollment, invoked only after the validated provider proof
|
|
470
|
+
* is durably staged. Validate/extract its exact subject on your backend and
|
|
471
|
+
* call enrollExternalMember with this stable idempotency key. Do not perform
|
|
472
|
+
* enrollment RPC inside exchangeProviderCode: failure there can otherwise
|
|
473
|
+
* require reusing an already-consumed provider authorization code.
|
|
474
|
+
*/
|
|
475
|
+
readonly ensureMemberEnrollment?: (input: {
|
|
476
|
+
readonly proofToken: string;
|
|
477
|
+
readonly idempotencyKey: string;
|
|
478
|
+
}) => Promise<XKOVAExternalMemberEnrollment>;
|
|
443
479
|
readonly applicationData: TApplicationData;
|
|
444
480
|
readonly applicationSessionSeconds?: number;
|
|
445
481
|
}): Promise<EstablishedXKOVAMemberSession<TApplicationData>>;
|
|
@@ -469,6 +505,11 @@ export declare class XKOVAMemberBffClient {
|
|
|
469
505
|
resolveResources<TApplicationData>(input: {
|
|
470
506
|
readonly reference: string;
|
|
471
507
|
readonly sessionStore: XKOVAMemberSessionStore<TApplicationData>;
|
|
508
|
+
/**
|
|
509
|
+
* Defaults true. Set false only when the bound resource request executes
|
|
510
|
+
* immediately and therefore performs the same exact-session validation.
|
|
511
|
+
*/
|
|
512
|
+
readonly validateWithXkova?: boolean;
|
|
472
513
|
}): Promise<{
|
|
473
514
|
readonly session: StoredXKOVAMemberSession<TApplicationData>;
|
|
474
515
|
readonly resources: XKOVAMemberResources;
|
|
@@ -492,8 +533,6 @@ export declare class XKOVAMemberBffClient {
|
|
|
492
533
|
private readSessionMaterial;
|
|
493
534
|
private requestSignal;
|
|
494
535
|
}
|
|
495
|
-
/** Only confirmed admission rejections may release a customer's pending demo reservation. */
|
|
496
|
-
export declare function isDefinitiveXKOVASandboxDemoAdmissionFailure(error: unknown): boolean;
|
|
497
536
|
export declare function projectXKOVAMemberSession(session: XKOVAMemberContext): XKOVABrowserMemberProjection;
|
|
498
537
|
export declare function serializeXKOVAMemberSessionCookie(input: {
|
|
499
538
|
readonly cookieName: string;
|
package/dist/member-bff.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { XKOVAApiError } from './errors.js';
|
|
3
|
+
import { isAllowedProtocol } from './url.js';
|
|
3
4
|
import { XKOVAClient } from './client.js';
|
|
4
5
|
import { XKOVAMemberResources } from './member-resources.js';
|
|
5
6
|
import { XKOVAMemberActionIntents, } from './member-action-intents.js';
|
|
@@ -78,6 +79,48 @@ export class XKOVAMemberBffClient {
|
|
|
78
79
|
proof_token: proofToken,
|
|
79
80
|
}), 'proof exchange');
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* BFF-only membership provisioning after validating the provider identity or
|
|
84
|
+
* resolving it from a trusted directory. This creates no login session or
|
|
85
|
+
* wallet and does not substitute for genuine provider-proof verification.
|
|
86
|
+
* Save one idempotencyKey before the first attempt and retain the same subject,
|
|
87
|
+
* selector and observed configuration version during response-loss recovery.
|
|
88
|
+
*/
|
|
89
|
+
async enrollExternalMember(input) {
|
|
90
|
+
if (typeof input.externalSub !== 'string' ||
|
|
91
|
+
input.externalSub.length === 0 ||
|
|
92
|
+
input.externalSub.length > 512 ||
|
|
93
|
+
/\u0000/u.test(input.externalSub) ||
|
|
94
|
+
!/\S/u.test(input.externalSub))
|
|
95
|
+
throw new TypeError('externalSub must be an exact nonempty provider subject of at most 512 characters');
|
|
96
|
+
assertPositiveInteger(input.applicationConfigVersion, 'applicationConfigVersion');
|
|
97
|
+
if (!/^[A-Za-z0-9._:-]{1,255}$/u.test(input.idempotencyKey))
|
|
98
|
+
throw new TypeError('idempotencyKey must be a retained opaque operation key of at most 255 characters');
|
|
99
|
+
const value = await this.recoverablePostJson('/member-auth/enrollments', {
|
|
100
|
+
publishable_key: this.options.publishableKey,
|
|
101
|
+
application_config_version: input.applicationConfigVersion,
|
|
102
|
+
external_sub: input.externalSub,
|
|
103
|
+
}, undefined, input.idempotencyKey);
|
|
104
|
+
const result = requireObject(value, 'external member enrollment');
|
|
105
|
+
const applicationId = requireString(result, 'application_id', 'external member enrollment');
|
|
106
|
+
assertUuid(applicationId, 'enrollment application id');
|
|
107
|
+
const publishableKey = requireString(result, 'publishable_key', 'external member enrollment');
|
|
108
|
+
const applicationConfigVersion = requirePositiveInteger(result, 'application_config_version', 'external member enrollment');
|
|
109
|
+
const providerNamespace = requireString(result, 'provider_namespace', 'external member enrollment');
|
|
110
|
+
if (publishableKey !== this.options.publishableKey ||
|
|
111
|
+
applicationConfigVersion !== input.applicationConfigVersion ||
|
|
112
|
+
!providerNamespace.startsWith('oidc:') ||
|
|
113
|
+
typeof result.created !== 'boolean')
|
|
114
|
+
throw new Error('XKOVA external member enrollment returned a different application or configuration');
|
|
115
|
+
return {
|
|
116
|
+
accountHolderId: requireString(result, 'account_holder_id', 'external member enrollment'),
|
|
117
|
+
applicationId,
|
|
118
|
+
publishableKey,
|
|
119
|
+
applicationConfigVersion,
|
|
120
|
+
providerNamespace,
|
|
121
|
+
created: result.created,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
81
124
|
async refresh(refreshToken, options) {
|
|
82
125
|
if (refreshToken.length < 20) {
|
|
83
126
|
throw new TypeError('member refresh token is malformed');
|
|
@@ -195,54 +238,6 @@ export class XKOVAMemberBffClient {
|
|
|
195
238
|
throw error;
|
|
196
239
|
}
|
|
197
240
|
}
|
|
198
|
-
/**
|
|
199
|
-
* Available only to sandbox applications enabled for demo sign-in. The stored
|
|
200
|
-
* Hosted transaction determines the demo account and callback. An uncertain
|
|
201
|
-
* response preserves the key so a retry recovers the same reservation.
|
|
202
|
-
* Complete the returned callback using
|
|
203
|
-
* completeHostedAuthorization and the original browser flow cookie.
|
|
204
|
-
*/
|
|
205
|
-
async completeSandboxDemoAuthorization(input) {
|
|
206
|
-
if (!this.options.apiKey.startsWith('xkv_test_') ||
|
|
207
|
-
!this.options.publishableKey.startsWith('xkova_pk_sandbox_')) {
|
|
208
|
-
throw new XKOVAMemberAuthorizationTerminalError('provider_rejected', 'Demo admission requires a sandbox application');
|
|
209
|
-
}
|
|
210
|
-
const stateHash = hashOpaque(input.state);
|
|
211
|
-
return input.transactionStore.runExclusive(stateHash, async () => {
|
|
212
|
-
const transaction = await input.transactionStore.get(stateHash);
|
|
213
|
-
if (!transaction || transaction.expiresAt <= Date.now()) {
|
|
214
|
-
throw new XKOVAMemberAuthorizationTerminalError('transaction_unavailable', 'Demo admission requires an active Hosted transaction');
|
|
215
|
-
}
|
|
216
|
-
assertMemberBffClientFingerprint(transaction.clientFingerprint, this.clientFingerprint);
|
|
217
|
-
if (transaction.status !== 'pending' ||
|
|
218
|
-
transaction.completionMode !== 'hosted_result_code' ||
|
|
219
|
-
!transaction.authorizationUrl) {
|
|
220
|
-
throw new XKOVAMemberAuthorizationTerminalError('transaction_unavailable', 'Demo admission requires a completed Hosted start');
|
|
221
|
-
}
|
|
222
|
-
const authorization = new URL(normalizeHostedAuthorizationUrl(transaction.authorizationUrl));
|
|
223
|
-
const capability = authorization.searchParams.get('transaction');
|
|
224
|
-
if (!/^\/authorize\/?$/.test(authorization.pathname) ||
|
|
225
|
-
!capability ||
|
|
226
|
-
!/^[A-Za-z0-9_-]{43}$/.test(capability)) {
|
|
227
|
-
throw new XKOVAMemberAuthorizationTerminalError('transaction_unavailable', 'Demo admission capability is invalid');
|
|
228
|
-
}
|
|
229
|
-
const value = await this.postJson('/member-auth/hosted/authorizations/sandbox-demo', { transaction: capability }, undefined, `sandbox-demo-${stateHash}`);
|
|
230
|
-
const body = requireObject(value, 'Sandbox demo admission');
|
|
231
|
-
const { expiresAt } = boundedAuthorizationExpiry(body, transaction.expiresAt, 'Sandbox demo admission');
|
|
232
|
-
const callback = new URL(requireString(body, 'redirect_url', 'Sandbox demo admission'));
|
|
233
|
-
const code = callback.searchParams.get('code');
|
|
234
|
-
if (!sameCallback(callback, new URL(transaction.redirectUri)) ||
|
|
235
|
-
callback.searchParams.getAll('state').length !== 1 ||
|
|
236
|
-
callback.searchParams.get('state') !== input.state ||
|
|
237
|
-
callback.searchParams.getAll('code').length !== 1 ||
|
|
238
|
-
!code ||
|
|
239
|
-
callback.searchParams.has('error')) {
|
|
240
|
-
throw new Error('Sandbox demo admission returned the wrong callback binding');
|
|
241
|
-
}
|
|
242
|
-
assertHostedAuthorizationResultCode(code);
|
|
243
|
-
return { callbackUrl: callback.toString(), expiresAt };
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
241
|
/**
|
|
247
242
|
* Completes the one-time Hosted Auth result-code exchange and persists the
|
|
248
243
|
* provider-neutral XKOVA member session. The returned projection is safe for
|
|
@@ -354,7 +349,7 @@ export class XKOVAMemberBffClient {
|
|
|
354
349
|
async beginAccountCenterHandoff(input) {
|
|
355
350
|
const browserOrigin = normalizeBrowserOrigin(input.browserOrigin);
|
|
356
351
|
const returnUri = normalizeCallbackUri(input.returnUri);
|
|
357
|
-
|
|
352
|
+
assertAccountCenterStartIntent(input.intent);
|
|
358
353
|
const applicationReturnUri = input.applicationReturnUri === undefined
|
|
359
354
|
? undefined
|
|
360
355
|
: normalizeApplicationReturnUri(input.applicationReturnUri, browserOrigin);
|
|
@@ -560,7 +555,7 @@ export class XKOVAMemberBffClient {
|
|
|
560
555
|
clientFingerprint: this.clientFingerprint,
|
|
561
556
|
codeVerifier,
|
|
562
557
|
redirectUri: returnUri,
|
|
563
|
-
exchangeAttemptId:
|
|
558
|
+
exchangeAttemptId: randomUUID(),
|
|
564
559
|
sessionReference: input.sessionReference,
|
|
565
560
|
intent: 'action',
|
|
566
561
|
...(applicationReturnUri !== undefined && { applicationReturnUri }),
|
|
@@ -583,15 +578,15 @@ export class XKOVAMemberBffClient {
|
|
|
583
578
|
const object = requireObject(value, 'member action authorization start');
|
|
584
579
|
const method = requireString(object, 'authorization_method', 'member action authorization start');
|
|
585
580
|
const { expiresAt, expiresAtMillis } = boundedAuthorizationExpiry(object, provisionalExpiresAt, 'member action authorization start');
|
|
586
|
-
if (method === 'hosted_passkey') {
|
|
581
|
+
if (method === 'hosted_passkey' || method === 'xkova_webauthn') {
|
|
587
582
|
const authorizationUrl = normalizeHostedAuthorizationUrl(requireString(object, 'authorization_url', 'member action authorization start'));
|
|
588
583
|
await input.transactionStore.replace(stateHash, {
|
|
589
584
|
...transaction,
|
|
590
|
-
actionAuthorization: { method
|
|
585
|
+
actionAuthorization: { method },
|
|
591
586
|
expiresAt: expiresAtMillis,
|
|
592
587
|
});
|
|
593
588
|
return {
|
|
594
|
-
authorizationMethod:
|
|
589
|
+
authorizationMethod: method,
|
|
595
590
|
authorizationUrl,
|
|
596
591
|
state,
|
|
597
592
|
expiresAt,
|
|
@@ -653,7 +648,7 @@ export class XKOVAMemberBffClient {
|
|
|
653
648
|
if (initial?.completionMode === 'account_center_result_code' &&
|
|
654
649
|
initial.status === 'pending' &&
|
|
655
650
|
initial.intent === 'action' &&
|
|
656
|
-
initial.actionAuthorization?.method
|
|
651
|
+
['hosted_passkey', 'xkova_webauthn'].includes(initial.actionAuthorization?.method ?? '')) {
|
|
657
652
|
const completion = await this.completeAccountCenterHandoff(input);
|
|
658
653
|
if (!completion.signingExchangeCode || !completion.signingUrl) {
|
|
659
654
|
throw new Error('XKOVA Hosted action completion omitted its signing projection');
|
|
@@ -806,6 +801,10 @@ export class XKOVAMemberBffClient {
|
|
|
806
801
|
if (transaction.completionMode !== 'provider_proof') {
|
|
807
802
|
throw new Error('member authorization transaction is not a provider-proof flow');
|
|
808
803
|
}
|
|
804
|
+
if (input.ensureMemberEnrollment &&
|
|
805
|
+
transaction.status === 'pending' &&
|
|
806
|
+
transaction.proofKind !== 'oidc')
|
|
807
|
+
throw new Error('external member enrollment is available only for an OIDC proof flow');
|
|
809
808
|
if (!sameCallback(callback, new URL(transaction.redirectUri))) {
|
|
810
809
|
throw new Error('member authorization callback does not match its redirect URI');
|
|
811
810
|
}
|
|
@@ -868,6 +867,14 @@ export class XKOVAMemberBffClient {
|
|
|
868
867
|
},
|
|
869
868
|
});
|
|
870
869
|
}
|
|
870
|
+
const enrollment = input.ensureMemberEnrollment
|
|
871
|
+
? await input.ensureMemberEnrollment({
|
|
872
|
+
proofToken,
|
|
873
|
+
idempotencyKey: `enroll:${transaction.exchangeAttemptId}`,
|
|
874
|
+
})
|
|
875
|
+
: undefined;
|
|
876
|
+
if (enrollment && enrollment.publishableKey !== this.options.publishableKey)
|
|
877
|
+
throw new Error('member enrollment does not belong to this application');
|
|
871
878
|
const established = await this.persistSessionWithAttempt({
|
|
872
879
|
proofToken,
|
|
873
880
|
attemptId: transaction.exchangeAttemptId,
|
|
@@ -875,6 +882,7 @@ export class XKOVAMemberBffClient {
|
|
|
875
882
|
sessionStore: input.sessionStore,
|
|
876
883
|
applicationData,
|
|
877
884
|
applicationExpiresAt,
|
|
885
|
+
...(enrollment !== undefined && { expectedAccountHolderId: enrollment.accountHolderId }),
|
|
878
886
|
});
|
|
879
887
|
await input.transactionStore.replace(stateHash, {
|
|
880
888
|
status: 'completed',
|
|
@@ -976,6 +984,11 @@ export class XKOVAMemberBffClient {
|
|
|
976
984
|
await input.sessionStore.replace(input.reference, active);
|
|
977
985
|
}
|
|
978
986
|
const rotated = await this.refresh(current.refreshToken, { attemptId });
|
|
987
|
+
if (rotated.sessionId !== current.sessionId ||
|
|
988
|
+
rotated.accountHolderId !== current.accountHolderId ||
|
|
989
|
+
rotated.providerSubject !== current.providerSubject) {
|
|
990
|
+
throw new Error('XKOVA member context changed during refresh');
|
|
991
|
+
}
|
|
979
992
|
const { pendingRefreshAttemptId: _completed, ...withoutPending } = active;
|
|
980
993
|
active = {
|
|
981
994
|
...withoutPending,
|
|
@@ -1061,6 +1074,9 @@ export class XKOVAMemberBffClient {
|
|
|
1061
1074
|
const material = await this.exchangeProof(input.proofToken, {
|
|
1062
1075
|
attemptId: input.attemptId,
|
|
1063
1076
|
});
|
|
1077
|
+
if (input.expectedAccountHolderId !== undefined &&
|
|
1078
|
+
material.accountHolderId !== input.expectedAccountHolderId)
|
|
1079
|
+
throw new Error('XKOVA proof exchange returned a different member from the enrolled identity');
|
|
1064
1080
|
return this.persistSessionMaterial({
|
|
1065
1081
|
material,
|
|
1066
1082
|
reference: input.reference,
|
|
@@ -1111,8 +1127,7 @@ export class XKOVAMemberBffClient {
|
|
|
1111
1127
|
...(idempotencyKey !== undefined && { idempotencyKey }),
|
|
1112
1128
|
});
|
|
1113
1129
|
}
|
|
1114
|
-
async recoverablePostJson(path, body, sessionId) {
|
|
1115
|
-
const idempotencyKey = generateIdempotencyKey();
|
|
1130
|
+
async recoverablePostJson(path, body, sessionId, idempotencyKey = generateIdempotencyKey()) {
|
|
1116
1131
|
try {
|
|
1117
1132
|
return await this.postJson(path, body, sessionId, idempotencyKey);
|
|
1118
1133
|
}
|
|
@@ -1210,14 +1225,6 @@ export class XKOVAMemberBffClient {
|
|
|
1210
1225
|
: AbortSignal.timeout(this.options.timeoutMs);
|
|
1211
1226
|
}
|
|
1212
1227
|
}
|
|
1213
|
-
/** Only confirmed admission rejections may release a customer's pending demo reservation. */
|
|
1214
|
-
export function isDefinitiveXKOVASandboxDemoAdmissionFailure(error) {
|
|
1215
|
-
return (error instanceof XKOVAMemberAuthorizationTerminalError ||
|
|
1216
|
-
(error instanceof XKOVAApiError &&
|
|
1217
|
-
(error.status < 500 ||
|
|
1218
|
-
error.code === 'demo_persona_unavailable' ||
|
|
1219
|
-
error.code === 'demo_login_disabled')));
|
|
1220
|
-
}
|
|
1221
1228
|
export function projectXKOVAMemberSession(session) {
|
|
1222
1229
|
return {
|
|
1223
1230
|
signedIn: true,
|
|
@@ -1339,8 +1346,8 @@ function normalizeApiBaseUrl(baseUrl) {
|
|
|
1339
1346
|
if (url.username || url.password || url.search || url.hash) {
|
|
1340
1347
|
throw new TypeError('XKOVA member BFF baseUrl must be an API origin or /v1 base URL');
|
|
1341
1348
|
}
|
|
1342
|
-
if (
|
|
1343
|
-
throw new TypeError('XKOVA member BFF baseUrl must use HTTPS or
|
|
1349
|
+
if (!isAllowedProtocol(url)) {
|
|
1350
|
+
throw new TypeError('XKOVA member BFF baseUrl must use HTTPS or loopback HTTP');
|
|
1344
1351
|
}
|
|
1345
1352
|
const pathname = url.pathname.replace(/\/+$/, '');
|
|
1346
1353
|
if (pathname !== '' && pathname !== '/v1') {
|
|
@@ -1353,16 +1360,15 @@ function normalizeCallbackUri(value) {
|
|
|
1353
1360
|
if (url.username || url.password || url.search || url.hash) {
|
|
1354
1361
|
throw new TypeError('member authorization redirectUri must not contain credentials, query, or fragment');
|
|
1355
1362
|
}
|
|
1356
|
-
if (
|
|
1357
|
-
throw new TypeError('member authorization redirectUri must use HTTPS or
|
|
1363
|
+
if (!isAllowedProtocol(url)) {
|
|
1364
|
+
throw new TypeError('member authorization redirectUri must use HTTPS or loopback HTTP');
|
|
1358
1365
|
}
|
|
1359
1366
|
return url.toString();
|
|
1360
1367
|
}
|
|
1361
1368
|
function normalizeBrowserOrigin(value) {
|
|
1362
1369
|
const url = new URL(value);
|
|
1363
|
-
if (url.origin !== value ||
|
|
1364
|
-
|
|
1365
|
-
throw new TypeError('member authorization browserOrigin must be an exact HTTPS or localhost origin');
|
|
1370
|
+
if (url.origin !== value || !isAllowedProtocol(url)) {
|
|
1371
|
+
throw new TypeError('member authorization browserOrigin must be an exact HTTPS or loopback origin');
|
|
1366
1372
|
}
|
|
1367
1373
|
return value;
|
|
1368
1374
|
}
|
|
@@ -1372,18 +1378,14 @@ function normalizeApplicationReturnUri(value, browserOrigin) {
|
|
|
1372
1378
|
url.username ||
|
|
1373
1379
|
url.password ||
|
|
1374
1380
|
url.hash ||
|
|
1375
|
-
|
|
1381
|
+
!isAllowedProtocol(url)) {
|
|
1376
1382
|
throw new TypeError('member action applicationReturnUri must use the exact browser origin');
|
|
1377
1383
|
}
|
|
1378
1384
|
return url.toString();
|
|
1379
1385
|
}
|
|
1380
1386
|
function normalizeHostedAuthorizationUrl(value) {
|
|
1381
1387
|
const url = new URL(value);
|
|
1382
|
-
if (url.username ||
|
|
1383
|
-
url.password ||
|
|
1384
|
-
url.hash ||
|
|
1385
|
-
(url.protocol !== 'https:' &&
|
|
1386
|
-
!(url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')))) {
|
|
1388
|
+
if (url.username || url.password || url.hash || !isAllowedProtocol(url)) {
|
|
1387
1389
|
throw new Error('XKOVA member Hosted authorization URL must use HTTPS or loopback HTTP without credentials or a fragment');
|
|
1388
1390
|
}
|
|
1389
1391
|
return url.toString();
|
|
@@ -1518,8 +1520,8 @@ function assertHostedAuthorizationResultCode(value) {
|
|
|
1518
1520
|
throw new TypeError('member Hosted authorization result code is malformed');
|
|
1519
1521
|
}
|
|
1520
1522
|
}
|
|
1521
|
-
function
|
|
1522
|
-
if (!new Set(['security', 'sessions', 'wallet_readiness'
|
|
1523
|
+
function assertAccountCenterStartIntent(value) {
|
|
1524
|
+
if (!new Set(['security', 'sessions', 'wallet_readiness']).has(value)) {
|
|
1523
1525
|
throw new TypeError('Account Center intent is unsupported');
|
|
1524
1526
|
}
|
|
1525
1527
|
}
|