@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.
Files changed (64) hide show
  1. package/dist/core/billing/billingContracts.d.ts +29 -14
  2. package/dist/core/billing/billingErrorBody.d.ts +8 -4
  3. package/dist/core/billing/billingErrorBody.js +14 -4
  4. package/dist/core/billing/credentialedTransport.d.ts +23 -7
  5. package/dist/core/billing/credentialedTransport.js +72 -8
  6. package/dist/core/billing/events.d.ts +47 -0
  7. package/dist/core/billing/events.js +43 -0
  8. package/dist/core/billing/index.d.ts +16 -11
  9. package/dist/core/billing/index.js +8 -6
  10. package/dist/core/billing/operationLifecycle.d.ts +34 -1
  11. package/dist/core/billing/operationLifecycle.js +146 -67
  12. package/dist/core/billing/operationPointer.d.ts +27 -2
  13. package/dist/core/billing/operationPointer.js +36 -25
  14. package/dist/core/billing/operationPolicy.d.ts +44 -7
  15. package/dist/core/billing/operationPolicy.js +64 -3
  16. package/dist/core/billing/operationState.d.ts +225 -19
  17. package/dist/core/billing/operationState.js +93 -8
  18. package/dist/core/billing/paymentCopy.d.ts +6 -1
  19. package/dist/core/billing/paymentCopy.js +34 -1
  20. package/dist/core/billing/paymentProjection.d.ts +14 -2
  21. package/dist/core/billing/paymentProjection.js +37 -9
  22. package/dist/core/billing/scopedReader.d.ts +11 -5
  23. package/dist/core/billing/scopedReader.js +20 -9
  24. package/dist/core/billing/sharedRead.js +4 -2
  25. package/dist/core/billing/subscriptionCommands.d.ts +63 -9
  26. package/dist/core/billing/subscriptionCommands.js +46 -30
  27. package/dist/core/billing/topup.d.ts +26 -0
  28. package/dist/core/billing/topup.js +36 -2
  29. package/dist/core/billing/wireCents.d.ts +8 -0
  30. package/dist/core/billing/wireCents.js +8 -0
  31. package/dist/core/billing/workspaceInvites.d.ts +12 -0
  32. package/dist/core/billing/workspaceInvites.js +26 -0
  33. package/dist/core/identity.d.ts +6 -6
  34. package/dist/core/identity.js +4 -10
  35. package/dist/core/lazyIdentity.d.ts +31 -0
  36. package/dist/core/lazyIdentity.js +96 -0
  37. package/dist/core/requestAuth.d.ts +30 -0
  38. package/dist/core/requestAuth.js +41 -0
  39. package/dist/core/requestTimeout.d.ts +7 -0
  40. package/dist/core/requestTimeout.js +13 -0
  41. package/dist/core/session.d.ts +12 -18
  42. package/dist/core/session.js +16 -26
  43. package/dist/core/sessionContracts.d.ts +39 -2
  44. package/dist/core/sessionTokenMint.d.ts +35 -0
  45. package/dist/core/sessionTokenMint.js +222 -0
  46. package/dist/core/webSession.d.ts +27 -0
  47. package/dist/core/webSession.js +144 -0
  48. package/dist/core/webSessionFlag.d.ts +19 -0
  49. package/dist/core/webSessionFlag.js +61 -0
  50. package/dist/core/webSessionIdentity.d.ts +297 -0
  51. package/dist/core/webSessionIdentity.js +509 -0
  52. package/dist/firebase/configSource.d.ts +54 -0
  53. package/dist/firebase/configSource.js +111 -0
  54. package/dist/firebase/index.d.ts +71 -8
  55. package/dist/firebase/index.js +236 -23
  56. package/dist/firebase/popupWatch.d.ts +42 -0
  57. package/dist/firebase/popupWatch.js +140 -0
  58. package/dist/testing.d.ts +38 -0
  59. package/dist/testing.js +116 -0
  60. package/dist/web/crossTabRefresh.d.ts +4 -2
  61. package/dist/web/crossTabRefresh.js +13 -0
  62. package/dist/workspaceLink.d.ts +45 -0
  63. package/dist/workspaceLink.js +78 -0
  64. 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, so a
7
- * caller cannot accidentally surface a server or payment-provider string to a
8
- * user. The codes carry no server text; the copy that belongs to each code is
9
- * owned by the billing core and localized by the host. The one server value
10
- * that crosses this boundary is `serverCode`, a machine identifier and never
11
- * copy: a command matches it against its own closed set, and a host stores it
12
- * where it already keeps `WorkspaceApiError.code`. Its brand makes it
13
- * unforgeable: only the response decoder mints one, and `unwrapServerCode`
14
- * names the one sanctioned widening.
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. Its `message` is dropped on purpose: a command acts on codes
61
- * it names, never on server text. Compare it through `matchesServerCode`,
62
- * and widen it through `unwrapServerCode` at a host's error-store
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. The body's
4
- * `message` is read by the schema and discarded here, so it cannot travel
5
- * further into the SDK. This decode is the only place a `BillingServerCode`
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. The body's
4
- * `message` is read by the schema and discarded here, so it cannot travel
5
- * further into the SDK. This decode is the only place a `BillingServerCode`
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
- * No CSRF header is sent. What stands in for one is the JSON content type
17
- * `exchangeBillingRequest` always sends: it makes every request non-simple,
18
- * so a cross-site caller has to clear a preflight it cannot satisfy. That
19
- * rests on the cookie's `SameSite` attribute and the backend's CORS
20
- * allowlist, neither of which is enforced here — sending a form-encoded or
21
- * otherwise CORS-simple request from this transport removes the protection.
22
- * A CSRF header is added here when the backend names one.
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
- if (captured === undefined) {
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 exchangeBillingRequest(request, {
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. */