@comfyorg/account-core 1.0.0-alpha.0 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/billing/billingContracts.d.ts +29 -14
- package/dist/core/billing/billingErrorBody.d.ts +8 -4
- package/dist/core/billing/billingErrorBody.js +14 -4
- package/dist/core/billing/credentialedTransport.d.ts +23 -7
- package/dist/core/billing/credentialedTransport.js +72 -8
- package/dist/core/billing/events.d.ts +47 -0
- package/dist/core/billing/events.js +43 -0
- package/dist/core/billing/index.d.ts +16 -11
- package/dist/core/billing/index.js +8 -6
- package/dist/core/billing/operationLifecycle.d.ts +34 -1
- package/dist/core/billing/operationLifecycle.js +146 -67
- package/dist/core/billing/operationPointer.d.ts +27 -2
- package/dist/core/billing/operationPointer.js +36 -25
- package/dist/core/billing/operationPolicy.d.ts +44 -7
- package/dist/core/billing/operationPolicy.js +64 -3
- package/dist/core/billing/operationState.d.ts +102 -18
- package/dist/core/billing/operationState.js +78 -8
- package/dist/core/billing/paymentCopy.d.ts +6 -1
- package/dist/core/billing/paymentCopy.js +34 -1
- package/dist/core/billing/paymentProjection.d.ts +14 -2
- package/dist/core/billing/paymentProjection.js +37 -9
- package/dist/core/billing/scopedReader.d.ts +11 -5
- package/dist/core/billing/scopedReader.js +20 -9
- package/dist/core/billing/sharedRead.js +4 -2
- package/dist/core/billing/subscriptionCommands.d.ts +37 -9
- package/dist/core/billing/subscriptionCommands.js +42 -30
- package/dist/core/billing/topup.d.ts +26 -0
- package/dist/core/billing/topup.js +36 -2
- package/dist/core/billing/wireCents.d.ts +8 -0
- package/dist/core/billing/wireCents.js +8 -0
- package/dist/core/billing/workspaceInvites.d.ts +12 -0
- package/dist/core/billing/workspaceInvites.js +26 -0
- package/dist/core/identity.d.ts +6 -6
- package/dist/core/identity.js +4 -10
- package/dist/core/lazyIdentity.d.ts +31 -0
- package/dist/core/lazyIdentity.js +96 -0
- package/dist/core/requestAuth.d.ts +30 -0
- package/dist/core/requestAuth.js +41 -0
- package/dist/core/session.d.ts +12 -18
- package/dist/core/session.js +16 -26
- package/dist/core/sessionContracts.d.ts +39 -2
- package/dist/core/sessionTokenMint.d.ts +33 -0
- package/dist/core/sessionTokenMint.js +205 -0
- package/dist/core/webSession.d.ts +25 -0
- package/dist/core/webSession.js +137 -0
- package/dist/core/webSessionFlag.d.ts +20 -0
- package/dist/core/webSessionFlag.js +62 -0
- package/dist/core/webSessionIdentity.d.ts +291 -0
- package/dist/core/webSessionIdentity.js +496 -0
- package/dist/firebase/configSource.d.ts +54 -0
- package/dist/firebase/configSource.js +111 -0
- package/dist/firebase/index.d.ts +71 -8
- package/dist/firebase/index.js +236 -23
- package/dist/firebase/popupWatch.d.ts +42 -0
- package/dist/firebase/popupWatch.js +140 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +116 -0
- package/dist/web/crossTabRefresh.d.ts +4 -2
- package/dist/web/crossTabRefresh.js +13 -0
- package/dist/workspaceLink.d.ts +45 -0
- package/dist/workspaceLink.js +78 -0
- package/package.json +30 -6
|
@@ -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
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
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
|
-
|
|
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,8 @@ 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>;
|
|
92
96
|
current_plan: z.ZodOptional<z.ZodObject<{
|
|
93
97
|
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
94
98
|
period_end: z.ZodOptional<z.ZodString>;
|
|
@@ -98,6 +102,7 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
98
102
|
} & {
|
|
99
103
|
credits_cents: z.ZodNumber;
|
|
100
104
|
price_cents: z.ZodNumber;
|
|
105
|
+
list_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
101
106
|
seat_summary: z.ZodObject<{
|
|
102
107
|
seat_count: z.ZodNumber;
|
|
103
108
|
} & {
|
|
@@ -114,8 +119,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
114
119
|
}>;
|
|
115
120
|
}, "strip", z.ZodTypeAny, {
|
|
116
121
|
price_cents: number;
|
|
117
|
-
credits_cents: number;
|
|
118
122
|
duration: "MONTHLY" | "ANNUAL";
|
|
123
|
+
credits_cents: number;
|
|
119
124
|
seat_summary: {
|
|
120
125
|
seat_count: number;
|
|
121
126
|
total_cost_cents: number;
|
|
@@ -125,10 +130,11 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
125
130
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
126
131
|
period_end?: string | undefined;
|
|
127
132
|
period_start?: string | undefined;
|
|
133
|
+
list_price_cents?: number | undefined;
|
|
128
134
|
}, {
|
|
129
135
|
price_cents: number;
|
|
130
|
-
credits_cents: number;
|
|
131
136
|
duration: "MONTHLY" | "ANNUAL";
|
|
137
|
+
credits_cents: number;
|
|
132
138
|
seat_summary: {
|
|
133
139
|
seat_count: number;
|
|
134
140
|
total_cost_cents: number;
|
|
@@ -138,6 +144,7 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
138
144
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
139
145
|
period_end?: string | undefined;
|
|
140
146
|
period_start?: string | undefined;
|
|
147
|
+
list_price_cents?: number | undefined;
|
|
141
148
|
}>>;
|
|
142
149
|
new_plan: z.ZodObject<{
|
|
143
150
|
duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
|
|
@@ -148,6 +155,7 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
148
155
|
} & {
|
|
149
156
|
credits_cents: z.ZodNumber;
|
|
150
157
|
price_cents: z.ZodNumber;
|
|
158
|
+
list_price_cents: z.ZodOptional<z.ZodNumber>;
|
|
151
159
|
seat_summary: z.ZodObject<{
|
|
152
160
|
seat_count: z.ZodNumber;
|
|
153
161
|
} & {
|
|
@@ -164,8 +172,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
164
172
|
}>;
|
|
165
173
|
}, "strip", z.ZodTypeAny, {
|
|
166
174
|
price_cents: number;
|
|
167
|
-
credits_cents: number;
|
|
168
175
|
duration: "MONTHLY" | "ANNUAL";
|
|
176
|
+
credits_cents: number;
|
|
169
177
|
seat_summary: {
|
|
170
178
|
seat_count: number;
|
|
171
179
|
total_cost_cents: number;
|
|
@@ -175,10 +183,11 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
175
183
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
176
184
|
period_end?: string | undefined;
|
|
177
185
|
period_start?: string | undefined;
|
|
186
|
+
list_price_cents?: number | undefined;
|
|
178
187
|
}, {
|
|
179
188
|
price_cents: number;
|
|
180
|
-
credits_cents: number;
|
|
181
189
|
duration: "MONTHLY" | "ANNUAL";
|
|
190
|
+
credits_cents: number;
|
|
182
191
|
seat_summary: {
|
|
183
192
|
seat_count: number;
|
|
184
193
|
total_cost_cents: number;
|
|
@@ -188,23 +197,30 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
188
197
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
189
198
|
period_end?: string | undefined;
|
|
190
199
|
period_start?: string | undefined;
|
|
200
|
+
list_price_cents?: number | undefined;
|
|
191
201
|
}>;
|
|
192
202
|
discounts: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
193
203
|
code: z.ZodString;
|
|
204
|
+
duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
|
|
194
205
|
kind: z.ZodEnum<["plan", "promotion"]>;
|
|
195
206
|
name: z.ZodOptional<z.ZodString>;
|
|
196
207
|
} & {
|
|
197
208
|
amount_off_cents: z.ZodOptional<z.ZodNumber>;
|
|
209
|
+
duration_in_months: z.ZodOptional<z.ZodNumber>;
|
|
198
210
|
}, "strip", z.ZodTypeAny, {
|
|
199
211
|
code: string;
|
|
200
212
|
kind: "plan" | "promotion";
|
|
201
213
|
name?: string | undefined;
|
|
202
214
|
amount_off_cents?: number | undefined;
|
|
215
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
216
|
+
duration_in_months?: number | undefined;
|
|
203
217
|
}, {
|
|
204
218
|
code: string;
|
|
205
219
|
kind: "plan" | "promotion";
|
|
206
220
|
name?: string | undefined;
|
|
207
221
|
amount_off_cents?: number | undefined;
|
|
222
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
223
|
+
duration_in_months?: number | undefined;
|
|
208
224
|
}>, "many">>;
|
|
209
225
|
}, "strip", z.ZodTypeAny, {
|
|
210
226
|
effective_at: string;
|
|
@@ -216,8 +232,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
216
232
|
is_immediate: boolean;
|
|
217
233
|
new_plan: {
|
|
218
234
|
price_cents: number;
|
|
219
|
-
credits_cents: number;
|
|
220
235
|
duration: "MONTHLY" | "ANNUAL";
|
|
236
|
+
credits_cents: number;
|
|
221
237
|
seat_summary: {
|
|
222
238
|
seat_count: number;
|
|
223
239
|
total_cost_cents: number;
|
|
@@ -227,6 +243,7 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
227
243
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
228
244
|
period_end?: string | undefined;
|
|
229
245
|
period_start?: string | undefined;
|
|
246
|
+
list_price_cents?: number | undefined;
|
|
230
247
|
};
|
|
231
248
|
transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
|
|
232
249
|
currency?: string | undefined;
|
|
@@ -235,10 +252,11 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
235
252
|
quote_id?: string | undefined;
|
|
236
253
|
quote_version?: number | undefined;
|
|
237
254
|
amount_due_cents?: number | undefined;
|
|
255
|
+
balance_applied_cents?: number | undefined;
|
|
238
256
|
current_plan?: {
|
|
239
257
|
price_cents: number;
|
|
240
|
-
credits_cents: number;
|
|
241
258
|
duration: "MONTHLY" | "ANNUAL";
|
|
259
|
+
credits_cents: number;
|
|
242
260
|
seat_summary: {
|
|
243
261
|
seat_count: number;
|
|
244
262
|
total_cost_cents: number;
|
|
@@ -248,18 +266,22 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
248
266
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
249
267
|
period_end?: string | undefined;
|
|
250
268
|
period_start?: string | undefined;
|
|
269
|
+
list_price_cents?: number | undefined;
|
|
251
270
|
} | undefined;
|
|
252
271
|
discounts?: {
|
|
253
272
|
code: string;
|
|
254
273
|
kind: "plan" | "promotion";
|
|
255
274
|
name?: string | undefined;
|
|
256
275
|
amount_off_cents?: number | undefined;
|
|
276
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
277
|
+
duration_in_months?: number | undefined;
|
|
257
278
|
}[] | undefined;
|
|
258
279
|
payment_method_configuration_id?: string | undefined;
|
|
259
280
|
reason?: string | undefined;
|
|
260
281
|
renewal_amount_cents?: number | undefined;
|
|
261
282
|
renewal_at?: string | undefined;
|
|
262
283
|
requires_reactivation_confirmation?: boolean | undefined;
|
|
284
|
+
subtotal_cents?: number | undefined;
|
|
263
285
|
}, {
|
|
264
286
|
effective_at: string;
|
|
265
287
|
allowed: boolean;
|
|
@@ -270,8 +292,8 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
270
292
|
is_immediate: boolean;
|
|
271
293
|
new_plan: {
|
|
272
294
|
price_cents: number;
|
|
273
|
-
credits_cents: number;
|
|
274
295
|
duration: "MONTHLY" | "ANNUAL";
|
|
296
|
+
credits_cents: number;
|
|
275
297
|
seat_summary: {
|
|
276
298
|
seat_count: number;
|
|
277
299
|
total_cost_cents: number;
|
|
@@ -281,6 +303,7 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
281
303
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
282
304
|
period_end?: string | undefined;
|
|
283
305
|
period_start?: string | undefined;
|
|
306
|
+
list_price_cents?: number | undefined;
|
|
284
307
|
};
|
|
285
308
|
transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
|
|
286
309
|
currency?: string | undefined;
|
|
@@ -289,10 +312,11 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
289
312
|
quote_id?: string | undefined;
|
|
290
313
|
quote_version?: number | undefined;
|
|
291
314
|
amount_due_cents?: number | undefined;
|
|
315
|
+
balance_applied_cents?: number | undefined;
|
|
292
316
|
current_plan?: {
|
|
293
317
|
price_cents: number;
|
|
294
|
-
credits_cents: number;
|
|
295
318
|
duration: "MONTHLY" | "ANNUAL";
|
|
319
|
+
credits_cents: number;
|
|
296
320
|
seat_summary: {
|
|
297
321
|
seat_count: number;
|
|
298
322
|
total_cost_cents: number;
|
|
@@ -302,18 +326,22 @@ declare const PreviewSchema: z.ZodObject<{
|
|
|
302
326
|
tier: "FREE" | "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION" | "TEAM" | "ENTERPRISE";
|
|
303
327
|
period_end?: string | undefined;
|
|
304
328
|
period_start?: string | undefined;
|
|
329
|
+
list_price_cents?: number | undefined;
|
|
305
330
|
} | undefined;
|
|
306
331
|
discounts?: {
|
|
307
332
|
code: string;
|
|
308
333
|
kind: "plan" | "promotion";
|
|
309
334
|
name?: string | undefined;
|
|
310
335
|
amount_off_cents?: number | undefined;
|
|
336
|
+
duration?: "once" | "repeating" | "forever" | undefined;
|
|
337
|
+
duration_in_months?: number | undefined;
|
|
311
338
|
}[] | undefined;
|
|
312
339
|
payment_method_configuration_id?: string | undefined;
|
|
313
340
|
reason?: string | undefined;
|
|
314
341
|
renewal_amount_cents?: number | undefined;
|
|
315
342
|
renewal_at?: string | undefined;
|
|
316
343
|
requires_reactivation_confirmation?: boolean | undefined;
|
|
344
|
+
subtotal_cents?: number | undefined;
|
|
317
345
|
}>;
|
|
318
346
|
/**
|
|
319
347
|
* 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,33 @@ 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
|
-
|
|
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:
|
|
41
|
-
price_cents:
|
|
33
|
+
credits_cents: wireCents,
|
|
34
|
+
price_cents: wireCents,
|
|
35
|
+
list_price_cents: wireCents.optional(),
|
|
42
36
|
seat_summary: zPreviewPlanInfo.shape.seat_summary.extend({
|
|
43
|
-
total_cost_cents:
|
|
44
|
-
total_credits_cents:
|
|
37
|
+
total_cost_cents: wireCents,
|
|
38
|
+
total_credits_cents: wireCents
|
|
45
39
|
})
|
|
46
40
|
});
|
|
47
41
|
const PreviewSchema = zPreviewSubscribeResponse.extend({
|
|
48
|
-
amount_due_cents:
|
|
49
|
-
cost_next_period_cents:
|
|
50
|
-
cost_today_cents:
|
|
51
|
-
credits_next_period_cents:
|
|
52
|
-
credits_today_cents:
|
|
53
|
-
renewal_amount_cents:
|
|
42
|
+
amount_due_cents: wireCents.optional(),
|
|
43
|
+
cost_next_period_cents: wireCents,
|
|
44
|
+
cost_today_cents: wireCents,
|
|
45
|
+
credits_next_period_cents: wireCents,
|
|
46
|
+
credits_today_cents: wireCents,
|
|
47
|
+
renewal_amount_cents: wireCents.optional(),
|
|
48
|
+
subtotal_cents: wireCents.optional(),
|
|
49
|
+
balance_applied_cents: wireCents.optional(),
|
|
54
50
|
current_plan: PlanInfoSchema.optional(),
|
|
55
51
|
new_plan: PlanInfoSchema,
|
|
56
52
|
discounts: z
|
|
57
|
-
.array(zSubscriptionDiscount.extend({
|
|
53
|
+
.array(zSubscriptionDiscount.extend({
|
|
54
|
+
amount_off_cents: wireCents.optional(),
|
|
55
|
+
duration_in_months: wireCents.optional()
|
|
56
|
+
}))
|
|
58
57
|
.optional()
|
|
59
58
|
});
|
|
60
59
|
const ALREADY_HELD = {
|
|
@@ -65,23 +64,33 @@ function coded(code) {
|
|
|
65
64
|
return { status: 'error', code };
|
|
66
65
|
}
|
|
67
66
|
/**
|
|
68
|
-
* A server code
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* A server code is trusted as state only from a 4xx: a 5xx echoing the code
|
|
68
|
+
* is an upstream failure that happens to carry it, and the state it names
|
|
69
|
+
* cannot be assumed to hold.
|
|
71
70
|
*/
|
|
72
|
-
function
|
|
71
|
+
function refusedWith(failure, serverCode) {
|
|
73
72
|
return (matchesServerCode(failure, serverCode) &&
|
|
74
73
|
failure.httpStatus !== undefined &&
|
|
75
74
|
failure.httpStatus >= 400 &&
|
|
76
75
|
failure.httpStatus < 500);
|
|
77
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The server's refusal because another subscription operation is still open
|
|
79
|
+
* is the same answer the lifecycle gives when it sees that operation first.
|
|
80
|
+
*/
|
|
81
|
+
function refusedWhilePending(failure) {
|
|
82
|
+
return refusedWith(failure, SUBSCRIPTION_CHANGE_IN_PROGRESS_SERVER_CODE)
|
|
83
|
+
? { ...failure, code: 'OPERATION_ALREADY_PENDING' }
|
|
84
|
+
: failure;
|
|
85
|
+
}
|
|
78
86
|
function mapServerCode(failure, alreadyHeldCode) {
|
|
79
|
-
|
|
87
|
+
// A server code the caller's request already satisfies is a success.
|
|
88
|
+
if (refusedWith(failure, alreadyHeldCode)) {
|
|
80
89
|
return { status: 'already_held' };
|
|
81
90
|
}
|
|
82
91
|
return matchesServerCode(failure, NO_ACTIVE_SUBSCRIPTION_SERVER_CODE)
|
|
83
92
|
? coded('NO_ACTIVE_SUBSCRIPTION')
|
|
84
|
-
: failure;
|
|
93
|
+
: refusedWhilePending(failure);
|
|
85
94
|
}
|
|
86
95
|
function dropEmpty(value) {
|
|
87
96
|
return value === '' ? undefined : value;
|
|
@@ -159,9 +168,12 @@ export function createBillingCommands(options) {
|
|
|
159
168
|
const key = idempotencyKey();
|
|
160
169
|
const response = await post(SUBSCRIBE_ROUTE, { ...input, idempotency_key: key }, (body) => zSubscribeResponse.safeParse(body), key);
|
|
161
170
|
if (response.status === 'error') {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
if (matchesServerCode(response, REACTIVATION_CONFIRMATION_REQUIRED_SERVER_CODE)) {
|
|
172
|
+
return coded('REACTIVATION_CONFIRMATION_REQUIRED');
|
|
173
|
+
}
|
|
174
|
+
return refusedWith(response, SUBSCRIPTION_QUOTE_STALE_SERVER_CODE)
|
|
175
|
+
? coded('QUOTE_STALE')
|
|
176
|
+
: refusedWhilePending(response);
|
|
165
177
|
}
|
|
166
178
|
const { billing_op_id, status, payment_method_url } = response.value.data;
|
|
167
179
|
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
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* own vocabulary, and the terminal operation is embedded whenever one was
|
|
11
11
|
* observed so a host reads the coded decline and recovery action from it.
|
|
12
12
|
*/
|
|
13
|
-
import { zCreateTopupCheckoutRequest, zCreateTopupCheckoutResponse, zCreateTopupRequest, zCreateTopupResponse } from '@comfyorg/ingest-types/zod';
|
|
13
|
+
import { zCreateTopupCheckoutRequest, zCreateTopupCheckoutResponse, zCreateTopupRequest, zCreateTopupResponse, zTopupQuoteResponse } from '@comfyorg/ingest-types/zod';
|
|
14
14
|
import { matchesServerCode } from './billingContracts.js';
|
|
15
15
|
import { isTerminal, validateActionUrl } from './operationState.js';
|
|
16
16
|
import { readValidatedBillingResponse } from './sharedRead.js';
|
|
17
|
+
import { wireCents } from './wireCents.js';
|
|
17
18
|
export const TOPUP_ROUTE = '/billing/topup';
|
|
18
19
|
/**
|
|
19
20
|
* The hosted variant: a provider-hosted Checkout session with no operation to
|
|
@@ -21,6 +22,14 @@ export const TOPUP_ROUTE = '/billing/topup';
|
|
|
21
22
|
* through a webhook, so completion is the host's return plus a balance watch.
|
|
22
23
|
*/
|
|
23
24
|
export const TOPUP_CHECKOUT_ROUTE = '/billing/topup/checkout';
|
|
25
|
+
/** A read the backend shapes as a POST: nothing is charged or reserved. */
|
|
26
|
+
export const TOPUP_QUOTE_ROUTE = '/billing/topup/quote';
|
|
27
|
+
/** The server's own server code for an amount it will not sell. */
|
|
28
|
+
const INVALID_AMOUNT_SERVER_CODE = 'INVALID_AMOUNT';
|
|
29
|
+
const TopupQuoteSchema = zTopupQuoteResponse.extend({
|
|
30
|
+
amount_cents: wireCents,
|
|
31
|
+
credits: wireCents
|
|
32
|
+
});
|
|
24
33
|
/** The one coded error body this command acts on; every other code stays a `BillingFailure`. */
|
|
25
34
|
const NO_PAYMENT_METHOD_SERVER_CODE = 'NO_PAYMENT_METHOD';
|
|
26
35
|
const INVALID_AMOUNT = {
|
|
@@ -205,5 +214,30 @@ export function createTopupCommand(options) {
|
|
|
205
214
|
: {})
|
|
206
215
|
};
|
|
207
216
|
}
|
|
208
|
-
|
|
217
|
+
async function quoteTopup(input) {
|
|
218
|
+
const { amountCents, signal } = input;
|
|
219
|
+
if (!wireCents.positive().safeParse(amountCents).success)
|
|
220
|
+
return INVALID_AMOUNT;
|
|
221
|
+
const response = await readValidatedBillingResponse(transport, {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
route: TOPUP_QUOTE_ROUTE,
|
|
224
|
+
body: { amount_cents: amountCents },
|
|
225
|
+
...(signal === undefined ? {} : { signal })
|
|
226
|
+
}, (raw) => TopupQuoteSchema.safeParse(raw));
|
|
227
|
+
if (response.status === 'error') {
|
|
228
|
+
if (matchesServerCode(response, INVALID_AMOUNT_SERVER_CODE))
|
|
229
|
+
return INVALID_AMOUNT;
|
|
230
|
+
return response.httpStatus === 404 ? NOT_AVAILABLE : response;
|
|
231
|
+
}
|
|
232
|
+
const quote = response.value.data;
|
|
233
|
+
return {
|
|
234
|
+
status: 'ok',
|
|
235
|
+
value: {
|
|
236
|
+
amountCents: quote.amount_cents,
|
|
237
|
+
credits: quote.credits,
|
|
238
|
+
expiresAt: quote.expires_at
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return { quoteTopup, createTopupCheckout, createHostedTopupCheckout };
|
|
209
243
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The generated schemas coerce every int64 to a `bigint`, which no caller can
|
|
4
|
+
* add to a price or hand to a currency formatter, while the generated *type*
|
|
5
|
+
* for the same field is a `number`. Billing amounts and credit counts sit well
|
|
6
|
+
* inside the JavaScript-safe range, so they are read as safe integers.
|
|
7
|
+
*/
|
|
8
|
+
export declare const wireCents: z.ZodNumber;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The generated schemas coerce every int64 to a `bigint`, which no caller can
|
|
4
|
+
* add to a price or hand to a currency formatter, while the generated *type*
|
|
5
|
+
* for the same field is a `number`. Billing amounts and credit counts sit well
|
|
6
|
+
* inside the JavaScript-safe range, so they are read as safe integers.
|
|
7
|
+
*/
|
|
8
|
+
export const wireCents = z.number().int().safe();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { zPendingInvite } from '@comfyorg/ingest-types/zod';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { BillingResult, BillingTransport } from './billingContracts.js';
|
|
4
|
+
export declare const WORKSPACE_INVITES_ROUTE = "/workspace/invites";
|
|
5
|
+
export type WorkspaceInvite = z.infer<typeof zPendingInvite>;
|
|
6
|
+
export interface WorkspaceInviteCommands {
|
|
7
|
+
readonly listPendingInvites: () => Promise<BillingResult<readonly WorkspaceInvite[]>>;
|
|
8
|
+
readonly createInvite: (email: string) => Promise<BillingResult<WorkspaceInvite>>;
|
|
9
|
+
}
|
|
10
|
+
export declare function createWorkspaceInviteCommands(options: {
|
|
11
|
+
readonly transport: BillingTransport;
|
|
12
|
+
}): WorkspaceInviteCommands;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace's pending invites, over the same session transport the
|
|
3
|
+
* billing commands use, so a host that checks out a team plan can invite the
|
|
4
|
+
* team without a second client. The workspace is the one the transport's
|
|
5
|
+
* token was minted for.
|
|
6
|
+
*/
|
|
7
|
+
import { zCreateWorkspaceInviteResponse, zListInvitesResponse } from '@comfyorg/ingest-types/zod';
|
|
8
|
+
import { readValidatedBillingResponse } from './sharedRead.js';
|
|
9
|
+
export const WORKSPACE_INVITES_ROUTE = '/workspace/invites';
|
|
10
|
+
export function createWorkspaceInviteCommands(options) {
|
|
11
|
+
const { transport } = options;
|
|
12
|
+
async function listPendingInvites() {
|
|
13
|
+
const response = await readValidatedBillingResponse(transport, { method: 'GET', route: WORKSPACE_INVITES_ROUTE }, (body) => zListInvitesResponse.safeParse(body));
|
|
14
|
+
return response.status === 'error'
|
|
15
|
+
? response
|
|
16
|
+
: { status: 'ok', value: response.value.data.invites };
|
|
17
|
+
}
|
|
18
|
+
async function createInvite(email) {
|
|
19
|
+
const body = { email };
|
|
20
|
+
const response = await readValidatedBillingResponse(transport, { method: 'POST', route: WORKSPACE_INVITES_ROUTE, body }, (raw) => zCreateWorkspaceInviteResponse.safeParse(raw));
|
|
21
|
+
return response.status === 'error'
|
|
22
|
+
? response
|
|
23
|
+
: { status: 'ok', value: response.value.data };
|
|
24
|
+
}
|
|
25
|
+
return { listPendingInvites, createInvite };
|
|
26
|
+
}
|
package/dist/core/identity.d.ts
CHANGED
|
@@ -2,20 +2,20 @@ import type { AccountUser } from './sessionContracts.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* The identity boundary. An internal port, not a host adapter: real hosts
|
|
4
4
|
* get their implementation from `@comfyorg/account-core/firebase`; tests brand a
|
|
5
|
-
* fake through `@comfyorg/account-core/testing`. `
|
|
6
|
-
* the branded form.
|
|
5
|
+
* fake through `@comfyorg/account-core/testing`. `createSessionClient` accepts
|
|
6
|
+
* only the branded form.
|
|
7
7
|
*/
|
|
8
8
|
export interface IdentityPort<TUser extends AccountUser = AccountUser> {
|
|
9
9
|
onUserChanged: (callback: (user: TUser | null) => void) => () => void;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The brand is a compile-time gate only: `createSessionClient` rejects an
|
|
13
|
+
* unbranded port in the type system, and no runtime check remains. The
|
|
14
|
+
* package's own identity entries mint it, and `./testing` deliberately
|
|
15
|
+
* re-exports the minter as `createTestIdentity` so a suite can brand a fake.
|
|
15
16
|
*/
|
|
16
17
|
export declare const identityBrand: unique symbol;
|
|
17
18
|
export interface AccountIdentity<TUser extends AccountUser = AccountUser> extends IdentityPort<TUser> {
|
|
18
19
|
readonly [identityBrand]: true;
|
|
19
20
|
}
|
|
20
21
|
export declare function brandIdentity<TUser extends AccountUser>(port: IdentityPort<TUser>): AccountIdentity<TUser>;
|
|
21
|
-
export declare function isAccountIdentity(value: unknown): value is AccountIdentity;
|
package/dist/core/identity.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The brand is a compile-time gate only: `createSessionClient` rejects an
|
|
3
|
+
* unbranded port in the type system, and no runtime check remains. The
|
|
4
|
+
* package's own identity entries mint it, and `./testing` deliberately
|
|
5
|
+
* re-exports the minter as `createTestIdentity` so a suite can brand a fake.
|
|
5
6
|
*/
|
|
6
7
|
export const identityBrand = Symbol('@comfyorg/account-core identity');
|
|
7
8
|
export function brandIdentity(port) {
|
|
8
9
|
return { ...port, [identityBrand]: true };
|
|
9
10
|
}
|
|
10
|
-
export function isAccountIdentity(value) {
|
|
11
|
-
if (typeof value !== 'object' || value === null)
|
|
12
|
-
return false;
|
|
13
|
-
const candidate = value;
|
|
14
|
-
return (candidate[identityBrand] === true &&
|
|
15
|
-
typeof candidate.onUserChanged === 'function');
|
|
16
|
-
}
|