@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,367 @@
1
+ /**
2
+ * The subscription commands on the shared operation lifecycle: subscribe,
3
+ * resubscribe, cancel, and the payment portal. Each one encodes its request
4
+ * against the generated contract, issues it through `lifecycle.begin`, and
5
+ * waits for the settled operation; polling, routing, recovery, and telemetry
6
+ * stay in the lifecycle. `previewSubscribe` sits beside them without a
7
+ * lifecycle: it only quotes a change.
8
+ *
9
+ * No command decides for the server whether a transition is allowed
10
+ * (`../../../docs/billing-command-eligibility.md`). Server codes are matched
11
+ * against the closed set named here; a code outside it reaches the caller only
12
+ * as the transport's coded failure.
13
+ */
14
+ import { zSubscribeRequest, zSubscribeResponse } from '@comfyorg/ingest-types/zod';
15
+ import { z } from 'zod';
16
+ import type { BillingFailure, BillingTransport } from './billingContracts.js';
17
+ import type { CapabilitiesReader } from './capabilities.js';
18
+ import type { CreditsReader } from './credits.js';
19
+ import type { BillingOperationLifecycle } from './operationLifecycle.js';
20
+ import type { BillingOperationState, PendingBillingOperation } from './operationState.js';
21
+ import type { BillingStatusReader } from './status.js';
22
+ export declare const SUBSCRIBE_ROUTE = "/billing/subscribe";
23
+ export declare const RESUBSCRIBE_ROUTE = "/billing/subscription/resubscribe";
24
+ export declare const CANCEL_SUBSCRIPTION_ROUTE = "/billing/subscription/cancel";
25
+ export declare const PAYMENT_PORTAL_ROUTE = "/billing/payment-portal";
26
+ export declare const PREVIEW_SUBSCRIBE_ROUTE = "/billing/preview-subscribe";
27
+ export type SubscribeInput = z.infer<typeof zSubscribeRequest>;
28
+ export type TerminalBillingOperation = Exclude<BillingOperationState, PendingBillingOperation>;
29
+ /** Outcomes a subscription command adds to the shared `BillingErrorCode` set. */
30
+ export type SubscriptionCommandCode =
31
+ /** The input fails the generated request contract; nothing was sent. */
32
+ 'INVALID_REQUEST'
33
+ /** `confirmation_token` and `saved_payment_method_id` were both given. */
34
+ | 'CONFLICTING_PAYMENT_METHOD'
35
+ /** The server wants the host to re-preview and resend with `confirm_reactivation`. */
36
+ | 'REACTIVATION_CONFIRMATION_REQUIRED' | 'NO_ACTIVE_SUBSCRIPTION'
37
+ /** The server asked for a hosted payment step but offered no page for it. */
38
+ | 'MISSING_PAYMENT_METHOD_URL';
39
+ export type SubscriptionCommandFailure = BillingFailure | {
40
+ readonly status: 'error';
41
+ readonly code: SubscriptionCommandCode;
42
+ };
43
+ /** What the subscribe route answered before the lifecycle drove the operation on. */
44
+ type SubscribeIssuedStatus = z.infer<typeof zSubscribeResponse>['status'];
45
+ export interface SubscriptionCommandOutcome {
46
+ readonly phase: TerminalBillingOperation['phase'];
47
+ /** Absent when the requested state already held and nothing was issued. */
48
+ readonly operation?: TerminalBillingOperation;
49
+ /**
50
+ * The `status` the subscribe response carried, before the lifecycle settled
51
+ * the operation: `subscribed` is a plan the server activated on the spot,
52
+ * the other two are payment steps it could not complete without the
53
+ * customer. Absent for every other command, and whenever this call read no
54
+ * subscribe response at all: `begin` adopted an operation the server was
55
+ * already settling, or joined one another `subscription` command had in
56
+ * flight. Both land on `requiredPayment: true`, the side an operation the
57
+ * server is mid-payment on belongs to.
58
+ */
59
+ readonly issuedStatus?: SubscribeIssuedStatus;
60
+ }
61
+ export type SubscriptionCommandResult = {
62
+ readonly status: 'ok';
63
+ readonly value: SubscriptionCommandOutcome;
64
+ } | SubscriptionCommandFailure;
65
+ export type PaymentPortalResult = {
66
+ readonly status: 'ok';
67
+ readonly value: {
68
+ readonly url: string;
69
+ };
70
+ } | BillingFailure;
71
+ declare const PreviewSchema: z.ZodObject<{
72
+ allowed: z.ZodBoolean;
73
+ currency: z.ZodOptional<z.ZodString>;
74
+ effective_at: z.ZodString;
75
+ is_immediate: z.ZodBoolean;
76
+ payment_method_configuration_id: z.ZodOptional<z.ZodString>;
77
+ promotion_code: z.ZodOptional<z.ZodString>;
78
+ proration_at: z.ZodOptional<z.ZodString>;
79
+ quote_id: z.ZodOptional<z.ZodString>;
80
+ quote_version: z.ZodOptional<z.ZodNumber>;
81
+ reason: z.ZodOptional<z.ZodString>;
82
+ renewal_at: z.ZodOptional<z.ZodString>;
83
+ requires_reactivation_confirmation: z.ZodOptional<z.ZodBoolean>;
84
+ transition_type: z.ZodEnum<["new_subscription", "upgrade", "downgrade", "duration_change"]>;
85
+ } & {
86
+ amount_due_cents: z.ZodOptional<z.ZodNumber>;
87
+ cost_next_period_cents: z.ZodNumber;
88
+ cost_today_cents: z.ZodNumber;
89
+ credits_next_period_cents: z.ZodNumber;
90
+ credits_today_cents: z.ZodNumber;
91
+ renewal_amount_cents: z.ZodOptional<z.ZodNumber>;
92
+ current_plan: z.ZodOptional<z.ZodObject<{
93
+ duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
94
+ period_end: z.ZodOptional<z.ZodString>;
95
+ period_start: z.ZodOptional<z.ZodString>;
96
+ slug: z.ZodString;
97
+ tier: z.ZodEnum<["FREE", "STANDARD", "CREATOR", "PRO", "FOUNDERS_EDITION", "TEAM", "ENTERPRISE"]>;
98
+ } & {
99
+ credits_cents: z.ZodNumber;
100
+ price_cents: z.ZodNumber;
101
+ seat_summary: z.ZodObject<{
102
+ seat_count: z.ZodNumber;
103
+ } & {
104
+ total_cost_cents: z.ZodNumber;
105
+ total_credits_cents: z.ZodNumber;
106
+ }, "strip", z.ZodTypeAny, {
107
+ seat_count: number;
108
+ total_cost_cents: number;
109
+ total_credits_cents: number;
110
+ }, {
111
+ seat_count: number;
112
+ total_cost_cents: number;
113
+ total_credits_cents: number;
114
+ }>;
115
+ }, "strip", z.ZodTypeAny, {
116
+ price_cents: number;
117
+ credits_cents: number;
118
+ duration: "MONTHLY" | "ANNUAL";
119
+ seat_summary: {
120
+ seat_count: number;
121
+ total_cost_cents: number;
122
+ total_credits_cents: number;
123
+ };
124
+ slug: string;
125
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
126
+ period_end?: string | undefined;
127
+ period_start?: string | undefined;
128
+ }, {
129
+ price_cents: number;
130
+ credits_cents: number;
131
+ duration: "MONTHLY" | "ANNUAL";
132
+ seat_summary: {
133
+ seat_count: number;
134
+ total_cost_cents: number;
135
+ total_credits_cents: number;
136
+ };
137
+ slug: string;
138
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
139
+ period_end?: string | undefined;
140
+ period_start?: string | undefined;
141
+ }>>;
142
+ new_plan: z.ZodObject<{
143
+ duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
144
+ period_end: z.ZodOptional<z.ZodString>;
145
+ period_start: z.ZodOptional<z.ZodString>;
146
+ slug: z.ZodString;
147
+ tier: z.ZodEnum<["FREE", "STANDARD", "CREATOR", "PRO", "FOUNDERS_EDITION", "TEAM", "ENTERPRISE"]>;
148
+ } & {
149
+ credits_cents: z.ZodNumber;
150
+ price_cents: z.ZodNumber;
151
+ seat_summary: z.ZodObject<{
152
+ seat_count: z.ZodNumber;
153
+ } & {
154
+ total_cost_cents: z.ZodNumber;
155
+ total_credits_cents: z.ZodNumber;
156
+ }, "strip", z.ZodTypeAny, {
157
+ seat_count: number;
158
+ total_cost_cents: number;
159
+ total_credits_cents: number;
160
+ }, {
161
+ seat_count: number;
162
+ total_cost_cents: number;
163
+ total_credits_cents: number;
164
+ }>;
165
+ }, "strip", z.ZodTypeAny, {
166
+ price_cents: number;
167
+ credits_cents: number;
168
+ duration: "MONTHLY" | "ANNUAL";
169
+ seat_summary: {
170
+ seat_count: number;
171
+ total_cost_cents: number;
172
+ total_credits_cents: number;
173
+ };
174
+ slug: string;
175
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
176
+ period_end?: string | undefined;
177
+ period_start?: string | undefined;
178
+ }, {
179
+ price_cents: number;
180
+ credits_cents: number;
181
+ duration: "MONTHLY" | "ANNUAL";
182
+ seat_summary: {
183
+ seat_count: number;
184
+ total_cost_cents: number;
185
+ total_credits_cents: number;
186
+ };
187
+ slug: string;
188
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
189
+ period_end?: string | undefined;
190
+ period_start?: string | undefined;
191
+ }>;
192
+ discounts: z.ZodOptional<z.ZodArray<z.ZodObject<{
193
+ code: z.ZodString;
194
+ kind: z.ZodEnum<["plan", "promotion"]>;
195
+ name: z.ZodOptional<z.ZodString>;
196
+ } & {
197
+ amount_off_cents: z.ZodOptional<z.ZodNumber>;
198
+ }, "strip", z.ZodTypeAny, {
199
+ code: string;
200
+ kind: "plan" | "promotion";
201
+ name?: string | undefined;
202
+ amount_off_cents?: number | undefined;
203
+ }, {
204
+ code: string;
205
+ kind: "plan" | "promotion";
206
+ name?: string | undefined;
207
+ amount_off_cents?: number | undefined;
208
+ }>, "many">>;
209
+ }, "strip", z.ZodTypeAny, {
210
+ effective_at: string;
211
+ allowed: boolean;
212
+ cost_next_period_cents: number;
213
+ cost_today_cents: number;
214
+ credits_next_period_cents: number;
215
+ credits_today_cents: number;
216
+ is_immediate: boolean;
217
+ new_plan: {
218
+ price_cents: number;
219
+ credits_cents: number;
220
+ duration: "MONTHLY" | "ANNUAL";
221
+ seat_summary: {
222
+ seat_count: number;
223
+ total_cost_cents: number;
224
+ total_credits_cents: number;
225
+ };
226
+ slug: string;
227
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
228
+ period_end?: string | undefined;
229
+ period_start?: string | undefined;
230
+ };
231
+ transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
232
+ currency?: string | undefined;
233
+ promotion_code?: string | undefined;
234
+ proration_at?: string | undefined;
235
+ quote_id?: string | undefined;
236
+ quote_version?: number | undefined;
237
+ amount_due_cents?: number | undefined;
238
+ current_plan?: {
239
+ price_cents: number;
240
+ credits_cents: number;
241
+ duration: "MONTHLY" | "ANNUAL";
242
+ seat_summary: {
243
+ seat_count: number;
244
+ total_cost_cents: number;
245
+ total_credits_cents: number;
246
+ };
247
+ slug: string;
248
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
249
+ period_end?: string | undefined;
250
+ period_start?: string | undefined;
251
+ } | undefined;
252
+ discounts?: {
253
+ code: string;
254
+ kind: "plan" | "promotion";
255
+ name?: string | undefined;
256
+ amount_off_cents?: number | undefined;
257
+ }[] | undefined;
258
+ payment_method_configuration_id?: string | undefined;
259
+ reason?: string | undefined;
260
+ renewal_amount_cents?: number | undefined;
261
+ renewal_at?: string | undefined;
262
+ requires_reactivation_confirmation?: boolean | undefined;
263
+ }, {
264
+ effective_at: string;
265
+ allowed: boolean;
266
+ cost_next_period_cents: number;
267
+ cost_today_cents: number;
268
+ credits_next_period_cents: number;
269
+ credits_today_cents: number;
270
+ is_immediate: boolean;
271
+ new_plan: {
272
+ price_cents: number;
273
+ credits_cents: number;
274
+ duration: "MONTHLY" | "ANNUAL";
275
+ seat_summary: {
276
+ seat_count: number;
277
+ total_cost_cents: number;
278
+ total_credits_cents: number;
279
+ };
280
+ slug: string;
281
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
282
+ period_end?: string | undefined;
283
+ period_start?: string | undefined;
284
+ };
285
+ transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
286
+ currency?: string | undefined;
287
+ promotion_code?: string | undefined;
288
+ proration_at?: string | undefined;
289
+ quote_id?: string | undefined;
290
+ quote_version?: number | undefined;
291
+ amount_due_cents?: number | undefined;
292
+ current_plan?: {
293
+ price_cents: number;
294
+ credits_cents: number;
295
+ duration: "MONTHLY" | "ANNUAL";
296
+ seat_summary: {
297
+ seat_count: number;
298
+ total_cost_cents: number;
299
+ total_credits_cents: number;
300
+ };
301
+ slug: string;
302
+ tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
303
+ period_end?: string | undefined;
304
+ period_start?: string | undefined;
305
+ } | undefined;
306
+ discounts?: {
307
+ code: string;
308
+ kind: "plan" | "promotion";
309
+ name?: string | undefined;
310
+ amount_off_cents?: number | undefined;
311
+ }[] | undefined;
312
+ payment_method_configuration_id?: string | undefined;
313
+ reason?: string | undefined;
314
+ renewal_amount_cents?: number | undefined;
315
+ renewal_at?: string | undefined;
316
+ requires_reactivation_confirmation?: boolean | undefined;
317
+ }>;
318
+ /**
319
+ * The quote the server returns for a plan change, in the generated field names.
320
+ *
321
+ * Its strings are product copy, not `serverCode`'s kind of machine identifier:
322
+ * a `discounts[]` entry describes a discount the server applied, and a
323
+ * `promotion` one carries back the very `promotionCode` the caller sent, so a
324
+ * host renders `code` and `name` rather than matching them.
325
+ */
326
+ export type SubscriptionPreview = z.infer<typeof PreviewSchema>;
327
+ export interface PreviewSubscribeInput {
328
+ readonly planSlug: string;
329
+ readonly promotionCode?: string;
330
+ readonly teamCreditStopId?: string;
331
+ readonly checkoutAttemptId?: string;
332
+ }
333
+ export interface PreviewSubscribeOptions {
334
+ readonly signal?: AbortSignal;
335
+ readonly timeoutMs?: number;
336
+ }
337
+ export type PreviewSubscribeResult = {
338
+ readonly status: 'ok';
339
+ readonly value: SubscriptionPreview;
340
+ } | SubscriptionCommandFailure;
341
+ export interface BillingCommandsOptions {
342
+ readonly transport: BillingTransport;
343
+ readonly lifecycle: BillingOperationLifecycle;
344
+ readonly statusReader: BillingStatusReader;
345
+ readonly capabilities: CapabilitiesReader;
346
+ readonly credits: CreditsReader;
347
+ /** One key per attempt; the backend's dedupe key and the transport's replay gate. */
348
+ readonly idempotencyKey?: () => string;
349
+ }
350
+ export interface BillingCommands {
351
+ subscribe: (input: SubscribeInput) => Promise<SubscriptionCommandResult>;
352
+ /**
353
+ * Quotes a plan change without issuing an operation or touching the
354
+ * capability cache: a read the backend happens to shape as a POST. It sends
355
+ * no idempotency key, so a stale token surfaces as the transient
356
+ * `REQUEST_FAILED` that callers already retry rather than as a denial.
357
+ */
358
+ previewSubscribe: (input: PreviewSubscribeInput, options?: PreviewSubscribeOptions) => Promise<PreviewSubscribeResult>;
359
+ resubscribe: () => Promise<SubscriptionCommandResult>;
360
+ cancelSubscription: () => Promise<SubscriptionCommandResult>;
361
+ /** Returns the portal URL; the host decides how to open it. */
362
+ openPaymentPortal: (input: {
363
+ readonly returnUrl?: string;
364
+ }) => Promise<PaymentPortalResult>;
365
+ }
366
+ export declare function createBillingCommands(options: BillingCommandsOptions): BillingCommands;
367
+ export {};
@@ -0,0 +1,263 @@
1
+ /**
2
+ * The subscription commands on the shared operation lifecycle: subscribe,
3
+ * resubscribe, cancel, and the payment portal. Each one encodes its request
4
+ * against the generated contract, issues it through `lifecycle.begin`, and
5
+ * waits for the settled operation; polling, routing, recovery, and telemetry
6
+ * stay in the lifecycle. `previewSubscribe` sits beside them without a
7
+ * lifecycle: it only quotes a change.
8
+ *
9
+ * No command decides for the server whether a transition is allowed
10
+ * (`../../../docs/billing-command-eligibility.md`). Server codes are matched
11
+ * against the closed set named here; a code outside it reaches the caller only
12
+ * as the transport's coded failure.
13
+ */
14
+ import { zCancelSubscriptionResponse2, zPaymentPortalResponse, zPreviewPlanInfo, zPreviewSubscribeRequest, zPreviewSubscribeResponse, zResubscribeResponse, zSubscribeRequest, zSubscribeResponse, zSubscriptionDiscount } from '@comfyorg/ingest-types/zod';
15
+ import { z } from 'zod';
16
+ import { matchesServerCode } from './billingContracts.js';
17
+ import { validateActionUrl } from './operationState.js';
18
+ import { readValidatedBillingResponse } from './sharedRead.js';
19
+ export const SUBSCRIBE_ROUTE = '/billing/subscribe';
20
+ export const RESUBSCRIBE_ROUTE = '/billing/subscription/resubscribe';
21
+ export const CANCEL_SUBSCRIPTION_ROUTE = '/billing/subscription/cancel';
22
+ export const PAYMENT_PORTAL_ROUTE = '/billing/payment-portal';
23
+ export const PREVIEW_SUBSCRIBE_ROUTE = '/billing/preview-subscribe';
24
+ /** The closed set of `serverCode` values these commands act on. */
25
+ const NO_ACTIVE_SUBSCRIPTION_SERVER_CODE = 'NO_ACTIVE_SUBSCRIPTION';
26
+ const REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE = 'REACTIVATION_CONFIRMATION_REQUIRED';
27
+ const NOT_SCHEDULED_FOR_CANCELLATION_SERVER_CODE = 'NOT_SCHEDULED_FOR_CANCELLATION';
28
+ const ALREADY_CANCELED_SERVER_CODE = 'ALREADY_CANCELED';
29
+ /**
30
+ * The generated schema coerces every int64 to a `bigint`, which no caller can
31
+ * add to a price or hand to a currency formatter — and the generated *type*
32
+ * for the same field is a `number`. Money on this route is bounded to cents
33
+ * well inside the JavaScript-safe range, so the cents are read as numbers, the
34
+ * way `capabilities` reads `revision` — as whole units of currency that
35
+ * survive arithmetic, since these amounts are displayed as prices and
36
+ * confirmed as charges.
37
+ */
38
+ const cents = z.number().int().safe();
39
+ const PlanInfoSchema = zPreviewPlanInfo.extend({
40
+ credits_cents: cents,
41
+ price_cents: cents,
42
+ seat_summary: zPreviewPlanInfo.shape.seat_summary.extend({
43
+ total_cost_cents: cents,
44
+ total_credits_cents: cents
45
+ })
46
+ });
47
+ const PreviewSchema = zPreviewSubscribeResponse.extend({
48
+ amount_due_cents: cents.optional(),
49
+ cost_next_period_cents: cents,
50
+ cost_today_cents: cents,
51
+ credits_next_period_cents: cents,
52
+ credits_today_cents: cents,
53
+ renewal_amount_cents: cents.optional(),
54
+ current_plan: PlanInfoSchema.optional(),
55
+ new_plan: PlanInfoSchema,
56
+ discounts: z
57
+ .array(zSubscriptionDiscount.extend({ amount_off_cents: cents.optional() }))
58
+ .optional()
59
+ });
60
+ const ALREADY_HELD = {
61
+ status: 'ok',
62
+ value: { phase: 'succeeded' }
63
+ };
64
+ function coded(code) {
65
+ return { status: 'error', code };
66
+ }
67
+ /**
68
+ * A server code the caller's request already satisfies is a success, but
69
+ * only from a 4xx: a 5xx echoing the code is an upstream failure that
70
+ * happens to carry it, and the requested state cannot be assumed to hold.
71
+ */
72
+ function alreadyInRequestedState(failure, serverCode) {
73
+ return (matchesServerCode(failure, serverCode) &&
74
+ failure.httpStatus !== undefined &&
75
+ failure.httpStatus >= 400 &&
76
+ failure.httpStatus < 500);
77
+ }
78
+ function mapServerCode(failure, alreadyHeldCode) {
79
+ if (alreadyInRequestedState(failure, alreadyHeldCode)) {
80
+ return { status: 'already_held' };
81
+ }
82
+ return matchesServerCode(failure, NO_ACTIVE_SUBSCRIPTION_SERVER_CODE)
83
+ ? coded('NO_ACTIVE_SUBSCRIPTION')
84
+ : failure;
85
+ }
86
+ function dropEmpty(value) {
87
+ return value === '' ? undefined : value;
88
+ }
89
+ function previewRequestBody(input) {
90
+ const { planSlug, promotionCode, teamCreditStopId, checkoutAttemptId } = input;
91
+ return {
92
+ plan_slug: planSlug,
93
+ ...(promotionCode === undefined ? {} : { promotion_code: promotionCode }),
94
+ ...(teamCreditStopId === undefined
95
+ ? {}
96
+ : { team_credit_stop_id: teamCreditStopId }),
97
+ ...(checkoutAttemptId === undefined
98
+ ? {}
99
+ : { checkout_attempt_id: checkoutAttemptId })
100
+ };
101
+ }
102
+ export function createBillingCommands(options) {
103
+ const { transport, lifecycle, statusReader, capabilities, credits, idempotencyKey = () => crypto.randomUUID() } = options;
104
+ async function refreshAfterSuccess() {
105
+ capabilities.invalidate();
106
+ await Promise.all([credits.read(), statusReader.read()]);
107
+ }
108
+ function post(route, body, parse, key) {
109
+ return readValidatedBillingResponse(transport, {
110
+ method: 'POST',
111
+ route,
112
+ body,
113
+ ...(key === undefined ? {} : { idempotencyKey: key })
114
+ }, parse);
115
+ }
116
+ async function settle(kind, issue) {
117
+ // The lifecycle carries only the shared codes; the command's own verdict
118
+ // and what it read off the issuing response wait here for `begin` to
119
+ // return. Both stay unset whenever `begin` never ran this caller's issue:
120
+ // it adopted an operation the server was already settling, or joined one
121
+ // another command of the same kind had in flight.
122
+ const verdict = {};
123
+ const issued = {};
124
+ const began = await lifecycle.begin(kind, async () => {
125
+ const outcome = await issue();
126
+ if (outcome.status === 'ok') {
127
+ issued.value = outcome;
128
+ return outcome;
129
+ }
130
+ verdict.value = outcome;
131
+ return { status: 'error', code: 'REQUEST_FAILED' };
132
+ });
133
+ if (verdict.value !== undefined) {
134
+ if (verdict.value.status !== 'already_held')
135
+ return verdict.value;
136
+ await refreshAfterSuccess();
137
+ return ALREADY_HELD;
138
+ }
139
+ if (began.status === 'error')
140
+ return began;
141
+ const settled = lifecycle.settled(began.value.id);
142
+ const operation = settled === undefined ? began.value : await settled;
143
+ if (operation.phase === 'pending') {
144
+ return { status: 'error', code: 'SUPERSEDED' };
145
+ }
146
+ if (operation.phase === 'succeeded')
147
+ await refreshAfterSuccess();
148
+ const issuedStatus = issued.value?.issuedStatus;
149
+ return {
150
+ status: 'ok',
151
+ value: {
152
+ phase: operation.phase,
153
+ operation,
154
+ ...(issuedStatus === undefined ? {} : { issuedStatus })
155
+ }
156
+ };
157
+ }
158
+ async function issueSubscribe(input) {
159
+ const key = idempotencyKey();
160
+ const response = await post(SUBSCRIBE_ROUTE, { ...input, idempotency_key: key }, (body) => zSubscribeResponse.safeParse(body), key);
161
+ if (response.status === 'error') {
162
+ return matchesServerCode(response, REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE)
163
+ ? coded('REACTIVATION_CONFIRMATION_REQUIRED')
164
+ : response;
165
+ }
166
+ const { billing_op_id, status, payment_method_url } = response.value.data;
167
+ if (status !== 'needs_payment_method') {
168
+ return {
169
+ status: 'ok',
170
+ value: { operationId: billing_op_id },
171
+ issuedStatus: status
172
+ };
173
+ }
174
+ if (payment_method_url === undefined) {
175
+ return coded('MISSING_PAYMENT_METHOD_URL');
176
+ }
177
+ return {
178
+ status: 'ok',
179
+ value: { operationId: billing_op_id, actionUrl: payment_method_url },
180
+ issuedStatus: status
181
+ };
182
+ }
183
+ async function issueResubscribe() {
184
+ const key = idempotencyKey();
185
+ const response = await post(RESUBSCRIBE_ROUTE, { idempotency_key: key }, (body) => zResubscribeResponse.safeParse(body), key);
186
+ if (response.status === 'error') {
187
+ return mapServerCode(response, NOT_SCHEDULED_FOR_CANCELLATION_SERVER_CODE);
188
+ }
189
+ return {
190
+ status: 'ok',
191
+ value: { operationId: response.value.data.billing_op_id }
192
+ };
193
+ }
194
+ async function issueCancel() {
195
+ const key = idempotencyKey();
196
+ const response = await post(CANCEL_SUBSCRIPTION_ROUTE, { idempotency_key: key }, (body) => zCancelSubscriptionResponse2.safeParse(body), key);
197
+ if (response.status === 'error') {
198
+ return mapServerCode(response, ALREADY_CANCELED_SERVER_CODE);
199
+ }
200
+ return {
201
+ status: 'ok',
202
+ value: { operationId: response.value.data.billing_op_id }
203
+ };
204
+ }
205
+ const resubscribe = () => settle('subscription', issueResubscribe);
206
+ async function subscribe(input) {
207
+ const confirmationToken = dropEmpty(input.confirmation_token);
208
+ const savedPaymentMethodId = dropEmpty(input.saved_payment_method_id);
209
+ if (confirmationToken !== undefined && savedPaymentMethodId !== undefined) {
210
+ return coded('CONFLICTING_PAYMENT_METHOD');
211
+ }
212
+ // JSON drops `undefined` but keeps `''`, so an empty credential would
213
+ // reach the server as a present-but-meaningless value.
214
+ const request = {
215
+ ...input,
216
+ confirmation_token: confirmationToken,
217
+ saved_payment_method_id: savedPaymentMethodId
218
+ };
219
+ if (!zSubscribeRequest.safeParse(request).success) {
220
+ return coded('INVALID_REQUEST');
221
+ }
222
+ return settle('subscription', () => issueSubscribe(request));
223
+ }
224
+ async function previewSubscribe(input, options = {}) {
225
+ const body = previewRequestBody(input);
226
+ if (!zPreviewSubscribeRequest.safeParse(body).success) {
227
+ return coded('INVALID_REQUEST');
228
+ }
229
+ const response = await readValidatedBillingResponse(transport, {
230
+ method: 'POST',
231
+ route: PREVIEW_SUBSCRIBE_ROUTE,
232
+ body,
233
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
234
+ ...(options.timeoutMs === undefined
235
+ ? {}
236
+ : { timeoutMs: options.timeoutMs })
237
+ }, (raw) => PreviewSchema.safeParse(raw));
238
+ return response.status === 'error'
239
+ ? response
240
+ : { status: 'ok', value: response.value.data };
241
+ }
242
+ async function openPaymentPortal(input) {
243
+ const response = await post(PAYMENT_PORTAL_ROUTE, input.returnUrl === undefined ? {} : { return_url: input.returnUrl }, (body) => zPaymentPortalResponse.safeParse(body));
244
+ if (response.status === 'error')
245
+ return response;
246
+ const url = validateActionUrl(response.value.data.url);
247
+ if (url === undefined) {
248
+ return {
249
+ status: 'error',
250
+ code: 'MALFORMED_RESPONSE',
251
+ httpStatus: response.value.httpStatus
252
+ };
253
+ }
254
+ return { status: 'ok', value: { url } };
255
+ }
256
+ return {
257
+ subscribe,
258
+ previewSubscribe,
259
+ resubscribe,
260
+ cancelSubscription: () => settle('cancel', issueCancel),
261
+ openPaymentPortal
262
+ };
263
+ }