@comfyorg/account-core 1.0.0-alpha.0 → 1.0.0-alpha.2
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/dist/core/billing/billingContracts.d.ts +29 -14
- package/dist/core/billing/billingErrorBody.d.ts +8 -4
- package/dist/core/billing/billingErrorBody.js +14 -4
- package/dist/core/billing/credentialedTransport.d.ts +23 -7
- package/dist/core/billing/credentialedTransport.js +72 -8
- package/dist/core/billing/events.d.ts +47 -0
- package/dist/core/billing/events.js +43 -0
- package/dist/core/billing/index.d.ts +16 -11
- package/dist/core/billing/index.js +8 -6
- package/dist/core/billing/operationLifecycle.d.ts +34 -1
- package/dist/core/billing/operationLifecycle.js +146 -67
- package/dist/core/billing/operationPointer.d.ts +27 -2
- package/dist/core/billing/operationPointer.js +36 -25
- package/dist/core/billing/operationPolicy.d.ts +44 -7
- package/dist/core/billing/operationPolicy.js +64 -3
- package/dist/core/billing/operationState.d.ts +225 -19
- package/dist/core/billing/operationState.js +93 -8
- package/dist/core/billing/paymentCopy.d.ts +6 -1
- package/dist/core/billing/paymentCopy.js +34 -1
- package/dist/core/billing/paymentProjection.d.ts +14 -2
- package/dist/core/billing/paymentProjection.js +37 -9
- package/dist/core/billing/scopedReader.d.ts +11 -5
- package/dist/core/billing/scopedReader.js +20 -9
- package/dist/core/billing/sharedRead.js +4 -2
- package/dist/core/billing/subscriptionCommands.d.ts +63 -9
- package/dist/core/billing/subscriptionCommands.js +46 -30
- package/dist/core/billing/topup.d.ts +26 -0
- package/dist/core/billing/topup.js +36 -2
- package/dist/core/billing/wireCents.d.ts +8 -0
- package/dist/core/billing/wireCents.js +8 -0
- package/dist/core/billing/workspaceInvites.d.ts +12 -0
- package/dist/core/billing/workspaceInvites.js +26 -0
- package/dist/core/identity.d.ts +6 -6
- package/dist/core/identity.js +4 -10
- package/dist/core/lazyIdentity.d.ts +31 -0
- package/dist/core/lazyIdentity.js +96 -0
- package/dist/core/requestAuth.d.ts +30 -0
- package/dist/core/requestAuth.js +41 -0
- package/dist/core/requestTimeout.d.ts +7 -0
- package/dist/core/requestTimeout.js +13 -0
- package/dist/core/session.d.ts +12 -18
- package/dist/core/session.js +16 -26
- package/dist/core/sessionContracts.d.ts +39 -2
- package/dist/core/sessionTokenMint.d.ts +35 -0
- package/dist/core/sessionTokenMint.js +222 -0
- package/dist/core/webSession.d.ts +27 -0
- package/dist/core/webSession.js +144 -0
- package/dist/core/webSessionFlag.d.ts +19 -0
- package/dist/core/webSessionFlag.js +61 -0
- package/dist/core/webSessionIdentity.d.ts +297 -0
- package/dist/core/webSessionIdentity.js +509 -0
- package/dist/firebase/configSource.d.ts +54 -0
- package/dist/firebase/configSource.js +111 -0
- package/dist/firebase/index.d.ts +71 -8
- package/dist/firebase/index.js +236 -23
- package/dist/firebase/popupWatch.d.ts +42 -0
- package/dist/firebase/popupWatch.js +140 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +116 -0
- package/dist/web/crossTabRefresh.d.ts +4 -2
- package/dist/web/crossTabRefresh.js +13 -0
- package/dist/workspaceLink.d.ts +45 -0
- package/dist/workspaceLink.js +78 -0
- package/package.json +30 -6
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
* by design, the same way `sessionContracts` is: the transport and the typed
|
|
4
4
|
* operations depend on this module, never back on each other.
|
|
5
5
|
*
|
|
6
|
-
* Billing reports failures as coded results rather than thrown errors
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
6
|
+
* Billing reports failures as coded results rather than thrown errors. The
|
|
7
|
+
* codes carry no text; the copy that belongs to each code is owned by the
|
|
8
|
+
* billing core and localized by the host. Two server values cross this
|
|
9
|
+
* boundary. `serverCode` is a machine identifier and never copy: a command
|
|
10
|
+
* matches it against its own closed set, and a host stores it where it
|
|
11
|
+
* already keeps `WorkspaceApiError.code`. Its brand makes it unforgeable:
|
|
12
|
+
* only the response decoder mints one, and `unwrapServerCode` names the one
|
|
13
|
+
* sanctioned widening. `serverMessage` is the sentence the server wrote for
|
|
14
|
+
* the customer, carried so a host can show it the way the legacy client does;
|
|
15
|
+
* nothing in the core branches on it.
|
|
15
16
|
*/
|
|
16
17
|
import type { SessionClient } from '../session.js';
|
|
17
18
|
/**
|
|
@@ -38,7 +39,16 @@ export type BillingErrorCode =
|
|
|
38
39
|
/** The identity or workspace changed while the request was in flight. */
|
|
39
40
|
| 'SUPERSEDED' | 'REQUEST_FAILED'
|
|
40
41
|
/** A 2xx whose body does not match the generated contract. */
|
|
41
|
-
| 'MALFORMED_RESPONSE'
|
|
42
|
+
| 'MALFORMED_RESPONSE'
|
|
43
|
+
/**
|
|
44
|
+
* The server already had an operation of this kind pending that this
|
|
45
|
+
* attempt did not issue. Refusing is the only honest answer: the status
|
|
46
|
+
* response names no plan, so joining it would settle whatever the earlier
|
|
47
|
+
* attempt chose and report it as this caller's success. The command
|
|
48
|
+
* usually declines before it sends anything; a subscription command also
|
|
49
|
+
* reports it when the server refuses with `SUBSCRIPTION_CHANGE_IN_PROGRESS`.
|
|
50
|
+
*/
|
|
51
|
+
| 'OPERATION_ALREADY_PENDING';
|
|
42
52
|
declare const billingServerCodeBrand: unique symbol;
|
|
43
53
|
/**
|
|
44
54
|
* An unbounded server string that is only ever a machine identifier. Nothing
|
|
@@ -57,12 +67,17 @@ export type BillingFailure = {
|
|
|
57
67
|
readonly httpStatus?: number;
|
|
58
68
|
/**
|
|
59
69
|
* The coded `code` of a generated `ErrorResponse` body, when the server
|
|
60
|
-
* sent one.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* boundary. Never render it.
|
|
70
|
+
* sent one. It is the only server value a command branches on. Compare it
|
|
71
|
+
* through `matchesServerCode`, and widen it through `unwrapServerCode` at
|
|
72
|
+
* a host's error-store boundary. Never render it.
|
|
64
73
|
*/
|
|
65
74
|
readonly serverCode?: BillingServerCode;
|
|
75
|
+
/**
|
|
76
|
+
* The `message` of that same `ErrorResponse`, present when the server sent
|
|
77
|
+
* a non-empty one. This is what a host shows the customer; the legacy
|
|
78
|
+
* client shows the same string.
|
|
79
|
+
*/
|
|
80
|
+
readonly serverMessage?: string;
|
|
66
81
|
};
|
|
67
82
|
/** Whether a failure carries the server code a command names. */
|
|
68
83
|
export declare function matchesServerCode(failure: Pick<BillingFailure, 'serverCode'>, code: string): boolean;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { BillingServerCode } from './billingContracts.js';
|
|
2
2
|
/**
|
|
3
|
-
* The `code` of a generated `ErrorResponse` body, or nothing.
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* is minted, and the brand it carries is erased at runtime.
|
|
3
|
+
* The `code` of a generated `ErrorResponse` body, or nothing. This decode is
|
|
4
|
+
* the only place a `BillingServerCode` is minted, and the brand it carries is
|
|
5
|
+
* erased at runtime.
|
|
7
6
|
*/
|
|
8
7
|
export declare function readBillingErrorCode(body: unknown): BillingServerCode | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The `message` of a generated `ErrorResponse` body, or nothing when the body
|
|
10
|
+
* is not that contract or the message is blank.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readBillingErrorMessage(body: unknown): string | undefined;
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { zErrorResponse } from '@comfyorg/ingest-types/zod';
|
|
2
2
|
/**
|
|
3
|
-
* The `code` of a generated `ErrorResponse` body, or nothing.
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* is minted, and the brand it carries is erased at runtime.
|
|
3
|
+
* The `code` of a generated `ErrorResponse` body, or nothing. This decode is
|
|
4
|
+
* the only place a `BillingServerCode` is minted, and the brand it carries is
|
|
5
|
+
* erased at runtime.
|
|
7
6
|
*/
|
|
8
7
|
export function readBillingErrorCode(body) {
|
|
9
8
|
const parsed = zErrorResponse.safeParse(body);
|
|
10
9
|
return parsed.success ? parsed.data.code : undefined;
|
|
11
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The `message` of a generated `ErrorResponse` body, or nothing when the body
|
|
13
|
+
* is not that contract or the message is blank.
|
|
14
|
+
*/
|
|
15
|
+
export function readBillingErrorMessage(body) {
|
|
16
|
+
const parsed = zErrorResponse.safeParse(body);
|
|
17
|
+
if (!parsed.success)
|
|
18
|
+
return undefined;
|
|
19
|
+
const message = parsed.data.message.trim();
|
|
20
|
+
return message === '' ? undefined : message;
|
|
21
|
+
}
|
|
@@ -13,14 +13,19 @@
|
|
|
13
13
|
* `authenticationRetrySkipped` stays unset; it means a replayable retry was
|
|
14
14
|
* possible and skipped, which is never the case on this transport.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* `exchangeBillingRequest` always sends: it makes
|
|
18
|
-
* so a cross-site caller has to clear a preflight
|
|
19
|
-
* rests on the cookie's `SameSite` attribute and the
|
|
20
|
-
* allowlist, neither of which is enforced here — sending a
|
|
21
|
-
* otherwise CORS-simple request from this transport removes
|
|
22
|
-
*
|
|
16
|
+
* Without a web session no CSRF header is sent. What stands in for one is
|
|
17
|
+
* the JSON content type `exchangeBillingRequest` always sends: it makes
|
|
18
|
+
* every request non-simple, so a cross-site caller has to clear a preflight
|
|
19
|
+
* it cannot satisfy. That rests on the cookie's `SameSite` attribute and the
|
|
20
|
+
* backend's CORS allowlist, neither of which is enforced here — sending a
|
|
21
|
+
* form-encoded or otherwise CORS-simple request from this transport removes
|
|
22
|
+
* the protection.
|
|
23
|
+
* A host on the shared web session opts in through `webSession`: headers
|
|
24
|
+
* then come from `authorize`, and a `csrf_invalid` answer is retried once
|
|
25
|
+
* after re-reading the session, only while it still belongs to the same user.
|
|
23
26
|
*/
|
|
27
|
+
import type { RequestAuthorizer } from '../requestAuth.js';
|
|
28
|
+
import type { WebSession, WebSessionResult } from '../sessionContracts.js';
|
|
24
29
|
import type { BillingTransport } from './billingContracts.js';
|
|
25
30
|
import type { BillingScopeSource } from './billingScope.js';
|
|
26
31
|
export interface CredentialedBillingTransportOptions {
|
|
@@ -31,5 +36,16 @@ export interface CredentialedBillingTransportOptions {
|
|
|
31
36
|
readonly credentials?: RequestCredentials;
|
|
32
37
|
readonly fetchImpl?: typeof fetch;
|
|
33
38
|
readonly defaultTimeoutMs?: number;
|
|
39
|
+
/** Opt-in cookie-session headers; `credentials` is then ignored. */
|
|
40
|
+
readonly webSession?: CredentialedWebSession;
|
|
41
|
+
}
|
|
42
|
+
export interface CredentialedWebSession {
|
|
43
|
+
readonly authorize: RequestAuthorizer;
|
|
44
|
+
readonly getSession: () => WebSession | undefined;
|
|
45
|
+
/** `readWebSession` bound to the host's ingest options. */
|
|
46
|
+
readonly readSession: (request: {
|
|
47
|
+
readonly expectedUserId: string;
|
|
48
|
+
readonly signal: AbortSignal;
|
|
49
|
+
}) => Promise<WebSessionResult>;
|
|
34
50
|
}
|
|
35
51
|
export declare function createCredentialedBillingTransport(options: CredentialedBillingTransportOptions): BillingTransport;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sameBillingScope } from './billingScope.js';
|
|
2
|
+
import { readBillingErrorCode } from './billingErrorBody.js';
|
|
2
3
|
import { DEFAULT_BILLING_TIMEOUT_MS, exchangeBillingRequest, startRequestBudget } from './transportExchange.js';
|
|
3
4
|
/**
|
|
4
5
|
* Any scope other than the captured one supersedes the response, undefined
|
|
@@ -21,21 +22,84 @@ function markSessionEnded(response) {
|
|
|
21
22
|
return response;
|
|
22
23
|
return { ...response, authenticationNotRenewable: true };
|
|
23
24
|
}
|
|
25
|
+
const SESSION_ENDED = new Set([
|
|
26
|
+
'NO_SESSION',
|
|
27
|
+
'SESSION_EXPIRED',
|
|
28
|
+
'SESSION_REVOKED'
|
|
29
|
+
]);
|
|
30
|
+
function isCsrfInvalid(response) {
|
|
31
|
+
return (response.httpStatus === 403 &&
|
|
32
|
+
readBillingErrorCode(response.body) === 'csrf_invalid');
|
|
33
|
+
}
|
|
34
|
+
function rereadFailure({ code }) {
|
|
35
|
+
if (code === 'IDENTITY_CHANGED')
|
|
36
|
+
return { status: 'error', code: 'SUPERSEDED' };
|
|
37
|
+
return {
|
|
38
|
+
status: 'error',
|
|
39
|
+
code: SESSION_ENDED.has(code) ? 'NOT_AUTHENTICATED' : 'REQUEST_FAILED'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `csrf_invalid` is the one refusal a fresh token can fix, and the server
|
|
44
|
+
* refused before acting, so the replay is safe for a write too. The re-read
|
|
45
|
+
* is pinned to the user the request started as, and the retry to the
|
|
46
|
+
* captured workspace: a changed user abandons the request rather than
|
|
47
|
+
* finishing it as someone else.
|
|
48
|
+
*/
|
|
49
|
+
async function exchangeWithSession(request, webSession, session, context) {
|
|
50
|
+
const sendAs = async (current) => {
|
|
51
|
+
const authorization = await webSession.authorize({ kind: 'session', session: current }, {
|
|
52
|
+
target: 'ingest',
|
|
53
|
+
method: request.method,
|
|
54
|
+
workspaceId: context.workspaceId
|
|
55
|
+
});
|
|
56
|
+
return context.send(authorization);
|
|
57
|
+
};
|
|
58
|
+
const first = await sendAs(session);
|
|
59
|
+
if (first.status === 'error' || !isCsrfInvalid(first.value))
|
|
60
|
+
return first;
|
|
61
|
+
if (!context.stillInScope())
|
|
62
|
+
return { status: 'error', code: 'SUPERSEDED' };
|
|
63
|
+
const reread = await webSession.readSession({
|
|
64
|
+
expectedUserId: session.user.id,
|
|
65
|
+
signal: context.signal
|
|
66
|
+
});
|
|
67
|
+
if (reread.status === 'error')
|
|
68
|
+
return rereadFailure(reread);
|
|
69
|
+
if (!context.stillInScope())
|
|
70
|
+
return { status: 'error', code: 'SUPERSEDED' };
|
|
71
|
+
return sendAs(reread.session);
|
|
72
|
+
}
|
|
24
73
|
export function createCredentialedBillingTransport(options) {
|
|
25
|
-
const { resolveUrl, scopeSource, credentials = 'include', fetchImpl = fetch, defaultTimeoutMs = DEFAULT_BILLING_TIMEOUT_MS } = options;
|
|
74
|
+
const { resolveUrl, scopeSource, credentials = 'include', fetchImpl = fetch, defaultTimeoutMs = DEFAULT_BILLING_TIMEOUT_MS, webSession } = options;
|
|
75
|
+
const exchange = (request, signal, auth) => exchangeBillingRequest(request, {
|
|
76
|
+
fetchImpl,
|
|
77
|
+
url: resolveUrl(request.route),
|
|
78
|
+
signal,
|
|
79
|
+
...auth
|
|
80
|
+
});
|
|
81
|
+
const sendPlain = (request, _captured, signal) => exchange(request, signal, { credentials });
|
|
82
|
+
function sessionSender(session) {
|
|
83
|
+
if (webSession === undefined || session === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
return (request, captured, signal) => exchangeWithSession(request, webSession, session, {
|
|
86
|
+
workspaceId: captured.workspaceId,
|
|
87
|
+
signal,
|
|
88
|
+
stillInScope: () => !movedOutOfScope(scopeSource, captured),
|
|
89
|
+
send: (authorization) => exchange(request, signal, authorization)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
26
92
|
return async function transport(request) {
|
|
27
93
|
const captured = scopeSource.getScope();
|
|
28
|
-
|
|
94
|
+
const send = webSession === undefined
|
|
95
|
+
? sendPlain
|
|
96
|
+
: sessionSender(webSession.getSession());
|
|
97
|
+
if (captured === undefined || send === undefined) {
|
|
29
98
|
return { status: 'error', code: 'NOT_AUTHENTICATED' };
|
|
30
99
|
}
|
|
31
100
|
const budget = startRequestBudget(request.signal, request.timeoutMs ?? defaultTimeoutMs);
|
|
32
101
|
try {
|
|
33
|
-
const response = await
|
|
34
|
-
fetchImpl,
|
|
35
|
-
url: resolveUrl(request.route),
|
|
36
|
-
signal: budget.signal,
|
|
37
|
-
credentials
|
|
38
|
-
});
|
|
102
|
+
const response = await send(request, captured, budget.signal);
|
|
39
103
|
if (response.status === 'error')
|
|
40
104
|
return response;
|
|
41
105
|
if (movedOutOfScope(scopeSource, captured)) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The billing events read: one page of the workspace's history feed, the
|
|
3
|
+
* charges, credits and adjustments behind the usage log.
|
|
4
|
+
*
|
|
5
|
+
* It is the only billing read whose request varies per call, so the page it
|
|
6
|
+
* asks for is part of what identifies the read: two callers on different
|
|
7
|
+
* pages each get their own answer rather than sharing the first one's. The
|
|
8
|
+
* snapshot reports the page the server says it served, not the one that was
|
|
9
|
+
* asked for, because the endpoint clamps a page past the end.
|
|
10
|
+
*/
|
|
11
|
+
import { zGetBillingEventsResponse } from '@comfyorg/ingest-types/zod';
|
|
12
|
+
import type { z } from 'zod';
|
|
13
|
+
import type { BillingResult, BillingTransport } from './billingContracts.js';
|
|
14
|
+
import type { BillingScope, BillingScopeSource } from './billingScope.js';
|
|
15
|
+
export declare const BILLING_EVENTS_ROUTE = "/billing/events";
|
|
16
|
+
export type BillingEventsData = z.infer<typeof zGetBillingEventsResponse>;
|
|
17
|
+
export type BillingEvent = BillingEventsData['events'][number];
|
|
18
|
+
export type BillingEventsScope = BillingScope;
|
|
19
|
+
export interface BillingEventsSnapshot {
|
|
20
|
+
readonly events: readonly BillingEvent[];
|
|
21
|
+
/** 1-indexed, as the endpoint counts. */
|
|
22
|
+
readonly page: number;
|
|
23
|
+
readonly limit: number;
|
|
24
|
+
readonly total: number;
|
|
25
|
+
readonly totalPages: number;
|
|
26
|
+
readonly scope: BillingEventsScope;
|
|
27
|
+
readonly readAt: number;
|
|
28
|
+
}
|
|
29
|
+
export interface BillingEventsReadOptions {
|
|
30
|
+
readonly signal?: AbortSignal;
|
|
31
|
+
/** 1-indexed; the endpoint's own default applies when omitted. */
|
|
32
|
+
readonly page?: number;
|
|
33
|
+
readonly limit?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface BillingEventsReader {
|
|
36
|
+
read: (options?: BillingEventsReadOptions) => Promise<BillingResult<BillingEventsSnapshot>>;
|
|
37
|
+
/** The last page read for the current scope; it carries which page it is. */
|
|
38
|
+
getSnapshot: () => BillingEventsSnapshot | undefined;
|
|
39
|
+
dispose: () => void;
|
|
40
|
+
}
|
|
41
|
+
export interface BillingEventsReaderOptions {
|
|
42
|
+
readonly transport: BillingTransport;
|
|
43
|
+
/** Where the core learns which user, workspace, and role it runs as. */
|
|
44
|
+
readonly scopeSource: BillingScopeSource;
|
|
45
|
+
readonly now?: () => number;
|
|
46
|
+
}
|
|
47
|
+
export declare function createBillingEventsReader(options: BillingEventsReaderOptions): BillingEventsReader;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The billing events read: one page of the workspace's history feed, the
|
|
3
|
+
* charges, credits and adjustments behind the usage log.
|
|
4
|
+
*
|
|
5
|
+
* It is the only billing read whose request varies per call, so the page it
|
|
6
|
+
* asks for is part of what identifies the read: two callers on different
|
|
7
|
+
* pages each get their own answer rather than sharing the first one's. The
|
|
8
|
+
* snapshot reports the page the server says it served, not the one that was
|
|
9
|
+
* asked for, because the endpoint clamps a page past the end.
|
|
10
|
+
*/
|
|
11
|
+
import { zGetBillingEventsResponse } from '@comfyorg/ingest-types/zod';
|
|
12
|
+
import { createScopedReader } from './scopedReader.js';
|
|
13
|
+
export const BILLING_EVENTS_ROUTE = '/billing/events';
|
|
14
|
+
/** Stable key order, so the same page asked for twice is the same request. */
|
|
15
|
+
function billingEventsRoute(options) {
|
|
16
|
+
const query = new URLSearchParams();
|
|
17
|
+
if (options?.page !== undefined)
|
|
18
|
+
query.set('page', String(options.page));
|
|
19
|
+
if (options?.limit !== undefined)
|
|
20
|
+
query.set('limit', String(options.limit));
|
|
21
|
+
const search = query.toString();
|
|
22
|
+
return search === ''
|
|
23
|
+
? BILLING_EVENTS_ROUTE
|
|
24
|
+
: `${BILLING_EVENTS_ROUTE}?${search}`;
|
|
25
|
+
}
|
|
26
|
+
export function createBillingEventsReader(options) {
|
|
27
|
+
const { transport, scopeSource, now = Date.now } = options;
|
|
28
|
+
const reader = createScopedReader({
|
|
29
|
+
transport,
|
|
30
|
+
scopeSource,
|
|
31
|
+
route: billingEventsRoute,
|
|
32
|
+
parse: (body) => zGetBillingEventsResponse.safeParse(body),
|
|
33
|
+
project: ({ data }, scope) => ({
|
|
34
|
+
status: 'ok',
|
|
35
|
+
value: { ...data, scope, readAt: now() }
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
read: reader.read,
|
|
40
|
+
getSnapshot: reader.getSnapshot,
|
|
41
|
+
dispose: reader.dispose
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -11,7 +11,7 @@ export type { BillingErrorCode, BillingFailure, BillingHttpResponse, BillingRequ
|
|
|
11
11
|
export { matchesServerCode, unwrapServerCode } from './billingContracts.js';
|
|
12
12
|
export type { SessionBillingTransportOptions } from './transport.js';
|
|
13
13
|
export { createSessionBillingTransport } from './transport.js';
|
|
14
|
-
export type { CredentialedBillingTransportOptions } from './credentialedTransport.js';
|
|
14
|
+
export type { CredentialedBillingTransportOptions, CredentialedWebSession } from './credentialedTransport.js';
|
|
15
15
|
export { createCredentialedBillingTransport } from './credentialedTransport.js';
|
|
16
16
|
export type { BillingScope, BillingScopeSource } from './billingScope.js';
|
|
17
17
|
export { sessionBillingScopeSource } from './billingScope.js';
|
|
@@ -21,32 +21,37 @@ export type { CapabilityDenialReason, CapabilityDenials } from './capabilityDeni
|
|
|
21
21
|
export { decodeCapabilityDenials } from './capabilityDenials.js';
|
|
22
22
|
export type { BillingBalance, CreditsReadOptions, CreditsReader, CreditsReaderOptions, CreditsScope, CreditsSnapshot } from './credits.js';
|
|
23
23
|
export { CREDITS_ROUTE, createCreditsReader } from './credits.js';
|
|
24
|
+
export type { BillingEvent, BillingEventsData, BillingEventsReader, BillingEventsReaderOptions, BillingEventsReadOptions, BillingEventsScope, BillingEventsSnapshot } from './events.js';
|
|
25
|
+
export { BILLING_EVENTS_ROUTE, createBillingEventsReader } from './events.js';
|
|
24
26
|
export type { BillingPlansData, PlansReadOptions, PlansReader, PlansReaderOptions, PlansScope, PlansSnapshot } from './plans.js';
|
|
25
27
|
export { PLANS_ROUTE, createPlansReader } from './plans.js';
|
|
28
|
+
export type { WorkspaceInvite, WorkspaceInviteCommands } from './workspaceInvites.js';
|
|
29
|
+
export { WORKSPACE_INVITES_ROUTE, createWorkspaceInviteCommands } from './workspaceInvites.js';
|
|
26
30
|
export type { PaymentMethodsReadOptions, PaymentMethodsReader, PaymentMethodsReaderOptions, PaymentMethodsScope, PaymentMethodsSnapshot, SavedPaymentMethod } from './paymentMethods.js';
|
|
27
31
|
export { PAYMENT_METHODS_ROUTE, createPaymentMethodsReader } from './paymentMethods.js';
|
|
28
32
|
export type { BillingStatusData, BillingStatusReader, BillingStatusReaderOptions, BillingStatusReadOptions, BillingStatusScope, BillingStatusSnapshot } from './status.js';
|
|
29
33
|
export { BILLING_STATUS_ROUTE, createBillingStatusReader } from './status.js';
|
|
30
|
-
export type { BillingAuthenticationState, BillingDeclineReason, BillingOpStatus, BillingOperationEvent, BillingOperationIdentity, BillingOperationKind, BillingOperationPhase, BillingOperationServerPhase, BillingOperationState, BillingPresentation, BillingPresentationState, BillingRecoveryAction, EmbeddedChallenge, FailedBillingOperation, HostedBillingDestination, PendingBillingOperation } from './operationState.js';
|
|
31
|
-
export { isBlockedOnCustomerPhase, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
|
|
32
|
-
export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, hasExhaustedPollBudget, isParkedOnCustomer, nextPollDelayMs, pollBudgetMs } from './operationPolicy.js';
|
|
34
|
+
export type { BillingAuthenticationState, BillingDeclineReason, BillingOpStatus, BillingOperationEvent, BillingOperationIdentity, BillingOperationReceipt, BillingOperationKind, BillingOperationPhase, BillingOperationServerPhase, BillingOperationState, BillingPresentation, BillingPresentationState, BillingRecoveryAction, EmbeddedChallenge, FailedBillingOperation, HostedBillingDestination, PendingBillingOperation, SucceededBillingOperation } from './operationState.js';
|
|
35
|
+
export { isBlockedOnCustomerPhase, isGrantLanding, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
|
|
36
|
+
export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, customerCanActHere, hasExhaustedPollBudget, isParkedOnCustomer, nextPollDelayMs, pendingOperationActionHold, pollBudgetMs } from './operationPolicy.js';
|
|
37
|
+
export type { CustomerActionHold } from './operationPolicy.js';
|
|
33
38
|
export type { BillingOperationPointer, BillingOperationPointerStorage, OperationPointerStore } from './operationPointer.js';
|
|
34
39
|
export { OPERATION_POINTER_MAX_AGE_MS, createOperationPointerStore, operationPointerKey } from './operationPointer.js';
|
|
35
40
|
export type { PresentationRoutingInput } from './presentation.js';
|
|
36
41
|
export { selectBillingPresentation } from './presentation.js';
|
|
37
|
-
export type { BillingOperationFailureCategory, BillingOperationLifecycle, BillingOperationLifecycleOptions, BillingOperationTelemetryEvent, IssuedBillingOperation, PresentationSwitchOutcome } from './operationLifecycle.js';
|
|
42
|
+
export type { BillingOperationFailureCategory, BillingOperationLifecycle, BillingOperationLifecycleOptions, BillingOperationTelemetryEvent, BillingRecoverOptions, IssuedBillingOperation, PresentationSwitchOutcome } from './operationLifecycle.js';
|
|
38
43
|
export { createBillingOperationLifecycle, operationRoute } from './operationLifecycle.js';
|
|
39
44
|
export { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
40
45
|
export type { EmbeddedChallengeOutcome, EmbeddedChallengePort } from './challengeDriver.js';
|
|
41
46
|
export { driveEmbeddedChallenge } from './challengeDriver.js';
|
|
42
|
-
export { readBillingErrorCode } from './billingErrorBody.js';
|
|
47
|
+
export { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
|
|
43
48
|
export type { BillingCommands, BillingCommandsOptions, PaymentPortalResult, PreviewSubscribeInput, PreviewSubscribeOptions, PreviewSubscribeResult, SubscribeInput, SubscriptionCommandCode, SubscriptionCommandFailure, SubscriptionCommandOutcome, SubscriptionCommandResult, SubscriptionPreview, TerminalBillingOperation } from './subscriptionCommands.js';
|
|
44
49
|
export { CANCEL_SUBSCRIPTION_ROUTE, PAYMENT_PORTAL_ROUTE, PREVIEW_SUBSCRIBE_ROUTE, RESUBSCRIBE_ROUTE, SUBSCRIBE_ROUTE, createBillingCommands } from './subscriptionCommands.js';
|
|
45
50
|
export type { HostPaymentStep, PaymentProjection, PaymentReasonKey, PaymentStep } from './paymentProjection.js';
|
|
46
|
-
export { projectPaymentStep } from './paymentProjection.js';
|
|
47
|
-
export type { PaymentCopyKey, PaymentCopyKeys } from './paymentCopy.js';
|
|
48
|
-
export { DEFAULT_PAYMENT_COPY, createPaymentCopy, paymentCopyKeys } from './paymentCopy.js';
|
|
49
|
-
export type { CreateHostedTopupCheckoutInput, CreateTopupCheckoutInput, HostedTopupCheckout, HostedTopupCheckoutFailure, HostedTopupCheckoutResult, TopupCommand, TopupCommandOptions, TopupDeclined, TopupDenied, TopupFailure, TopupInvalidAmount, TopupInvalidReturnUrl, TopupNoPaymentMethod, TopupNotAvailable, TopupResult, TopupSucceeded, TopupUnsettled } from './topup.js';
|
|
50
|
-
export { TOPUP_CHECKOUT_ROUTE, TOPUP_ROUTE, createTopupCommand } from './topup.js';
|
|
51
|
+
export { awaitsHostedAction, projectPaymentStep } from './paymentProjection.js';
|
|
52
|
+
export type { DeclineDetailKey, PaymentCopyKey, PaymentCopyKeys } from './paymentCopy.js';
|
|
53
|
+
export { DEFAULT_PAYMENT_COPY, createPaymentCopy, declineDetailKey, paymentCopyKeys } from './paymentCopy.js';
|
|
54
|
+
export type { CreateHostedTopupCheckoutInput, CreateTopupCheckoutInput, HostedTopupCheckout, HostedTopupCheckoutFailure, HostedTopupCheckoutResult, QuoteTopupInput, TopupCommand, TopupCommandOptions, TopupDeclined, TopupDenied, TopupFailure, TopupInvalidAmount, TopupInvalidReturnUrl, TopupNoPaymentMethod, TopupNotAvailable, TopupQuote, TopupQuoteResult, TopupResult, TopupSucceeded, TopupUnsettled } from './topup.js';
|
|
55
|
+
export { TOPUP_CHECKOUT_ROUTE, TOPUP_QUOTE_ROUTE, TOPUP_ROUTE, createTopupCommand } from './topup.js';
|
|
51
56
|
export type { BalanceWatch, BalanceWatchOptions, BalanceWatchOutcome } from './balanceWatch.js';
|
|
52
57
|
export { BALANCE_WATCH_LIFETIME_MS, BALANCE_WATCH_MAX_SCHEDULED_RUNS, BALANCE_WATCH_RETRY_GAPS_MS, createBalanceWatch } from './balanceWatch.js';
|
|
@@ -5,19 +5,21 @@ export { sessionBillingScopeSource } from './billingScope.js';
|
|
|
5
5
|
export { CAPABILITIES_ROUTE, CAPABILITY_REVISION_HEADER, createCapabilitiesReader, readCapabilityRevision } from './capabilities.js';
|
|
6
6
|
export { decodeCapabilityDenials } from './capabilityDenials.js';
|
|
7
7
|
export { CREDITS_ROUTE, createCreditsReader } from './credits.js';
|
|
8
|
+
export { BILLING_EVENTS_ROUTE, createBillingEventsReader } from './events.js';
|
|
8
9
|
export { PLANS_ROUTE, createPlansReader } from './plans.js';
|
|
10
|
+
export { WORKSPACE_INVITES_ROUTE, createWorkspaceInviteCommands } from './workspaceInvites.js';
|
|
9
11
|
export { PAYMENT_METHODS_ROUTE, createPaymentMethodsReader } from './paymentMethods.js';
|
|
10
12
|
export { BILLING_STATUS_ROUTE, createBillingStatusReader } from './status.js';
|
|
11
|
-
export { isBlockedOnCustomerPhase, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
|
|
12
|
-
export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, hasExhaustedPollBudget, isParkedOnCustomer, nextPollDelayMs, pollBudgetMs } from './operationPolicy.js';
|
|
13
|
+
export { isBlockedOnCustomerPhase, isGrantLanding, isTerminal, reduceBillingOperation, validateActionUrl } from './operationState.js';
|
|
14
|
+
export { OPERATION_POLL_BUDGET, OPERATION_POLL_TIMING, customerCanActHere, hasExhaustedPollBudget, isParkedOnCustomer, nextPollDelayMs, pendingOperationActionHold, pollBudgetMs } from './operationPolicy.js';
|
|
13
15
|
export { OPERATION_POINTER_MAX_AGE_MS, createOperationPointerStore, operationPointerKey } from './operationPointer.js';
|
|
14
16
|
export { selectBillingPresentation } from './presentation.js';
|
|
15
17
|
export { createBillingOperationLifecycle, operationRoute } from './operationLifecycle.js';
|
|
16
18
|
export { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
17
19
|
export { driveEmbeddedChallenge } from './challengeDriver.js';
|
|
18
|
-
export { readBillingErrorCode } from './billingErrorBody.js';
|
|
20
|
+
export { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
|
|
19
21
|
export { CANCEL_SUBSCRIPTION_ROUTE, PAYMENT_PORTAL_ROUTE, PREVIEW_SUBSCRIBE_ROUTE, RESUBSCRIBE_ROUTE, SUBSCRIBE_ROUTE, createBillingCommands } from './subscriptionCommands.js';
|
|
20
|
-
export { projectPaymentStep } from './paymentProjection.js';
|
|
21
|
-
export { DEFAULT_PAYMENT_COPY, createPaymentCopy, paymentCopyKeys } from './paymentCopy.js';
|
|
22
|
-
export { TOPUP_CHECKOUT_ROUTE, TOPUP_ROUTE, createTopupCommand } from './topup.js';
|
|
22
|
+
export { awaitsHostedAction, projectPaymentStep } from './paymentProjection.js';
|
|
23
|
+
export { DEFAULT_PAYMENT_COPY, createPaymentCopy, declineDetailKey, paymentCopyKeys } from './paymentCopy.js';
|
|
24
|
+
export { TOPUP_CHECKOUT_ROUTE, TOPUP_QUOTE_ROUTE, TOPUP_ROUTE, createTopupCommand } from './topup.js';
|
|
23
25
|
export { BALANCE_WATCH_LIFETIME_MS, BALANCE_WATCH_MAX_SCHEDULED_RUNS, BALANCE_WATCH_RETRY_GAPS_MS, createBalanceWatch } from './balanceWatch.js';
|
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operation lifecycle every billing command sits on: adopt a
|
|
3
|
+
* `billing_op_id`, observe it to a terminal state, and reattach to it after a
|
|
4
|
+
* reload, a focus, or a return to the workspace — without ever creating a
|
|
5
|
+
* replacement operation. Commands issue the request; this owns everything
|
|
6
|
+
* that happens to the id afterwards.
|
|
7
|
+
*
|
|
8
|
+
* Framework-free by construction. Nothing here opens a URL, drives a
|
|
9
|
+
* payment-provider challenge, or listens to a document: a host renders the
|
|
10
|
+
* state (`actionUrl`, `challenge`), performs those effects itself, and
|
|
11
|
+
* reports back through `reportChallenge*` and `wake`.
|
|
12
|
+
*
|
|
13
|
+
* Behavior is ported from the cloud app's `billingOperationStore` — the
|
|
14
|
+
* cadence, the budgets, the parked-on-customer rules, the challenge echo
|
|
15
|
+
* suppression — and from the pending-checkout pointer's terminal rule; the
|
|
16
|
+
* wiring onto the scope source, the scope tracker, and the generated
|
|
17
|
+
* contract is new.
|
|
18
|
+
*/
|
|
1
19
|
import { BILLING_OPERATION_TELEMETRY_EVENT } from '../../telemetry.js';
|
|
2
20
|
import type { BillingResult, BillingTransport } from './billingContracts.js';
|
|
3
21
|
import type { BillingScope, BillingScopeSource } from './billingScope.js';
|
|
@@ -39,6 +57,13 @@ export interface BillingOperationLifecycleOptions {
|
|
|
39
57
|
readonly statusReader: BillingStatusReader;
|
|
40
58
|
/** Tab-local storage for the operation pointer; absent means nothing survives a reload. */
|
|
41
59
|
readonly pointerStorage?: BillingOperationPointerStorage;
|
|
60
|
+
/**
|
|
61
|
+
* Keep the pointer of an operation that succeeded or needs reconciliation,
|
|
62
|
+
* for a host whose page is the checkout itself: a revisit reads it back
|
|
63
|
+
* through `recover({ includeSettled: true })`. A plain `recover` never
|
|
64
|
+
* sees it, so every other caller recovers exactly what it did before.
|
|
65
|
+
*/
|
|
66
|
+
readonly retainSettledPointer?: boolean;
|
|
42
67
|
/** Whether the host can drive an in-page challenge right now. Absent routes everything hosted. */
|
|
43
68
|
readonly embeddedCheckoutAvailable?: () => boolean;
|
|
44
69
|
/** Which origin serves a hosted page right now. Absent keeps every hosted operation on the provider page. */
|
|
@@ -46,6 +71,14 @@ export interface BillingOperationLifecycleOptions {
|
|
|
46
71
|
readonly onTelemetry?: (event: BillingOperationTelemetryEvent) => void;
|
|
47
72
|
readonly now?: () => number;
|
|
48
73
|
}
|
|
74
|
+
export interface BillingRecoverOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Also read back the operation a retained pointer says already settled, so
|
|
77
|
+
* the checkout that issued it can show it finished. Only a lifecycle with
|
|
78
|
+
* `retainSettledPointer` keeps one.
|
|
79
|
+
*/
|
|
80
|
+
readonly includeSettled?: boolean;
|
|
81
|
+
}
|
|
49
82
|
export interface BillingOperationLifecycle {
|
|
50
83
|
/**
|
|
51
84
|
* Runs a command on the lifecycle: single-flight per kind, the backend's
|
|
@@ -59,7 +92,7 @@ export interface BillingOperationLifecycle {
|
|
|
59
92
|
* pending operation first, then the tab-local pointer. Resolves undefined
|
|
60
93
|
* when there is nothing to recover.
|
|
61
94
|
*/
|
|
62
|
-
recover: () => Promise<BillingResult<BillingOperationState | undefined>>;
|
|
95
|
+
recover: (options?: BillingRecoverOptions) => Promise<BillingResult<BillingOperationState | undefined>>;
|
|
63
96
|
/** The host became visible or focused: poll every pending operation now. */
|
|
64
97
|
wake: () => void;
|
|
65
98
|
/** Moves the operation between presentations under the same id. */
|