@comfyorg/account-core 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/dist/core/billing/balanceWatch.d.ts +35 -0
  3. package/dist/core/billing/balanceWatch.js +83 -0
  4. package/dist/core/billing/billingContracts.d.ts +123 -0
  5. package/dist/core/billing/billingContracts.js +11 -0
  6. package/dist/core/billing/billingErrorBody.d.ts +8 -0
  7. package/dist/core/billing/billingErrorBody.js +11 -0
  8. package/dist/core/billing/billingScope.d.ts +39 -0
  9. package/dist/core/billing/billingScope.js +45 -0
  10. package/dist/core/billing/capabilities.d.ts +70 -0
  11. package/dist/core/billing/capabilities.js +137 -0
  12. package/dist/core/billing/capabilityDenials.d.ts +26 -0
  13. package/dist/core/billing/capabilityDenials.js +43 -0
  14. package/dist/core/billing/challengeDriver.d.ts +22 -0
  15. package/dist/core/billing/challengeDriver.js +38 -0
  16. package/dist/core/billing/credentialedTransport.d.ts +35 -0
  17. package/dist/core/billing/credentialedTransport.js +53 -0
  18. package/dist/core/billing/credits.d.ts +43 -0
  19. package/dist/core/billing/credits.js +29 -0
  20. package/dist/core/billing/httpStatus.d.ts +7 -0
  21. package/dist/core/billing/httpStatus.js +25 -0
  22. package/dist/core/billing/index.d.ts +52 -0
  23. package/dist/core/billing/index.js +23 -0
  24. package/dist/core/billing/operationLifecycle.d.ts +77 -0
  25. package/dist/core/billing/operationLifecycle.js +547 -0
  26. package/dist/core/billing/operationPointer.d.ts +48 -0
  27. package/dist/core/billing/operationPointer.js +89 -0
  28. package/dist/core/billing/operationPolicy.d.ts +26 -0
  29. package/dist/core/billing/operationPolicy.js +51 -0
  30. package/dist/core/billing/operationState.d.ts +128 -0
  31. package/dist/core/billing/operationState.js +187 -0
  32. package/dist/core/billing/paymentCopy.d.ts +18 -0
  33. package/dist/core/billing/paymentCopy.js +47 -0
  34. package/dist/core/billing/paymentMethods.d.ts +40 -0
  35. package/dist/core/billing/paymentMethods.js +37 -0
  36. package/dist/core/billing/paymentProjection.d.ts +34 -0
  37. package/dist/core/billing/paymentProjection.js +56 -0
  38. package/dist/core/billing/plans.d.ts +44 -0
  39. package/dist/core/billing/plans.js +29 -0
  40. package/dist/core/billing/presentation.d.ts +20 -0
  41. package/dist/core/billing/presentation.js +10 -0
  42. package/dist/core/billing/scopedReader.d.ts +61 -0
  43. package/dist/core/billing/scopedReader.js +108 -0
  44. package/dist/core/billing/sharedRead.d.ts +33 -0
  45. package/dist/core/billing/sharedRead.js +59 -0
  46. package/dist/core/billing/status.d.ts +27 -0
  47. package/dist/core/billing/status.js +21 -0
  48. package/dist/core/billing/subscriptionCommands.d.ts +367 -0
  49. package/dist/core/billing/subscriptionCommands.js +263 -0
  50. package/dist/core/billing/topup.d.ts +113 -0
  51. package/dist/core/billing/topup.js +209 -0
  52. package/dist/core/billing/transport.d.ts +14 -0
  53. package/dist/core/billing/transport.js +106 -0
  54. package/dist/core/billing/transportExchange.d.ts +24 -0
  55. package/dist/core/billing/transportExchange.js +79 -0
  56. package/dist/core/boundedOperation.d.ts +16 -0
  57. package/dist/core/boundedOperation.js +12 -0
  58. package/dist/core/credentialCache.d.ts +41 -0
  59. package/dist/core/credentialCache.js +97 -0
  60. package/dist/core/customerRecovery.d.ts +39 -0
  61. package/dist/core/customerRecovery.js +79 -0
  62. package/dist/core/exchange.d.ts +50 -0
  63. package/dist/core/exchange.js +137 -0
  64. package/dist/core/identity.d.ts +21 -0
  65. package/dist/core/identity.js +16 -0
  66. package/dist/core/mintCoordinator.d.ts +18 -0
  67. package/dist/core/mintCoordinator.js +31 -0
  68. package/dist/core/refreshScheduler.d.ts +46 -0
  69. package/dist/core/refreshScheduler.js +262 -0
  70. package/dist/core/session.d.ts +146 -0
  71. package/dist/core/session.js +312 -0
  72. package/dist/core/sessionContracts.d.ts +135 -0
  73. package/dist/core/sessionContracts.js +9 -0
  74. package/dist/core/sessionState.d.ts +118 -0
  75. package/dist/core/sessionState.js +235 -0
  76. package/dist/firebase/index.d.ts +53 -0
  77. package/dist/firebase/index.js +74 -0
  78. package/dist/firebaseAuthError.d.ts +52 -0
  79. package/dist/firebaseAuthError.js +88 -0
  80. package/dist/loadExternalScript.d.ts +7 -0
  81. package/dist/loadExternalScript.js +88 -0
  82. package/dist/provisioning.d.ts +53 -0
  83. package/dist/provisioning.js +76 -0
  84. package/dist/redirect.d.ts +13 -0
  85. package/dist/redirect.js +38 -0
  86. package/dist/signInSchemas.d.ts +88 -0
  87. package/dist/signInSchemas.js +71 -0
  88. package/dist/telemetry.d.ts +39 -0
  89. package/dist/telemetry.js +26 -0
  90. package/dist/testing.d.ts +7 -0
  91. package/dist/testing.js +3 -0
  92. package/dist/turnstile.d.ts +14 -0
  93. package/dist/turnstile.js +17 -0
  94. package/dist/turnstileScript.d.ts +18 -0
  95. package/dist/turnstileScript.js +6 -0
  96. package/dist/web/crossTabRefresh.d.ts +18 -0
  97. package/dist/web/crossTabRefresh.js +121 -0
  98. package/dist/webviewDetection.d.ts +5 -0
  99. package/dist/webviewDetection.js +62 -0
  100. package/package.json +114 -0
@@ -0,0 +1,26 @@
1
+ import type { PendingBillingOperation } from './operationState.js';
2
+ export declare const OPERATION_POLL_TIMING: {
3
+ readonly initialMs: 1000;
4
+ readonly maxMs: 8000;
5
+ readonly multiplier: 1.5;
6
+ /** An operation parked on the customer is checked on a slow, flat cadence. */
7
+ readonly parkedMs: 30000;
8
+ };
9
+ export declare const OPERATION_POLL_BUDGET: {
10
+ readonly defaultMs: 120000;
11
+ /** A subscription may take minutes to discover that it needs the customer. */
12
+ readonly subscriptionDiscoveryMs: number;
13
+ /** Once the customer is involved, a bank challenge can idle for hours. */
14
+ readonly customerActionMs: number;
15
+ };
16
+ /**
17
+ * Waiting on the customer, not on the backend: a challenge to complete
18
+ * elsewhere, a hosted page to finish, a phase the server reports as blocked on
19
+ * them, or a declined attempt awaiting their retry. Once this tab's own
20
+ * challenge completes the state reads processing and nothing waits on the
21
+ * customer anymore.
22
+ */
23
+ export declare function isParkedOnCustomer(state: PendingBillingOperation): boolean;
24
+ export declare function nextPollDelayMs(state: PendingBillingOperation, previousDelayMs: number | undefined): number;
25
+ export declare function pollBudgetMs(state: PendingBillingOperation): number;
26
+ export declare function hasExhaustedPollBudget(state: PendingBillingOperation, now: number): boolean;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The cadence and budget for observing one operation, taken from the cloud
3
+ * app's `billingOperationStore` rather than invented, so a consumer moving
4
+ * onto the SDK sees the same request rate and the same give-up points.
5
+ */
6
+ import { isBlockedOnCustomerPhase } from './operationState.js';
7
+ export const OPERATION_POLL_TIMING = {
8
+ initialMs: 1_000,
9
+ maxMs: 8_000,
10
+ multiplier: 1.5,
11
+ /** An operation parked on the customer is checked on a slow, flat cadence. */
12
+ parkedMs: 30_000
13
+ };
14
+ export const OPERATION_POLL_BUDGET = {
15
+ defaultMs: 120_000,
16
+ /** A subscription may take minutes to discover that it needs the customer. */
17
+ subscriptionDiscoveryMs: 5 * 60_000,
18
+ /** Once the customer is involved, a bank challenge can idle for hours. */
19
+ customerActionMs: 23 * 60 * 60_000
20
+ };
21
+ /**
22
+ * Waiting on the customer, not on the backend: a challenge to complete
23
+ * elsewhere, a hosted page to finish, a phase the server reports as blocked on
24
+ * them, or a declined attempt awaiting their retry. Once this tab's own
25
+ * challenge completes the state reads processing and nothing waits on the
26
+ * customer anymore.
27
+ */
28
+ export function isParkedOnCustomer(state) {
29
+ return (state.authenticationState === 'requires_action' ||
30
+ state.actionUrl !== undefined ||
31
+ isBlockedOnCustomerPhase(state.serverPhase) ||
32
+ (state.authenticationState === 'failed_retryable' &&
33
+ state.customerActionSeen));
34
+ }
35
+ export function nextPollDelayMs(state, previousDelayMs) {
36
+ if (isParkedOnCustomer(state))
37
+ return OPERATION_POLL_TIMING.parkedMs;
38
+ return Math.min((previousDelayMs ?? OPERATION_POLL_TIMING.initialMs) *
39
+ OPERATION_POLL_TIMING.multiplier, OPERATION_POLL_TIMING.maxMs);
40
+ }
41
+ export function pollBudgetMs(state) {
42
+ if (state.kind !== 'cancel' && state.customerActionSeen) {
43
+ return OPERATION_POLL_BUDGET.customerActionMs;
44
+ }
45
+ return state.kind === 'subscription'
46
+ ? OPERATION_POLL_BUDGET.subscriptionDiscoveryMs
47
+ : OPERATION_POLL_BUDGET.defaultMs;
48
+ }
49
+ export function hasExhaustedPollBudget(state, now) {
50
+ return now - state.observedAt > pollBudgetMs(state);
51
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * One billing operation as the SDK sees it: a discriminated union over the
3
+ * lifecycle a `billing_op_id` moves through, and the pure transition that
4
+ * advances it. The identity is fixed at adoption and survives every
5
+ * presentation change, so switching a checkout from the embedded challenge
6
+ * to the hosted page (or back) is a transition on the same operation, never
7
+ * a replacement one.
8
+ *
9
+ * The transition consumes the generated `BillingOpStatusResponse`. It reads
10
+ * the coded `decline_reason` and `recovery_action` and never `error_message`,
11
+ * so no server or payment-provider text can reach a consumer through this
12
+ * state.
13
+ */
14
+ import type { zBillingOpStatusResponse } from '@comfyorg/ingest-types/zod';
15
+ import type { z } from 'zod';
16
+ import type { BillingScope } from './billingScope.js';
17
+ export type BillingOpStatus = z.infer<typeof zBillingOpStatusResponse>;
18
+ export type BillingOperationKind = 'subscription' | 'topup' | 'cancel';
19
+ /**
20
+ * Where the customer completes the operation: the challenge this tab drives
21
+ * through the host's payment-provider adapter, or the page behind the
22
+ * server's `action_url`.
23
+ */
24
+ export type BillingPresentation = 'embedded' | 'hosted';
25
+ /**
26
+ * Which origin serves a hosted presentation: the provider page behind the
27
+ * server's `action_url`, or the hosted billing app.
28
+ */
29
+ export type HostedBillingDestination = 'stripe' | 'billing_web';
30
+ export type BillingDeclineReason = NonNullable<BillingOpStatus['decline_reason']>;
31
+ export type BillingRecoveryAction = NonNullable<BillingOpStatus['recovery_action']>;
32
+ export type BillingAuthenticationState = NonNullable<BillingOpStatus['authentication_state']>;
33
+ export type BillingOperationServerPhase = NonNullable<BillingOpStatus['phase']>;
34
+ /**
35
+ * The phases the contract defines as blocked on the customer. Neither advances
36
+ * on its own, so an operation reporting one waits on them even before it has a
37
+ * link to offer.
38
+ */
39
+ export declare function isBlockedOnCustomerPhase(phase: BillingOperationServerPhase | null | undefined): boolean;
40
+ export type BillingPresentationState = {
41
+ readonly presentation: 'embedded';
42
+ readonly hostedDestination?: never;
43
+ } | {
44
+ readonly presentation: 'hosted';
45
+ readonly hostedDestination: HostedBillingDestination;
46
+ };
47
+ export type BillingOperationIdentity = BillingPresentationState & {
48
+ readonly id: string;
49
+ readonly kind: BillingOperationKind;
50
+ readonly scope: BillingScope;
51
+ /** When this tab began observing the operation; the poll budget counts from here. */
52
+ readonly observedAt: number;
53
+ /** When the attempt began, before the command was issued; telemetry durations count from here. */
54
+ readonly attemptStartedAt: number;
55
+ };
56
+ /**
57
+ * The in-page challenge for an embedded presentation. `completed` and
58
+ * `failed` are this tab's verdicts and outrank a server echo of the same
59
+ * challenge, so the presentation neither flaps back to "verify" after a
60
+ * failure nor reopens the pay step mid-payment after a success.
61
+ */
62
+ export interface EmbeddedChallenge {
63
+ readonly clientSecret: string;
64
+ readonly status: 'required' | 'in_progress' | 'completed' | 'failed';
65
+ }
66
+ export type PendingBillingOperation = BillingOperationIdentity & {
67
+ readonly phase: 'pending';
68
+ /** The hosted continuation the server currently offers; https only. */
69
+ readonly actionUrl?: string;
70
+ readonly challenge?: EmbeddedChallenge;
71
+ readonly authenticationState?: BillingAuthenticationState;
72
+ readonly serverPhase?: BillingOperationServerPhase;
73
+ /** Set while the customer's last attempt was declined and they may try again. */
74
+ readonly declineReason?: BillingDeclineReason;
75
+ readonly recoveryAction?: BillingRecoveryAction;
76
+ /** True once the operation has ever waited on the customer; widens the poll budget. */
77
+ readonly customerActionSeen: boolean;
78
+ };
79
+ export type FailedBillingOperation = BillingOperationIdentity & {
80
+ readonly phase: 'failed';
81
+ readonly declineReason: BillingDeclineReason;
82
+ readonly recoveryAction?: BillingRecoveryAction;
83
+ readonly retryable: boolean;
84
+ };
85
+ export type BillingOperationState = PendingBillingOperation | (BillingOperationIdentity & {
86
+ readonly phase: 'succeeded';
87
+ }) | FailedBillingOperation
88
+ /** This tab's poll budget ran out; the server may still settle the operation. */
89
+ | (BillingOperationIdentity & {
90
+ readonly phase: 'timed_out';
91
+ })
92
+ /** The server cannot settle it without a human; surface the id to support. */
93
+ | (BillingOperationIdentity & {
94
+ readonly phase: 'reconciliation_needed';
95
+ })
96
+ /** The session or workspace changed underneath it; nothing here may be attributed to the new scope. */
97
+ | (BillingOperationIdentity & {
98
+ readonly phase: 'superseded';
99
+ });
100
+ export type BillingOperationPhase = BillingOperationState['phase'];
101
+ export type BillingOperationEvent = {
102
+ readonly type: 'status_polled';
103
+ readonly status: BillingOpStatus;
104
+ } | {
105
+ readonly type: 'timed_out';
106
+ } | {
107
+ readonly type: 'superseded';
108
+ }
109
+ /** The operation could not be found under this scope. */
110
+ | {
111
+ readonly type: 'lost';
112
+ } | {
113
+ readonly type: 'presentation_switched';
114
+ readonly presentation: 'hosted';
115
+ readonly hostedDestination: HostedBillingDestination;
116
+ } | {
117
+ readonly type: 'presentation_switched';
118
+ readonly presentation: 'embedded';
119
+ } | {
120
+ readonly type: 'challenge_started';
121
+ } | {
122
+ readonly type: 'challenge_settled';
123
+ readonly outcome: 'completed' | 'failed';
124
+ };
125
+ export declare function isTerminal(state: BillingOperationState): state is Exclude<BillingOperationState, PendingBillingOperation>;
126
+ /** A continuation link the SDK will hand to a host: absolute and https, nothing else. */
127
+ export declare function validateActionUrl(value: string | undefined): string | undefined;
128
+ export declare function reduceBillingOperation(state: BillingOperationState, event: BillingOperationEvent): BillingOperationState;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The phases the contract defines as blocked on the customer. Neither advances
3
+ * on its own, so an operation reporting one waits on them even before it has a
4
+ * link to offer.
5
+ */
6
+ export function isBlockedOnCustomerPhase(phase) {
7
+ return (phase === 'awaiting_payment_method' || phase === 'awaiting_invoice_payment');
8
+ }
9
+ export function isTerminal(state) {
10
+ return state.phase !== 'pending';
11
+ }
12
+ /** A continuation link the SDK will hand to a host: absolute and https, nothing else. */
13
+ export function validateActionUrl(value) {
14
+ if (value === undefined)
15
+ return undefined;
16
+ try {
17
+ return new URL(value).protocol === 'https:' ? value : undefined;
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function presentationOf(state) {
24
+ return state.presentation === 'hosted'
25
+ ? { presentation: 'hosted', hostedDestination: state.hostedDestination }
26
+ : { presentation: 'embedded' };
27
+ }
28
+ function identityOf(state) {
29
+ return {
30
+ id: state.id,
31
+ kind: state.kind,
32
+ scope: state.scope,
33
+ observedAt: state.observedAt,
34
+ attemptStartedAt: state.attemptStartedAt,
35
+ ...presentationOf(state)
36
+ };
37
+ }
38
+ function withPhase(state, phase) {
39
+ return { ...identityOf(state), phase };
40
+ }
41
+ /**
42
+ * The server's `requires_action` is an echo of a challenge this tab already
43
+ * settled when it names the same client secret (or none). The tab's verdict
44
+ * stands until the intent actually moves.
45
+ */
46
+ function echoesSettledChallenge(state, status) {
47
+ const challenge = state.challenge;
48
+ if (challenge === undefined)
49
+ return false;
50
+ if (status.authentication_state !== 'requires_action')
51
+ return false;
52
+ if (challenge.status !== 'completed' && challenge.status !== 'failed') {
53
+ return false;
54
+ }
55
+ return (status.payment_intent_client_secret === undefined ||
56
+ status.payment_intent_client_secret === challenge.clientSecret);
57
+ }
58
+ function nextChallenge(state, status) {
59
+ if (state.presentation !== 'embedded')
60
+ return state.challenge;
61
+ const secret = status.payment_intent_client_secret;
62
+ if (secret === undefined)
63
+ return state.challenge;
64
+ if (state.challenge?.clientSecret === secret)
65
+ return state.challenge;
66
+ if (status.authentication_state !== 'requires_action')
67
+ return state.challenge;
68
+ return { clientSecret: secret, status: 'required' };
69
+ }
70
+ function terminalFromStatus(state, status) {
71
+ if (status.status === 'succeeded')
72
+ return withPhase(state, 'succeeded');
73
+ if (status.status === 'failed') {
74
+ return {
75
+ ...identityOf(state),
76
+ phase: 'failed',
77
+ declineReason: status.decline_reason ?? 'generic',
78
+ ...(status.recovery_action === undefined
79
+ ? {}
80
+ : { recoveryAction: status.recovery_action }),
81
+ retryable: status.retryable === true
82
+ };
83
+ }
84
+ if (status.status === 'reconciliation_needed' ||
85
+ status.authentication_state === 'reconciliation_needed') {
86
+ return withPhase(state, 'reconciliation_needed');
87
+ }
88
+ return undefined;
89
+ }
90
+ /**
91
+ * A link echoed while this tab's completed challenge is still processing
92
+ * points at that same challenge; surfacing it would ask the customer to
93
+ * redo a step they just finished.
94
+ */
95
+ function nextActionUrl(state, status, authenticationState) {
96
+ return state.challenge?.status === 'completed' &&
97
+ authenticationState !== 'requires_action'
98
+ ? state.actionUrl
99
+ : validateActionUrl(status.action_url);
100
+ }
101
+ function reducePending(state, status) {
102
+ if (status.id !== state.id)
103
+ return state;
104
+ const terminal = terminalFromStatus(state, status);
105
+ if (terminal !== undefined)
106
+ return terminal;
107
+ const authenticationState = echoesSettledChallenge(state, status)
108
+ ? state.authenticationState
109
+ : status.authentication_state;
110
+ const actionUrl = nextActionUrl(state, status, authenticationState);
111
+ const declineReason = authenticationState === 'failed_retryable'
112
+ ? (status.decline_reason ?? state.declineReason)
113
+ : undefined;
114
+ return {
115
+ ...state,
116
+ challenge: nextChallenge(state, status),
117
+ authenticationState,
118
+ actionUrl,
119
+ serverPhase: status.phase,
120
+ declineReason,
121
+ recoveryAction: status.recovery_action,
122
+ customerActionSeen: state.customerActionSeen ||
123
+ actionUrl !== undefined ||
124
+ isBlockedOnCustomerPhase(status.phase) ||
125
+ status.authentication_state === 'requires_action'
126
+ };
127
+ }
128
+ function settleChallenge(state, outcome) {
129
+ if (state.challenge === undefined)
130
+ return state;
131
+ const challenge = { ...state.challenge, status: outcome };
132
+ if (outcome === 'failed') {
133
+ return {
134
+ ...state,
135
+ challenge,
136
+ authenticationState: 'failed_retryable',
137
+ customerActionSeen: true
138
+ };
139
+ }
140
+ return {
141
+ ...state,
142
+ challenge,
143
+ authenticationState: 'processing',
144
+ actionUrl: undefined,
145
+ declineReason: undefined
146
+ };
147
+ }
148
+ export function reduceBillingOperation(state, event) {
149
+ if (state.phase !== 'pending')
150
+ return state;
151
+ switch (event.type) {
152
+ case 'status_polled':
153
+ return reducePending(state, event.status);
154
+ case 'timed_out':
155
+ return withPhase(state, 'timed_out');
156
+ case 'superseded':
157
+ return withPhase(state, 'superseded');
158
+ case 'lost':
159
+ return withPhase(state, 'reconciliation_needed');
160
+ case 'presentation_switched': {
161
+ if (event.presentation === state.presentation)
162
+ return state;
163
+ // The challenge record survives a hosted switch so a rollback keeps the
164
+ // client secret; a failed challenge becomes required again on return.
165
+ const { presentation, hostedDestination, ...rest } = state;
166
+ return event.presentation === 'hosted'
167
+ ? {
168
+ ...rest,
169
+ presentation: 'hosted',
170
+ hostedDestination: event.hostedDestination
171
+ }
172
+ : {
173
+ ...rest,
174
+ presentation: 'embedded',
175
+ ...(state.challenge?.status === 'failed'
176
+ ? { challenge: { ...state.challenge, status: 'required' } }
177
+ : {})
178
+ };
179
+ }
180
+ case 'challenge_started':
181
+ return state.challenge?.status === 'required'
182
+ ? { ...state, challenge: { ...state.challenge, status: 'in_progress' } }
183
+ : state;
184
+ case 'challenge_settled':
185
+ return settleChallenge(state, event.outcome);
186
+ }
187
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The copy keys behind the payment projection, with a default English table
3
+ * a host may override — except the safety line, which only the projection's
4
+ * `noChargeConfirmed` may ever unlock.
5
+ */
6
+ import type { PaymentProjection, PaymentReasonKey, PaymentStep } from './paymentProjection.js';
7
+ export type PaymentCopyKey = `billing.step.${PaymentStep}.header` | `billing.step.${PaymentStep}.body` | `billing.reason.${PaymentReasonKey}` | 'billing.action.retry' | 'billing.action.continue_verification' | 'billing.safety.nothing_was_charged';
8
+ declare const SAFETY_KEY = "billing.safety.nothing_was_charged";
9
+ export declare const DEFAULT_PAYMENT_COPY: Readonly<Record<PaymentCopyKey, string>>;
10
+ export declare function createPaymentCopy(overrides?: Partial<Record<PaymentCopyKey, string>>): Readonly<Record<PaymentCopyKey, string>>;
11
+ export interface PaymentCopyKeys {
12
+ readonly header: PaymentCopyKey;
13
+ readonly body: PaymentCopyKey;
14
+ readonly reason?: PaymentCopyKey;
15
+ readonly safety?: typeof SAFETY_KEY;
16
+ }
17
+ export declare function paymentCopyKeys(projection: PaymentProjection): PaymentCopyKeys;
18
+ export {};
@@ -0,0 +1,47 @@
1
+ const SAFETY_KEY = 'billing.safety.nothing_was_charged';
2
+ export const DEFAULT_PAYMENT_COPY = {
3
+ 'billing.step.select.header': 'Choose an option',
4
+ 'billing.step.select.body': 'Select a plan or credit amount.',
5
+ 'billing.step.preview.header': 'Review payment',
6
+ 'billing.step.preview.body': 'Confirm your selection.',
7
+ 'billing.step.verifying.header': 'Verify your payment',
8
+ 'billing.step.verifying.body': 'Complete verification to continue.',
9
+ 'billing.step.canceled.header': 'Payment canceled',
10
+ 'billing.step.canceled.body': 'The payment was canceled.',
11
+ 'billing.step.declined.header': 'Payment declined',
12
+ 'billing.step.declined.body': 'Try another payment method.',
13
+ 'billing.step.processing_error.header': 'Payment could not be processed',
14
+ 'billing.step.processing_error.body': 'Please try again.',
15
+ 'billing.step.payment_received_hold.header': 'Payment received',
16
+ 'billing.step.payment_received_hold.body': 'We are finishing your account update.',
17
+ 'billing.step.success.header': 'Payment complete',
18
+ 'billing.step.success.body': 'Your account is updated.',
19
+ 'billing.reason.generic': 'Something went wrong.',
20
+ 'billing.reason.checkout_expired': 'This checkout expired. Start again when you are ready.',
21
+ 'billing.reason.card_declined': 'Your bank declined the payment.',
22
+ 'billing.reason.insufficient_funds': 'Your bank reported insufficient funds.',
23
+ 'billing.reason.expired_card': 'This card has expired.',
24
+ 'billing.reason.incorrect_cvc': 'The security code did not match.',
25
+ 'billing.reason.authentication_required': 'Your bank requires authentication.',
26
+ 'billing.reason.authentication_failed': 'Authentication with your bank did not complete.',
27
+ 'billing.reason.processing_error': 'The payment could not be processed right now.',
28
+ 'billing.action.retry': 'Try again',
29
+ 'billing.action.continue_verification': 'Continue verification',
30
+ [SAFETY_KEY]: 'Nothing was charged.'
31
+ };
32
+ export function createPaymentCopy(overrides = {}) {
33
+ const { [SAFETY_KEY]: _protected, ...safe } = overrides;
34
+ return { ...DEFAULT_PAYMENT_COPY, ...safe };
35
+ }
36
+ export function paymentCopyKeys(projection) {
37
+ return {
38
+ header: `billing.step.${projection.step}.header`,
39
+ body: `billing.step.${projection.step}.body`,
40
+ ...(projection.reasonKey === undefined
41
+ ? {}
42
+ : { reason: `billing.reason.${projection.reasonKey}` }),
43
+ ...(projection.step === 'canceled' && projection.noChargeConfirmed
44
+ ? { safety: SAFETY_KEY }
45
+ : {})
46
+ };
47
+ }
@@ -0,0 +1,40 @@
1
+ import type { zSavedPaymentMethod } from '@comfyorg/ingest-types/zod';
2
+ import type { z } from 'zod';
3
+ import type { BillingResult, BillingTransport } from './billingContracts.js';
4
+ import type { BillingScope, BillingScopeSource } from './billingScope.js';
5
+ export declare const PAYMENT_METHODS_ROUTE = "/billing/payment-methods";
6
+ export type SavedPaymentMethod = z.infer<typeof zSavedPaymentMethod>;
7
+ export type PaymentMethodsScope = BillingScope;
8
+ export interface PaymentMethodsSnapshot {
9
+ readonly scope: PaymentMethodsScope;
10
+ readonly methods: readonly SavedPaymentMethod[];
11
+ /** ms since epoch at which this read settled. */
12
+ readonly readAt: number;
13
+ }
14
+ export interface PaymentMethodsReadOptions {
15
+ readonly signal?: AbortSignal;
16
+ /** Bounds the request, when this caller is the one that issues it. */
17
+ readonly timeoutMs?: number;
18
+ }
19
+ export interface PaymentMethodsReader {
20
+ /**
21
+ * Always requests. Concurrent callers for one scope share a single request.
22
+ */
23
+ read: (options?: PaymentMethodsReadOptions) => Promise<BillingResult<PaymentMethodsSnapshot>>;
24
+ /** The last list published for the current scope. */
25
+ getSnapshot: () => PaymentMethodsSnapshot | undefined;
26
+ /**
27
+ * Drops the published list without a scope change. A payment-portal round
28
+ * trip adds or removes a card outside this reader, so the host that sent the
29
+ * user there is the only thing that knows the list it holds is now wrong.
30
+ */
31
+ invalidate: () => void;
32
+ dispose: () => void;
33
+ }
34
+ export interface PaymentMethodsReaderOptions {
35
+ readonly transport: BillingTransport;
36
+ /** Where the core learns which user, workspace, and role it runs as. */
37
+ readonly scopeSource: BillingScopeSource;
38
+ readonly now?: () => number;
39
+ }
40
+ export declare function createPaymentMethodsReader(options: PaymentMethodsReaderOptions): PaymentMethodsReader;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The saved payment methods read: the cards a workspace can charge, cached per
3
+ * scope.
4
+ *
5
+ * Card details never reach this core beyond what the server already publishes —
6
+ * brand, last four digits, and which one is the default — so nothing here has
7
+ * to be redacted before a host renders it.
8
+ */
9
+ import { zListSavedPaymentMethodsResponse } from '@comfyorg/ingest-types/zod';
10
+ import { createScopedReader } from './scopedReader.js';
11
+ export const PAYMENT_METHODS_ROUTE = '/billing/payment-methods';
12
+ export function createPaymentMethodsReader(options) {
13
+ const { transport, scopeSource, now = Date.now } = options;
14
+ const reader = createScopedReader({
15
+ transport,
16
+ scopeSource,
17
+ route: PAYMENT_METHODS_ROUTE,
18
+ parse: (body) => zListSavedPaymentMethodsResponse.safeParse(body),
19
+ timeoutMs: (readOptions) => readOptions?.timeoutMs,
20
+ project: ({ data }, scope) => ({
21
+ status: 'ok',
22
+ value: { scope, methods: data, readAt: now() }
23
+ }),
24
+ // A list resolved before the change the host is reporting may be served to
25
+ // the callers already waiting on it, but must not be published.
26
+ publish: (value, fenced) => (fenced ? undefined : value)
27
+ });
28
+ return {
29
+ read: reader.read,
30
+ getSnapshot: reader.getSnapshot,
31
+ invalidate: () => {
32
+ reader.setSnapshot(undefined);
33
+ reader.fenceInFlight({ detach: true });
34
+ },
35
+ dispose: reader.dispose
36
+ };
37
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The eight public payment states as a pure projection of one lifecycle
3
+ * operation. A host renders the projection and reports where it is when no
4
+ * operation exists; it never advances the machine itself.
5
+ *
6
+ * A server verdict outranks anything the host reports: a customer who backed
7
+ * out of a page after the charge went through still sees the success.
8
+ */
9
+ import type { BillingDeclineReason, BillingOperationState, BillingRecoveryAction } from './operationState.js';
10
+ export type PaymentStep = 'select' | 'preview' | 'verifying' | 'canceled' | 'declined' | 'processing_error'
11
+ /** Reserved: the contract cannot yet tell "paid, entitlement pending" from "needs a human". */
12
+ | 'payment_received_hold' | 'success';
13
+ /** Where the host is when nothing is being observed, or the customer backed out. */
14
+ export type HostPaymentStep = 'select' | 'preview' | 'canceled';
15
+ /**
16
+ * The generated `decline_reason` values plus the two the SDK adds. Every key
17
+ * is a coded reason; raw server or provider text never becomes one.
18
+ * `checkout_expired` is reserved: the generated operation status has no
19
+ * expired state to produce it from.
20
+ */
21
+ export type PaymentReasonKey = BillingDeclineReason | 'checkout_expired' | 'generic';
22
+ export interface PaymentProjection {
23
+ readonly step: PaymentStep;
24
+ readonly reasonKey?: PaymentReasonKey;
25
+ readonly recoveryAction?: BillingRecoveryAction;
26
+ /** Present whenever an operation backs the projection; the id support can act on. */
27
+ readonly operationId?: string;
28
+ /**
29
+ * Permits "nothing was charged" only once a terminal backend result rules
30
+ * out any later charge; the contract has no such field yet, so always false.
31
+ */
32
+ readonly noChargeConfirmed: boolean;
33
+ }
34
+ export declare function projectPaymentStep(operation: BillingOperationState | undefined, hostStep: HostPaymentStep): PaymentProjection;
@@ -0,0 +1,56 @@
1
+ /** Reasons that describe a processing fault rather than the customer's card. */
2
+ const PROCESSING_REASONS = new Set([
3
+ 'processing_error',
4
+ 'generic'
5
+ ]);
6
+ function stepForReason(reason) {
7
+ return PROCESSING_REASONS.has(reason) ? 'processing_error' : 'declined';
8
+ }
9
+ function projectPending(state, hostStep) {
10
+ const base = { operationId: state.id, noChargeConfirmed: false };
11
+ if (hostStep === 'canceled')
12
+ return { ...base, step: 'canceled' };
13
+ const reason = state.declineReason ??
14
+ (state.challenge?.status === 'failed' ? 'authentication_failed' : undefined);
15
+ if (reason !== undefined) {
16
+ return {
17
+ ...base,
18
+ step: stepForReason(reason),
19
+ reasonKey: reason,
20
+ ...(state.recoveryAction === undefined
21
+ ? {}
22
+ : { recoveryAction: state.recoveryAction })
23
+ };
24
+ }
25
+ const parked = state.challenge !== undefined || state.actionUrl !== undefined;
26
+ return { ...base, step: parked ? 'verifying' : 'preview' };
27
+ }
28
+ export function projectPaymentStep(operation, hostStep) {
29
+ if (operation === undefined) {
30
+ return { step: hostStep, noChargeConfirmed: false };
31
+ }
32
+ const base = { operationId: operation.id, noChargeConfirmed: false };
33
+ switch (operation.phase) {
34
+ case 'pending':
35
+ return projectPending(operation, hostStep);
36
+ case 'succeeded':
37
+ return { ...base, step: 'success' };
38
+ case 'failed':
39
+ return {
40
+ ...base,
41
+ step: stepForReason(operation.declineReason),
42
+ reasonKey: operation.declineReason,
43
+ ...(operation.recoveryAction === undefined
44
+ ? {}
45
+ : { recoveryAction: operation.recoveryAction })
46
+ };
47
+ case 'reconciliation_needed':
48
+ return { ...base, step: 'processing_error', reasonKey: 'generic' };
49
+ case 'timed_out':
50
+ return hostStep === 'canceled'
51
+ ? { ...base, step: 'canceled' }
52
+ : { ...base, step: 'processing_error', reasonKey: 'generic' };
53
+ case 'superseded':
54
+ return { ...base, step: hostStep === 'canceled' ? 'canceled' : 'select' };
55
+ }
56
+ }