@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,9 +3,35 @@ const PROCESSING_REASONS = new Set([
3
3
  'processing_error',
4
4
  'generic'
5
5
  ]);
6
+ const RECOVERY_ACTIONS = {
7
+ retry: true,
8
+ replace_payment_method: true,
9
+ authenticate_payment: true,
10
+ contact_support: true
11
+ };
12
+ /** A recovery action this build cannot act on reads as none named. */
13
+ function knownRecoveryAction(action) {
14
+ return action !== undefined && isRecoveryAction(action) ? action : undefined;
15
+ }
16
+ function isRecoveryAction(action) {
17
+ return Object.hasOwn(RECOVERY_ACTIONS, action);
18
+ }
6
19
  function stepForReason(reason) {
7
20
  return PROCESSING_REASONS.has(reason) ? 'processing_error' : 'declined';
8
21
  }
22
+ /** A completed challenge waits on the server, not on the customer. */
23
+ function awaitsVerification(state) {
24
+ const challengeOpen = state.challenge !== undefined && state.challenge.status !== 'completed';
25
+ return challengeOpen || state.actionUrl !== undefined;
26
+ }
27
+ /**
28
+ * Whether a pending payment's progress reads as a prompt rather than a
29
+ * spinner: only a hosted page the customer still has to visit asks them for
30
+ * anything. An in-page challenge is driven by the host itself.
31
+ */
32
+ export function awaitsHostedAction(state) {
33
+ return state.actionUrl !== undefined;
34
+ }
9
35
  function projectPending(state, hostStep) {
10
36
  const base = { operationId: state.id, noChargeConfirmed: false };
11
37
  if (hostStep === 'canceled')
@@ -13,17 +39,18 @@ function projectPending(state, hostStep) {
13
39
  const reason = state.declineReason ??
14
40
  (state.challenge?.status === 'failed' ? 'authentication_failed' : undefined);
15
41
  if (reason !== undefined) {
42
+ const recoveryAction = knownRecoveryAction(state.recoveryAction);
16
43
  return {
17
44
  ...base,
18
45
  step: stepForReason(reason),
19
46
  reasonKey: reason,
20
- ...(state.recoveryAction === undefined
21
- ? {}
22
- : { recoveryAction: state.recoveryAction })
47
+ ...(recoveryAction === undefined ? {} : { recoveryAction })
23
48
  };
24
49
  }
25
- const parked = state.challenge !== undefined || state.actionUrl !== undefined;
26
- return { ...base, step: parked ? 'verifying' : 'preview' };
50
+ return {
51
+ ...base,
52
+ step: awaitsVerification(state) ? 'verifying' : 'preview'
53
+ };
27
54
  }
28
55
  export function projectPaymentStep(operation, hostStep) {
29
56
  if (operation === undefined) {
@@ -35,15 +62,16 @@ export function projectPaymentStep(operation, hostStep) {
35
62
  return projectPending(operation, hostStep);
36
63
  case 'succeeded':
37
64
  return { ...base, step: 'success' };
38
- case 'failed':
65
+ case 'failed': {
66
+ const recoveryAction = knownRecoveryAction(operation.recoveryAction) ??
67
+ (operation.retryable ? undefined : 'contact_support');
39
68
  return {
40
69
  ...base,
41
70
  step: stepForReason(operation.declineReason),
42
71
  reasonKey: operation.declineReason,
43
- ...(operation.recoveryAction === undefined
44
- ? {}
45
- : { recoveryAction: operation.recoveryAction })
72
+ ...(recoveryAction === undefined ? {} : { recoveryAction })
46
73
  };
74
+ }
47
75
  case 'reconciliation_needed':
48
76
  return { ...base, step: 'processing_error', reasonKey: 'generic' };
49
77
  case 'timed_out':
@@ -5,10 +5,10 @@
5
5
  * between reads, so only those live in the readers.
6
6
  *
7
7
  * What the skeleton owns is what a reader therefore cannot get wrong on its
8
- * own: one shared request per scope, a caller's signal releasing only that
9
- * caller, the publish fence that reports a late answer as `SUPERSEDED`, and
10
- * the denial that drops a published snapshot unless it predates a change the
11
- * host reported.
8
+ * own: one shared request per scope and route, a caller's signal releasing
9
+ * only that caller, the publish fence that reports a late answer as
10
+ * `SUPERSEDED`, and the denial that drops a published snapshot unless it
11
+ * predates a change the host reported.
12
12
  */
13
13
  import type { BillingResult, BillingTransport } from './billingContracts.js';
14
14
  import type { BillingScope, BillingScopeSource } from './billingScope.js';
@@ -21,7 +21,13 @@ export interface ScopedReaderDefinition<TData, TSnapshot, TOptions extends Scope
21
21
  readonly transport: BillingTransport;
22
22
  /** Where the core learns which user, workspace, and role it runs as. */
23
23
  readonly scopeSource: BillingScopeSource;
24
- readonly route: string;
24
+ /**
25
+ * The route to request, as a function for a read whose query varies per
26
+ * call. The resolved route joins the scope in identifying an in-flight
27
+ * read, so two callers asking for different pages each get their own answer
28
+ * rather than the first one's.
29
+ */
30
+ readonly route: string | ((options: TOptions | undefined) => string);
25
31
  readonly parse: (body: unknown) => ParsedBillingBody<TData>;
26
32
  /**
27
33
  * The reader's own reading of a validated response. Returning a failure is
@@ -5,6 +5,7 @@ function publishAsRead(value) {
5
5
  }
6
6
  export function createScopedReader(definition) {
7
7
  const { transport, scopeSource, route, parse, project, timeoutMs, cached, publish = publishAsRead } = definition;
8
+ const resolveRoute = typeof route === 'string' ? () => route : route;
8
9
  let snapshot;
9
10
  let inFlight;
10
11
  const lifetime = { disposed: false };
@@ -12,11 +13,11 @@ export function createScopedReader(definition) {
12
13
  snapshot = undefined;
13
14
  inFlight = undefined;
14
15
  });
15
- async function request(scope, options) {
16
+ async function request(scope, options, resolvedRoute) {
16
17
  const budget = timeoutMs?.(options);
17
18
  const billingRequest = {
18
19
  method: 'GET',
19
- route,
20
+ route: resolvedRoute,
20
21
  ...(budget === undefined ? {} : { timeoutMs: budget })
21
22
  };
22
23
  const response = await readValidatedBillingResponse(transport, billingRequest, parse);
@@ -30,8 +31,8 @@ export function createScopedReader(definition) {
30
31
  snapshot = published;
31
32
  return { status: 'ok', value: published ?? value };
32
33
  }
33
- async function settle(context, pending, options) {
34
- const result = await request(context.scope, options);
34
+ async function settle(context, pending, options, resolvedRoute) {
35
+ const result = await request(context.scope, options, resolvedRoute);
35
36
  // The publish guard. Between issuing the request and settling it the host
36
37
  // may have changed workspace or signed out, so an answer cached now would
37
38
  // be attributed to whoever is signed in next, and a failure says nothing
@@ -60,10 +61,19 @@ export function createScopedReader(definition) {
60
61
  const served = cached?.(snapshot, context.scope, options);
61
62
  if (served !== undefined)
62
63
  return { status: 'ok', value: served };
63
- // One read per scope. A caller arriving mid-flight for the same scope
64
- // joins rather than issuing a second identical request; a caller for a
65
- // different scope starts its own, and the older one can no longer publish.
66
- if (matchesScopedRead(inFlight, context)) {
64
+ // Resolved once, then used both to issue the request and to identify it,
65
+ // so the answer a joining caller is served is the one its own route asked
66
+ // for.
67
+ const resolvedRoute = resolveRoute(options);
68
+ // A caller arriving mid-flight joins only a read of the same scope and
69
+ // route, rather than issuing a second identical request. A caller for a
70
+ // different scope starts its own, and the older one can no longer
71
+ // publish. A caller for a different route on the same scope also starts
72
+ // its own and takes the slot, so the displaced read still settles and
73
+ // publishes — the snapshot carries which page it is — but nobody can join
74
+ // it any more. One slot is enough for a feed read one page at a time.
75
+ if (matchesScopedRead(inFlight, context) &&
76
+ inFlight.route === resolvedRoute) {
67
77
  return releaseOnAbort(inFlight.promise, options?.signal);
68
78
  }
69
79
  // No caller signal reaches the shared request: it is bounded by its own
@@ -72,7 +82,8 @@ export function createScopedReader(definition) {
72
82
  const pending = { fenced: false };
73
83
  const attempt = {
74
84
  context,
75
- promise: settle(context, pending, options),
85
+ route: resolvedRoute,
86
+ promise: settle(context, pending, options, resolvedRoute),
76
87
  pending
77
88
  };
78
89
  inFlight = attempt;
@@ -1,5 +1,5 @@
1
1
  import { sameBillingScope } from './billingScope.js';
2
- import { readBillingErrorCode } from './billingErrorBody.js';
2
+ import { readBillingErrorCode, readBillingErrorMessage } from './billingErrorBody.js';
3
3
  import { codeForHttpStatus } from './httpStatus.js';
4
4
  /**
5
5
  * An abandoned wait is transient, the same bucket the transport reports an
@@ -22,11 +22,13 @@ export async function readValidatedBillingResponse(transport, request, parse) {
22
22
  const { httpStatus, body } = response.value;
23
23
  if (httpStatus < 200 || httpStatus >= 300) {
24
24
  const serverCode = readBillingErrorCode(body);
25
+ const serverMessage = readBillingErrorMessage(body);
25
26
  return {
26
27
  status: 'error',
27
28
  code: codeForHttpStatus(response.value),
28
29
  httpStatus,
29
- ...(serverCode === undefined ? {} : { serverCode })
30
+ ...(serverCode === undefined ? {} : { serverCode }),
31
+ ...(serverMessage === undefined ? {} : { serverMessage })
30
32
  };
31
33
  }
32
34
  const parsed = parse(body);
@@ -35,7 +35,9 @@ export type SubscriptionCommandCode =
35
35
  /** The server wants the host to re-preview and resend with `confirm_reactivation`. */
36
36
  | 'REACTIVATION_CONFIRMATION_REQUIRED' | 'NO_ACTIVE_SUBSCRIPTION'
37
37
  /** The server asked for a hosted payment step but offered no page for it. */
38
- | 'MISSING_PAYMENT_METHOD_URL';
38
+ | 'MISSING_PAYMENT_METHOD_URL'
39
+ /** The quote no longer matches what the server would charge; re-preview. */
40
+ | 'QUOTE_STALE';
39
41
  export type SubscriptionCommandFailure = BillingFailure | {
40
42
  readonly status: 'error';
41
43
  readonly code: SubscriptionCommandCode;
@@ -89,6 +91,10 @@ declare const PreviewSchema: z.ZodObject<{
89
91
  credits_next_period_cents: z.ZodNumber;
90
92
  credits_today_cents: z.ZodNumber;
91
93
  renewal_amount_cents: z.ZodOptional<z.ZodNumber>;
94
+ subtotal_cents: z.ZodOptional<z.ZodNumber>;
95
+ balance_applied_cents: z.ZodOptional<z.ZodNumber>;
96
+ proration_remaining_cents: z.ZodOptional<z.ZodNumber>;
97
+ proration_unused_cents: z.ZodOptional<z.ZodNumber>;
92
98
  current_plan: z.ZodOptional<z.ZodObject<{
93
99
  duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
94
100
  period_end: z.ZodOptional<z.ZodString>;
@@ -98,6 +104,9 @@ declare const PreviewSchema: z.ZodObject<{
98
104
  } & {
99
105
  credits_cents: z.ZodNumber;
100
106
  price_cents: z.ZodNumber;
107
+ list_price_cents: z.ZodOptional<z.ZodNumber>;
108
+ monthly_list_price_cents: z.ZodOptional<z.ZodNumber>;
109
+ monthly_price_cents: z.ZodOptional<z.ZodNumber>;
101
110
  seat_summary: z.ZodObject<{
102
111
  seat_count: z.ZodNumber;
103
112
  } & {
@@ -114,8 +123,8 @@ declare const PreviewSchema: z.ZodObject<{
114
123
  }>;
115
124
  }, "strip", z.ZodTypeAny, {
116
125
  price_cents: number;
117
- credits_cents: number;
118
126
  duration: "MONTHLY" | "ANNUAL";
127
+ credits_cents: number;
119
128
  seat_summary: {
120
129
  seat_count: number;
121
130
  total_cost_cents: number;
@@ -125,10 +134,13 @@ declare const PreviewSchema: z.ZodObject<{
125
134
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
126
135
  period_end?: string | undefined;
127
136
  period_start?: string | undefined;
137
+ list_price_cents?: number | undefined;
138
+ monthly_list_price_cents?: number | undefined;
139
+ monthly_price_cents?: number | undefined;
128
140
  }, {
129
141
  price_cents: number;
130
- credits_cents: number;
131
142
  duration: "MONTHLY" | "ANNUAL";
143
+ credits_cents: number;
132
144
  seat_summary: {
133
145
  seat_count: number;
134
146
  total_cost_cents: number;
@@ -138,6 +150,9 @@ declare const PreviewSchema: z.ZodObject<{
138
150
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
139
151
  period_end?: string | undefined;
140
152
  period_start?: string | undefined;
153
+ list_price_cents?: number | undefined;
154
+ monthly_list_price_cents?: number | undefined;
155
+ monthly_price_cents?: number | undefined;
141
156
  }>>;
142
157
  new_plan: z.ZodObject<{
143
158
  duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
@@ -148,6 +163,9 @@ declare const PreviewSchema: z.ZodObject<{
148
163
  } & {
149
164
  credits_cents: z.ZodNumber;
150
165
  price_cents: z.ZodNumber;
166
+ list_price_cents: z.ZodOptional<z.ZodNumber>;
167
+ monthly_list_price_cents: z.ZodOptional<z.ZodNumber>;
168
+ monthly_price_cents: z.ZodOptional<z.ZodNumber>;
151
169
  seat_summary: z.ZodObject<{
152
170
  seat_count: z.ZodNumber;
153
171
  } & {
@@ -164,8 +182,8 @@ declare const PreviewSchema: z.ZodObject<{
164
182
  }>;
165
183
  }, "strip", z.ZodTypeAny, {
166
184
  price_cents: number;
167
- credits_cents: number;
168
185
  duration: "MONTHLY" | "ANNUAL";
186
+ credits_cents: number;
169
187
  seat_summary: {
170
188
  seat_count: number;
171
189
  total_cost_cents: number;
@@ -175,10 +193,13 @@ declare const PreviewSchema: z.ZodObject<{
175
193
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
176
194
  period_end?: string | undefined;
177
195
  period_start?: string | undefined;
196
+ list_price_cents?: number | undefined;
197
+ monthly_list_price_cents?: number | undefined;
198
+ monthly_price_cents?: number | undefined;
178
199
  }, {
179
200
  price_cents: number;
180
- credits_cents: number;
181
201
  duration: "MONTHLY" | "ANNUAL";
202
+ credits_cents: number;
182
203
  seat_summary: {
183
204
  seat_count: number;
184
205
  total_cost_cents: number;
@@ -188,23 +209,32 @@ declare const PreviewSchema: z.ZodObject<{
188
209
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
189
210
  period_end?: string | undefined;
190
211
  period_start?: string | undefined;
212
+ list_price_cents?: number | undefined;
213
+ monthly_list_price_cents?: number | undefined;
214
+ monthly_price_cents?: number | undefined;
191
215
  }>;
192
216
  discounts: z.ZodOptional<z.ZodArray<z.ZodObject<{
193
217
  code: z.ZodString;
218
+ duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
194
219
  kind: z.ZodEnum<["plan", "promotion"]>;
195
220
  name: z.ZodOptional<z.ZodString>;
196
221
  } & {
197
222
  amount_off_cents: z.ZodOptional<z.ZodNumber>;
223
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
198
224
  }, "strip", z.ZodTypeAny, {
199
225
  code: string;
200
226
  kind: "plan" | "promotion";
201
227
  name?: string | undefined;
202
228
  amount_off_cents?: number | undefined;
229
+ duration?: "once" | "repeating" | "forever" | undefined;
230
+ duration_in_months?: number | undefined;
203
231
  }, {
204
232
  code: string;
205
233
  kind: "plan" | "promotion";
206
234
  name?: string | undefined;
207
235
  amount_off_cents?: number | undefined;
236
+ duration?: "once" | "repeating" | "forever" | undefined;
237
+ duration_in_months?: number | undefined;
208
238
  }>, "many">>;
209
239
  }, "strip", z.ZodTypeAny, {
210
240
  effective_at: string;
@@ -216,8 +246,8 @@ declare const PreviewSchema: z.ZodObject<{
216
246
  is_immediate: boolean;
217
247
  new_plan: {
218
248
  price_cents: number;
219
- credits_cents: number;
220
249
  duration: "MONTHLY" | "ANNUAL";
250
+ credits_cents: number;
221
251
  seat_summary: {
222
252
  seat_count: number;
223
253
  total_cost_cents: number;
@@ -227,6 +257,9 @@ declare const PreviewSchema: z.ZodObject<{
227
257
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
228
258
  period_end?: string | undefined;
229
259
  period_start?: string | undefined;
260
+ list_price_cents?: number | undefined;
261
+ monthly_list_price_cents?: number | undefined;
262
+ monthly_price_cents?: number | undefined;
230
263
  };
231
264
  transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
232
265
  currency?: string | undefined;
@@ -235,10 +268,11 @@ declare const PreviewSchema: z.ZodObject<{
235
268
  quote_id?: string | undefined;
236
269
  quote_version?: number | undefined;
237
270
  amount_due_cents?: number | undefined;
271
+ balance_applied_cents?: number | undefined;
238
272
  current_plan?: {
239
273
  price_cents: number;
240
- credits_cents: number;
241
274
  duration: "MONTHLY" | "ANNUAL";
275
+ credits_cents: number;
242
276
  seat_summary: {
243
277
  seat_count: number;
244
278
  total_cost_cents: number;
@@ -248,18 +282,26 @@ declare const PreviewSchema: z.ZodObject<{
248
282
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
249
283
  period_end?: string | undefined;
250
284
  period_start?: string | undefined;
285
+ list_price_cents?: number | undefined;
286
+ monthly_list_price_cents?: number | undefined;
287
+ monthly_price_cents?: number | undefined;
251
288
  } | undefined;
252
289
  discounts?: {
253
290
  code: string;
254
291
  kind: "plan" | "promotion";
255
292
  name?: string | undefined;
256
293
  amount_off_cents?: number | undefined;
294
+ duration?: "once" | "repeating" | "forever" | undefined;
295
+ duration_in_months?: number | undefined;
257
296
  }[] | undefined;
258
297
  payment_method_configuration_id?: string | undefined;
298
+ proration_remaining_cents?: number | undefined;
299
+ proration_unused_cents?: number | undefined;
259
300
  reason?: string | undefined;
260
301
  renewal_amount_cents?: number | undefined;
261
302
  renewal_at?: string | undefined;
262
303
  requires_reactivation_confirmation?: boolean | undefined;
304
+ subtotal_cents?: number | undefined;
263
305
  }, {
264
306
  effective_at: string;
265
307
  allowed: boolean;
@@ -270,8 +312,8 @@ declare const PreviewSchema: z.ZodObject<{
270
312
  is_immediate: boolean;
271
313
  new_plan: {
272
314
  price_cents: number;
273
- credits_cents: number;
274
315
  duration: "MONTHLY" | "ANNUAL";
316
+ credits_cents: number;
275
317
  seat_summary: {
276
318
  seat_count: number;
277
319
  total_cost_cents: number;
@@ -281,6 +323,9 @@ declare const PreviewSchema: z.ZodObject<{
281
323
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
282
324
  period_end?: string | undefined;
283
325
  period_start?: string | undefined;
326
+ list_price_cents?: number | undefined;
327
+ monthly_list_price_cents?: number | undefined;
328
+ monthly_price_cents?: number | undefined;
284
329
  };
285
330
  transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
286
331
  currency?: string | undefined;
@@ -289,10 +334,11 @@ declare const PreviewSchema: z.ZodObject<{
289
334
  quote_id?: string | undefined;
290
335
  quote_version?: number | undefined;
291
336
  amount_due_cents?: number | undefined;
337
+ balance_applied_cents?: number | undefined;
292
338
  current_plan?: {
293
339
  price_cents: number;
294
- credits_cents: number;
295
340
  duration: "MONTHLY" | "ANNUAL";
341
+ credits_cents: number;
296
342
  seat_summary: {
297
343
  seat_count: number;
298
344
  total_cost_cents: number;
@@ -302,18 +348,26 @@ declare const PreviewSchema: z.ZodObject<{
302
348
  tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
303
349
  period_end?: string | undefined;
304
350
  period_start?: string | undefined;
351
+ list_price_cents?: number | undefined;
352
+ monthly_list_price_cents?: number | undefined;
353
+ monthly_price_cents?: number | undefined;
305
354
  } | undefined;
306
355
  discounts?: {
307
356
  code: string;
308
357
  kind: "plan" | "promotion";
309
358
  name?: string | undefined;
310
359
  amount_off_cents?: number | undefined;
360
+ duration?: "once" | "repeating" | "forever" | undefined;
361
+ duration_in_months?: number | undefined;
311
362
  }[] | undefined;
312
363
  payment_method_configuration_id?: string | undefined;
364
+ proration_remaining_cents?: number | undefined;
365
+ proration_unused_cents?: number | undefined;
313
366
  reason?: string | undefined;
314
367
  renewal_amount_cents?: number | undefined;
315
368
  renewal_at?: string | undefined;
316
369
  requires_reactivation_confirmation?: boolean | undefined;
370
+ subtotal_cents?: number | undefined;
317
371
  }>;
318
372
  /**
319
373
  * The quote the server returns for a plan change, in the generated field names.
@@ -16,6 +16,7 @@ import { z } from 'zod';
16
16
  import { matchesServerCode } from './billingContracts.js';
17
17
  import { validateActionUrl } from './operationState.js';
18
18
  import { readValidatedBillingResponse } from './sharedRead.js';
19
+ import { wireCents } from './wireCents.js';
19
20
  export const SUBSCRIBE_ROUTE = '/billing/subscribe';
20
21
  export const RESUBSCRIBE_ROUTE = '/billing/subscription/resubscribe';
21
22
  export const CANCEL_SUBSCRIPTION_ROUTE = '/billing/subscription/cancel';
@@ -26,35 +27,37 @@ const NO_ACTIVE_SUBSCRIPTION_SERVER_CODE = 'NO_ACTIVE_SUBSCRIPTION';
26
27
  const REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE = 'REACTIVATION_CONFIRMATION_REQUIRED';
27
28
  const NOT_SCHEDULED_FOR_CANCELLATION_SERVER_CODE = 'NOT_SCHEDULED_FOR_CANCELLATION';
28
29
  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();
30
+ const SUBSCRIPTION_QUOTE_STALE_SERVER_CODE = 'SUBSCRIPTION_QUOTE_STALE';
31
+ const SUBSCRIPTION_CHANGE_IN_PROGRESS_SERVER_CODE = 'SUBSCRIPTION_CHANGE_IN_PROGRESS';
39
32
  const PlanInfoSchema = zPreviewPlanInfo.extend({
40
- credits_cents: cents,
41
- price_cents: cents,
33
+ credits_cents: wireCents,
34
+ price_cents: wireCents,
35
+ list_price_cents: wireCents.optional(),
36
+ monthly_list_price_cents: wireCents.optional(),
37
+ monthly_price_cents: wireCents.optional(),
42
38
  seat_summary: zPreviewPlanInfo.shape.seat_summary.extend({
43
- total_cost_cents: cents,
44
- total_credits_cents: cents
39
+ total_cost_cents: wireCents,
40
+ total_credits_cents: wireCents
45
41
  })
46
42
  });
47
43
  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(),
44
+ amount_due_cents: wireCents.optional(),
45
+ cost_next_period_cents: wireCents,
46
+ cost_today_cents: wireCents,
47
+ credits_next_period_cents: wireCents,
48
+ credits_today_cents: wireCents,
49
+ renewal_amount_cents: wireCents.optional(),
50
+ subtotal_cents: wireCents.optional(),
51
+ balance_applied_cents: wireCents.optional(),
52
+ proration_remaining_cents: wireCents.optional(),
53
+ proration_unused_cents: wireCents.optional(),
54
54
  current_plan: PlanInfoSchema.optional(),
55
55
  new_plan: PlanInfoSchema,
56
56
  discounts: z
57
- .array(zSubscriptionDiscount.extend({ amount_off_cents: cents.optional() }))
57
+ .array(zSubscriptionDiscount.extend({
58
+ amount_off_cents: wireCents.optional(),
59
+ duration_in_months: wireCents.optional()
60
+ }))
58
61
  .optional()
59
62
  });
60
63
  const ALREADY_HELD = {
@@ -65,23 +68,33 @@ function coded(code) {
65
68
  return { status: 'error', code };
66
69
  }
67
70
  /**
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
+ * A server code is trusted as state only from a 4xx: a 5xx echoing the code
72
+ * is an upstream failure that happens to carry it, and the state it names
73
+ * cannot be assumed to hold.
71
74
  */
72
- function alreadyInRequestedState(failure, serverCode) {
75
+ function refusedWith(failure, serverCode) {
73
76
  return (matchesServerCode(failure, serverCode) &&
74
77
  failure.httpStatus !== undefined &&
75
78
  failure.httpStatus >= 400 &&
76
79
  failure.httpStatus < 500);
77
80
  }
81
+ /**
82
+ * The server's refusal because another subscription operation is still open
83
+ * is the same answer the lifecycle gives when it sees that operation first.
84
+ */
85
+ function refusedWhilePending(failure) {
86
+ return refusedWith(failure, SUBSCRIPTION_CHANGE_IN_PROGRESS_SERVER_CODE)
87
+ ? { ...failure, code: 'OPERATION_ALREADY_PENDING' }
88
+ : failure;
89
+ }
78
90
  function mapServerCode(failure, alreadyHeldCode) {
79
- if (alreadyInRequestedState(failure, alreadyHeldCode)) {
91
+ // A server code the caller's request already satisfies is a success.
92
+ if (refusedWith(failure, alreadyHeldCode)) {
80
93
  return { status: 'already_held' };
81
94
  }
82
95
  return matchesServerCode(failure, NO_ACTIVE_SUBSCRIPTION_SERVER_CODE)
83
96
  ? coded('NO_ACTIVE_SUBSCRIPTION')
84
- : failure;
97
+ : refusedWhilePending(failure);
85
98
  }
86
99
  function dropEmpty(value) {
87
100
  return value === '' ? undefined : value;
@@ -159,9 +172,12 @@ export function createBillingCommands(options) {
159
172
  const key = idempotencyKey();
160
173
  const response = await post(SUBSCRIBE_ROUTE, { ...input, idempotency_key: key }, (body) => zSubscribeResponse.safeParse(body), key);
161
174
  if (response.status === 'error') {
162
- return matchesServerCode(response, REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE)
163
- ? coded('REACTIVATION_CONFIRMATION_REQUIRED')
164
- : response;
175
+ if (matchesServerCode(response, REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE)) {
176
+ return coded('REACTIVATION_CONFIRMATION_REQUIRED');
177
+ }
178
+ return refusedWith(response, SUBSCRIPTION_QUOTE_STALE_SERVER_CODE)
179
+ ? coded('QUOTE_STALE')
180
+ : refusedWhilePending(response);
165
181
  }
166
182
  const { billing_op_id, status, payment_method_url } = response.value.data;
167
183
  if (status !== 'needs_payment_method') {
@@ -11,6 +11,26 @@ export declare const TOPUP_ROUTE = "/billing/topup";
11
11
  * through a webhook, so completion is the host's return plus a balance watch.
12
12
  */
13
13
  export declare const TOPUP_CHECKOUT_ROUTE = "/billing/topup/checkout";
14
+ /** A read the backend shapes as a POST: nothing is charged or reserved. */
15
+ export declare const TOPUP_QUOTE_ROUTE = "/billing/topup/quote";
16
+ /**
17
+ * What the server says an amount buys. `credits` is the server's display
18
+ * count; `expiresAt` is when the grant would lapse, which the server may
19
+ * move, so a host never states the expiry policy itself.
20
+ */
21
+ export interface TopupQuote {
22
+ readonly amountCents: number;
23
+ readonly credits: number;
24
+ readonly expiresAt: string;
25
+ }
26
+ export type TopupQuoteResult = {
27
+ readonly status: 'ok';
28
+ readonly value: TopupQuote;
29
+ } | BillingFailure | TopupNotAvailable | TopupInvalidAmount;
30
+ export interface QuoteTopupInput {
31
+ readonly amountCents: number;
32
+ readonly signal?: AbortSignal;
33
+ }
14
34
  type OperationIn<P extends BillingOperationState['phase']> = Extract<BillingOperationState, {
15
35
  readonly phase: P;
16
36
  }>;
@@ -101,6 +121,12 @@ export interface TopupCommandOptions {
101
121
  readonly idempotencyKey?: () => string;
102
122
  }
103
123
  export interface TopupCommand {
124
+ /**
125
+ * Quotes an amount without starting anything. It does not check the plan
126
+ * tier or the payment method, so a quote does not promise the top-up will
127
+ * go through.
128
+ */
129
+ quoteTopup: (input: QuoteTopupInput) => Promise<TopupQuoteResult>;
104
130
  createTopupCheckout: (input: CreateTopupCheckoutInput) => Promise<TopupResult>;
105
131
  /**
106
132
  * The route Workshop and Platform ship on today. Unlike