@comfyorg/account-core 1.0.0-alpha.1 → 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.
@@ -1,4 +1,4 @@
1
- import type { z } from 'zod';
1
+ import { z } from 'zod';
2
2
  import type { BillingScope } from './billingScope.js';
3
3
  export declare const BillingOpStatusSchema: z.ZodObject<{
4
4
  action_url: z.ZodOptional<z.ZodString>;
@@ -26,6 +26,94 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
26
26
  } & {
27
27
  amount_charged_cents: z.ZodOptional<z.ZodNumber>;
28
28
  credits_added: z.ZodOptional<z.ZodNumber>;
29
+ charge_breakdown: z.ZodOptional<z.ZodObject<{
30
+ currency: z.ZodString;
31
+ prorated: z.ZodBoolean;
32
+ } & {
33
+ amount_charged_cents: z.ZodNumber;
34
+ reasons: z.ZodArray<z.ZodObject<{
35
+ kind: z.ZodEnum<["promo_code", "subscription_discount", "account_balance"]>;
36
+ } & {
37
+ amount_cents: z.ZodNumber;
38
+ discount: z.ZodOptional<z.ZodObject<{
39
+ code: z.ZodString;
40
+ duration: z.ZodOptional<z.ZodEnum<["once", "repeating", "forever"]>>;
41
+ kind: z.ZodEnum<["plan", "promotion"]>;
42
+ name: z.ZodOptional<z.ZodString>;
43
+ } & {
44
+ amount_off_cents: z.ZodOptional<z.ZodNumber>;
45
+ duration_in_months: z.ZodOptional<z.ZodNumber>;
46
+ }, "strip", z.ZodTypeAny, {
47
+ code: string;
48
+ kind: "plan" | "promotion";
49
+ name?: string | undefined;
50
+ amount_off_cents?: number | undefined;
51
+ duration?: "once" | "repeating" | "forever" | undefined;
52
+ duration_in_months?: number | undefined;
53
+ }, {
54
+ code: string;
55
+ kind: "plan" | "promotion";
56
+ name?: string | undefined;
57
+ amount_off_cents?: number | undefined;
58
+ duration?: "once" | "repeating" | "forever" | undefined;
59
+ duration_in_months?: number | undefined;
60
+ }>>;
61
+ }, "strip", z.ZodTypeAny, {
62
+ amount_cents: number;
63
+ kind: "promo_code" | "subscription_discount" | "account_balance";
64
+ discount?: {
65
+ code: string;
66
+ kind: "plan" | "promotion";
67
+ name?: string | undefined;
68
+ amount_off_cents?: number | undefined;
69
+ duration?: "once" | "repeating" | "forever" | undefined;
70
+ duration_in_months?: number | undefined;
71
+ } | undefined;
72
+ }, {
73
+ amount_cents: number;
74
+ kind: "promo_code" | "subscription_discount" | "account_balance";
75
+ discount?: {
76
+ code: string;
77
+ kind: "plan" | "promotion";
78
+ name?: string | undefined;
79
+ amount_off_cents?: number | undefined;
80
+ duration?: "once" | "repeating" | "forever" | undefined;
81
+ duration_in_months?: number | undefined;
82
+ } | undefined;
83
+ }>, "many">;
84
+ }, "strip", z.ZodTypeAny, {
85
+ currency: string;
86
+ amount_charged_cents: number;
87
+ prorated: boolean;
88
+ reasons: {
89
+ amount_cents: number;
90
+ kind: "promo_code" | "subscription_discount" | "account_balance";
91
+ discount?: {
92
+ code: string;
93
+ kind: "plan" | "promotion";
94
+ name?: string | undefined;
95
+ amount_off_cents?: number | undefined;
96
+ duration?: "once" | "repeating" | "forever" | undefined;
97
+ duration_in_months?: number | undefined;
98
+ } | undefined;
99
+ }[];
100
+ }, {
101
+ currency: string;
102
+ amount_charged_cents: number;
103
+ prorated: boolean;
104
+ reasons: {
105
+ amount_cents: number;
106
+ kind: "promo_code" | "subscription_discount" | "account_balance";
107
+ discount?: {
108
+ code: string;
109
+ kind: "plan" | "promotion";
110
+ name?: string | undefined;
111
+ amount_off_cents?: number | undefined;
112
+ duration?: "once" | "repeating" | "forever" | undefined;
113
+ duration_in_months?: number | undefined;
114
+ } | undefined;
115
+ }[];
116
+ }>>;
29
117
  }, "strip", z.ZodTypeAny, {
30
118
  status: "failed" | "pending" | "succeeded" | "reconciliation_needed";
31
119
  id: string;
@@ -40,6 +128,23 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
40
128
  payment_intent_client_secret?: string | undefined;
41
129
  amount_charged_cents?: number | undefined;
42
130
  authentication_state?: "requires_action" | "processing" | "failed_retryable" | "succeeded" | "reconciliation_needed" | undefined;
131
+ charge_breakdown?: {
132
+ currency: string;
133
+ amount_charged_cents: number;
134
+ prorated: boolean;
135
+ reasons: {
136
+ amount_cents: number;
137
+ kind: "promo_code" | "subscription_discount" | "account_balance";
138
+ discount?: {
139
+ code: string;
140
+ kind: "plan" | "promotion";
141
+ name?: string | undefined;
142
+ amount_off_cents?: number | undefined;
143
+ duration?: "once" | "repeating" | "forever" | undefined;
144
+ duration_in_months?: number | undefined;
145
+ } | undefined;
146
+ }[];
147
+ } | undefined;
43
148
  credits_added?: number | undefined;
44
149
  decline_reason?: "card_declined" | "insufficient_funds" | "expired_card" | "incorrect_cvc" | "authentication_required" | "authentication_failed" | "processing_error" | "payment_not_completed" | "generic" | undefined;
45
150
  phase?: "in_progress" | "awaiting_payment_method" | "awaiting_invoice_payment" | undefined;
@@ -59,6 +164,23 @@ export declare const BillingOpStatusSchema: z.ZodObject<{
59
164
  payment_intent_client_secret?: string | undefined;
60
165
  amount_charged_cents?: number | undefined;
61
166
  authentication_state?: "requires_action" | "processing" | "failed_retryable" | "succeeded" | "reconciliation_needed" | undefined;
167
+ charge_breakdown?: {
168
+ currency: string;
169
+ amount_charged_cents: number;
170
+ prorated: boolean;
171
+ reasons: {
172
+ amount_cents: number;
173
+ kind: "promo_code" | "subscription_discount" | "account_balance";
174
+ discount?: {
175
+ code: string;
176
+ kind: "plan" | "promotion";
177
+ name?: string | undefined;
178
+ amount_off_cents?: number | undefined;
179
+ duration?: "once" | "repeating" | "forever" | undefined;
180
+ duration_in_months?: number | undefined;
181
+ } | undefined;
182
+ }[];
183
+ } | undefined;
62
184
  credits_added?: number | undefined;
63
185
  decline_reason?: "card_declined" | "insufficient_funds" | "expired_card" | "incorrect_cvc" | "authentication_required" | "authentication_failed" | "processing_error" | "payment_not_completed" | "generic" | undefined;
64
186
  phase?: "in_progress" | "awaiting_payment_method" | "awaiting_invoice_payment" | undefined;
@@ -11,11 +11,26 @@
11
11
  * so no server or payment-provider text can reach a consumer through this
12
12
  * state.
13
13
  */
14
- import { zBillingOpStatusResponse } from '@comfyorg/ingest-types/zod';
14
+ import { zBillingOpChargeBreakdown, zBillingOpChargeReason, zBillingOpStatusResponse, zSubscriptionDiscount } from '@comfyorg/ingest-types/zod';
15
+ import { z } from 'zod';
15
16
  import { wireCents } from './wireCents.js';
16
17
  export const BillingOpStatusSchema = zBillingOpStatusResponse.extend({
17
18
  amount_charged_cents: wireCents.optional(),
18
- credits_added: wireCents.optional()
19
+ credits_added: wireCents.optional(),
20
+ charge_breakdown: zBillingOpChargeBreakdown
21
+ .extend({
22
+ amount_charged_cents: wireCents,
23
+ reasons: z.array(zBillingOpChargeReason.extend({
24
+ amount_cents: wireCents,
25
+ discount: zSubscriptionDiscount
26
+ .extend({
27
+ amount_off_cents: wireCents.optional(),
28
+ duration_in_months: wireCents.optional()
29
+ })
30
+ .optional()
31
+ }))
32
+ })
33
+ .optional()
19
34
  });
20
35
  /**
21
36
  * The phases the contract defines as blocked on the customer. Neither advances
@@ -93,6 +93,8 @@ declare const PreviewSchema: z.ZodObject<{
93
93
  renewal_amount_cents: z.ZodOptional<z.ZodNumber>;
94
94
  subtotal_cents: z.ZodOptional<z.ZodNumber>;
95
95
  balance_applied_cents: z.ZodOptional<z.ZodNumber>;
96
+ proration_remaining_cents: z.ZodOptional<z.ZodNumber>;
97
+ proration_unused_cents: z.ZodOptional<z.ZodNumber>;
96
98
  current_plan: z.ZodOptional<z.ZodObject<{
97
99
  duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
98
100
  period_end: z.ZodOptional<z.ZodString>;
@@ -103,6 +105,8 @@ declare const PreviewSchema: z.ZodObject<{
103
105
  credits_cents: z.ZodNumber;
104
106
  price_cents: z.ZodNumber;
105
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>;
106
110
  seat_summary: z.ZodObject<{
107
111
  seat_count: z.ZodNumber;
108
112
  } & {
@@ -131,6 +135,8 @@ declare const PreviewSchema: z.ZodObject<{
131
135
  period_end?: string | undefined;
132
136
  period_start?: string | undefined;
133
137
  list_price_cents?: number | undefined;
138
+ monthly_list_price_cents?: number | undefined;
139
+ monthly_price_cents?: number | undefined;
134
140
  }, {
135
141
  price_cents: number;
136
142
  duration: "MONTHLY" | "ANNUAL";
@@ -145,6 +151,8 @@ declare const PreviewSchema: z.ZodObject<{
145
151
  period_end?: string | undefined;
146
152
  period_start?: string | undefined;
147
153
  list_price_cents?: number | undefined;
154
+ monthly_list_price_cents?: number | undefined;
155
+ monthly_price_cents?: number | undefined;
148
156
  }>>;
149
157
  new_plan: z.ZodObject<{
150
158
  duration: z.ZodEnum<["MONTHLY", "ANNUAL"]>;
@@ -156,6 +164,8 @@ declare const PreviewSchema: z.ZodObject<{
156
164
  credits_cents: z.ZodNumber;
157
165
  price_cents: z.ZodNumber;
158
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>;
159
169
  seat_summary: z.ZodObject<{
160
170
  seat_count: z.ZodNumber;
161
171
  } & {
@@ -184,6 +194,8 @@ declare const PreviewSchema: z.ZodObject<{
184
194
  period_end?: string | undefined;
185
195
  period_start?: string | undefined;
186
196
  list_price_cents?: number | undefined;
197
+ monthly_list_price_cents?: number | undefined;
198
+ monthly_price_cents?: number | undefined;
187
199
  }, {
188
200
  price_cents: number;
189
201
  duration: "MONTHLY" | "ANNUAL";
@@ -198,6 +210,8 @@ declare const PreviewSchema: z.ZodObject<{
198
210
  period_end?: string | undefined;
199
211
  period_start?: string | undefined;
200
212
  list_price_cents?: number | undefined;
213
+ monthly_list_price_cents?: number | undefined;
214
+ monthly_price_cents?: number | undefined;
201
215
  }>;
202
216
  discounts: z.ZodOptional<z.ZodArray<z.ZodObject<{
203
217
  code: z.ZodString;
@@ -244,6 +258,8 @@ declare const PreviewSchema: z.ZodObject<{
244
258
  period_end?: string | undefined;
245
259
  period_start?: string | undefined;
246
260
  list_price_cents?: number | undefined;
261
+ monthly_list_price_cents?: number | undefined;
262
+ monthly_price_cents?: number | undefined;
247
263
  };
248
264
  transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
249
265
  currency?: string | undefined;
@@ -267,6 +283,8 @@ declare const PreviewSchema: z.ZodObject<{
267
283
  period_end?: string | undefined;
268
284
  period_start?: string | undefined;
269
285
  list_price_cents?: number | undefined;
286
+ monthly_list_price_cents?: number | undefined;
287
+ monthly_price_cents?: number | undefined;
270
288
  } | undefined;
271
289
  discounts?: {
272
290
  code: string;
@@ -277,6 +295,8 @@ declare const PreviewSchema: z.ZodObject<{
277
295
  duration_in_months?: number | undefined;
278
296
  }[] | undefined;
279
297
  payment_method_configuration_id?: string | undefined;
298
+ proration_remaining_cents?: number | undefined;
299
+ proration_unused_cents?: number | undefined;
280
300
  reason?: string | undefined;
281
301
  renewal_amount_cents?: number | undefined;
282
302
  renewal_at?: string | undefined;
@@ -304,6 +324,8 @@ declare const PreviewSchema: z.ZodObject<{
304
324
  period_end?: string | undefined;
305
325
  period_start?: string | undefined;
306
326
  list_price_cents?: number | undefined;
327
+ monthly_list_price_cents?: number | undefined;
328
+ monthly_price_cents?: number | undefined;
307
329
  };
308
330
  transition_type: "new_subscription" | "upgrade" | "downgrade" | "duration_change";
309
331
  currency?: string | undefined;
@@ -327,6 +349,8 @@ declare const PreviewSchema: z.ZodObject<{
327
349
  period_end?: string | undefined;
328
350
  period_start?: string | undefined;
329
351
  list_price_cents?: number | undefined;
352
+ monthly_list_price_cents?: number | undefined;
353
+ monthly_price_cents?: number | undefined;
330
354
  } | undefined;
331
355
  discounts?: {
332
356
  code: string;
@@ -337,6 +361,8 @@ declare const PreviewSchema: z.ZodObject<{
337
361
  duration_in_months?: number | undefined;
338
362
  }[] | undefined;
339
363
  payment_method_configuration_id?: string | undefined;
364
+ proration_remaining_cents?: number | undefined;
365
+ proration_unused_cents?: number | undefined;
340
366
  reason?: string | undefined;
341
367
  renewal_amount_cents?: number | undefined;
342
368
  renewal_at?: string | undefined;
@@ -33,6 +33,8 @@ const PlanInfoSchema = zPreviewPlanInfo.extend({
33
33
  credits_cents: wireCents,
34
34
  price_cents: wireCents,
35
35
  list_price_cents: wireCents.optional(),
36
+ monthly_list_price_cents: wireCents.optional(),
37
+ monthly_price_cents: wireCents.optional(),
36
38
  seat_summary: zPreviewPlanInfo.shape.seat_summary.extend({
37
39
  total_cost_cents: wireCents,
38
40
  total_credits_cents: wireCents
@@ -47,6 +49,8 @@ const PreviewSchema = zPreviewSubscribeResponse.extend({
47
49
  renewal_amount_cents: wireCents.optional(),
48
50
  subtotal_cents: wireCents.optional(),
49
51
  balance_applied_cents: wireCents.optional(),
52
+ proration_remaining_cents: wireCents.optional(),
53
+ proration_unused_cents: wireCents.optional(),
50
54
  current_plan: PlanInfoSchema.optional(),
51
55
  new_plan: PlanInfoSchema,
52
56
  discounts: z
@@ -0,0 +1,7 @@
1
+ export interface TimedSignal {
2
+ readonly signal: AbortSignal | undefined;
3
+ /** Disarms the timer; call once the response body has been read. */
4
+ readonly release: () => void;
5
+ }
6
+ /** Joins the caller's signal with an abort after `timeoutMs`, if one is set. */
7
+ export declare function timedSignal(signal: AbortSignal | undefined, timeoutMs: number | undefined): TimedSignal;
@@ -0,0 +1,13 @@
1
+ /** Joins the caller's signal with an abort after `timeoutMs`, if one is set. */
2
+ export function timedSignal(signal, timeoutMs) {
3
+ if (timeoutMs === undefined)
4
+ return { signal, release: () => undefined };
5
+ const controller = new AbortController();
6
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
7
+ return {
8
+ signal: signal
9
+ ? AbortSignal.any([signal, controller.signal])
10
+ : controller.signal,
11
+ release: () => clearTimeout(timer)
12
+ };
13
+ }
@@ -8,6 +8,8 @@ export interface SessionTokenMintOptions {
8
8
  readonly now?: () => number;
9
9
  /** Re-mint once a cached token has this long left. Default 60s. */
10
10
  readonly refreshBufferMs?: number;
11
+ /** Caps each mint, body included; a timeout is `SESSION_UNAVAILABLE`. Default: none. */
12
+ readonly timeoutMs?: number;
11
13
  }
12
14
  export interface SessionTokenFailure extends WebSessionFailure {
13
15
  /** From a 429's `Retry-After`; no request is sent until it passes. */
@@ -30,4 +32,4 @@ export interface SessionTokenMint {
30
32
  readonly getWorkspaceToken: (workspaceId?: string) => Promise<string>;
31
33
  }
32
34
  export declare const DEFAULT_REFRESH_BUFFER_MS = 60000;
33
- export declare function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now, refreshBufferMs }: SessionTokenMintOptions): SessionTokenMint;
35
+ export declare function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now, refreshBufferMs, timeoutMs }: SessionTokenMintOptions): SessionTokenMint;
@@ -7,6 +7,7 @@
7
7
  import { zErrorResponse, zExchangeTokenResponse } from '@comfyorg/ingest-types/zod';
8
8
  import { isCredentialFresh } from './credentialCache.js';
9
9
  import { COMFY_CLIENT } from './requestAuth.js';
10
+ import { timedSignal } from './requestTimeout.js';
10
11
  export class SessionTokenError extends Error {
11
12
  failure;
12
13
  constructor(failure) {
@@ -21,7 +22,8 @@ const STATUS_RULES = {
21
22
  401: {
22
23
  byServerCode: {
23
24
  session_expired: 'SESSION_EXPIRED',
24
- session_revoked: 'SESSION_REVOKED'
25
+ session_revoked: 'SESSION_REVOKED',
26
+ TOKEN_REVOKED: 'SESSION_REVOKED'
25
27
  },
26
28
  fallback: 'NO_SESSION'
27
29
  },
@@ -31,6 +33,11 @@ const STATUS_RULES = {
31
33
  workspace_access_denied: 'WORKSPACE_ACCESS_DENIED'
32
34
  },
33
35
  fallback: 'SESSION_REQUEST_REFUSED'
36
+ },
37
+ /** The workspace is unknown, deleted, or not the user's: one remedy. */
38
+ 404: {
39
+ byServerCode: { NOT_FOUND: 'WORKSPACE_ACCESS_DENIED' },
40
+ fallback: 'SESSION_REQUEST_REFUSED'
34
41
  }
35
42
  };
36
43
  const zServerCode = zErrorResponse.pick({ code: true });
@@ -72,17 +79,17 @@ function codeFor(status, serverCode) {
72
79
  return 'SESSION_REQUEST_REFUSED';
73
80
  return rule.byServerCode[serverCode ?? ''] ?? rule.fallback;
74
81
  }
75
- async function classifyFailure(response, nowMs) {
82
+ function classifyFailure(response, body, nowMs) {
76
83
  const { status } = response;
77
84
  if (status === 429)
78
85
  return rateLimited(response, nowMs);
79
86
  if (status >= 500)
80
87
  return failure('SESSION_UNAVAILABLE', status);
81
- const parsed = zServerCode.safeParse(await readJson(response));
88
+ const parsed = zServerCode.safeParse(body);
82
89
  const serverCode = parsed.success ? parsed.data.code : undefined;
83
90
  return failure(codeFor(status, serverCode), status, serverCode);
84
91
  }
85
- export function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now = Date.now, refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS }) {
92
+ export function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now = Date.now, refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS, timeoutMs }) {
86
93
  const tokenUrl = `${apiBaseUrl.replace(/\/+$/, '')}/auth/token`;
87
94
  /** Keyed by workspace; every entry belongs to `cacheOwner`. */
88
95
  const cache = new Map();
@@ -101,9 +108,9 @@ export function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now
101
108
  rateLimit = undefined;
102
109
  }
103
110
  async function request(session, workspaceId) {
104
- let response;
111
+ const { signal, release } = timedSignal(undefined, timeoutMs);
105
112
  try {
106
- response = await fetchImpl(tokenUrl, {
113
+ const response = await fetchImpl(tokenUrl, {
107
114
  method: 'POST',
108
115
  credentials: 'include',
109
116
  headers: {
@@ -114,20 +121,30 @@ export function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now
114
121
  ? {}
115
122
  : { 'X-Comfy-Workspace-ID': workspaceId })
116
123
  },
117
- body: JSON.stringify(workspaceId === undefined ? {} : { workspace_id: workspaceId })
124
+ body: JSON.stringify(workspaceId === undefined ? {} : { workspace_id: workspaceId }),
125
+ signal
118
126
  });
127
+ const body = await readJson(response);
128
+ if (signal?.aborted)
129
+ return failure('SESSION_UNAVAILABLE');
130
+ return response.ok
131
+ ? credentialFrom(session, response.status, body)
132
+ : classifyFailure(response, body, now());
119
133
  }
120
134
  catch {
121
135
  return failure('SESSION_UNAVAILABLE');
122
136
  }
123
- if (!response.ok)
124
- return classifyFailure(response, now());
125
- const parsed = zExchangeTokenResponse.safeParse(await readJson(response));
137
+ finally {
138
+ release();
139
+ }
140
+ }
141
+ function credentialFrom(session, status, body) {
142
+ const parsed = zExchangeTokenResponse.safeParse(body);
126
143
  const expiresAt = parsed.success
127
144
  ? Date.parse(parsed.data.expires_at)
128
145
  : Number.NaN;
129
146
  if (!parsed.success || parsed.data.token === '' || !(expiresAt > now())) {
130
- return failure('SESSION_UNAVAILABLE', response.status);
147
+ return failure('SESSION_UNAVAILABLE', status);
131
148
  }
132
149
  return {
133
150
  status: 'ok',
@@ -5,6 +5,8 @@ export interface WebSessionOptions {
5
5
  readonly apiBaseUrl: string;
6
6
  readonly fetchImpl: typeof fetch;
7
7
  readonly signal?: AbortSignal;
8
+ /** Caps each request, body included; a timeout is `SESSION_UNAVAILABLE`. Default: none. */
9
+ readonly timeoutMs?: number;
8
10
  }
9
11
  export declare function readWebSession(options: WebSessionOptions, { expectedUserId }?: {
10
12
  readonly expectedUserId?: string;
@@ -4,6 +4,7 @@
4
4
  * Identity proof is an opaque string from the caller, so no provider leaks in.
5
5
  */
6
6
  import { zCreateSessionResponse, zDeleteSessionResponse, zErrorResponse, zGetSessionResponse, zRevokeAllSessionsResponse } from '@comfyorg/ingest-types/zod';
7
+ import { timedSignal } from './requestTimeout.js';
7
8
  const UNAUTHORIZED_CODES = {
8
9
  no_session: 'NO_SESSION',
9
10
  session_expired: 'SESSION_EXPIRED',
@@ -31,12 +32,11 @@ async function readJson(response) {
31
32
  return undefined;
32
33
  }
33
34
  }
34
- async function classifyFailure(response) {
35
- const { status } = response;
35
+ function classifyFailure(status, body) {
36
36
  if (status === 429 || status >= 500) {
37
37
  return failure('SESSION_UNAVAILABLE', status);
38
38
  }
39
- const parsed = zErrorResponse.safeParse(await readJson(response));
39
+ const parsed = zErrorResponse.safeParse(body);
40
40
  if (!parsed.success)
41
41
  return failure('SESSION_REQUEST_REFUSED', status);
42
42
  const serverCode = parsed.data.code;
@@ -48,24 +48,31 @@ async function classifyFailure(response) {
48
48
  return failure(byServerCode, status, serverCode);
49
49
  }
50
50
  async function send(options, path, init) {
51
- let response;
51
+ const { signal, release } = timedSignal(options.signal, options.timeoutMs);
52
52
  try {
53
- response = await options.fetchImpl(`${options.apiBaseUrl.replace(/\/+$/, '')}${path}`, { ...init, credentials: 'include', signal: options.signal });
53
+ const response = await options.fetchImpl(`${options.apiBaseUrl.replace(/\/+$/, '')}${path}`, { ...init, credentials: 'include', signal });
54
+ const body = await readJson(response);
55
+ if (signal?.aborted)
56
+ return failure('SESSION_UNAVAILABLE');
57
+ const { status } = response;
58
+ return response.ok ? { status, body } : classifyFailure(status, body);
54
59
  }
55
60
  catch {
56
61
  return failure('SESSION_UNAVAILABLE');
57
62
  }
58
- return response.ok ? { response } : classifyFailure(response);
63
+ finally {
64
+ release();
65
+ }
59
66
  }
60
67
  export async function readWebSession(options, { expectedUserId } = {}) {
61
68
  const sent = await send(options, '/auth/session', {
62
69
  method: 'GET',
63
70
  cache: 'no-store'
64
71
  });
65
- if (!('response' in sent))
72
+ if ('code' in sent)
66
73
  return sent;
67
- const { status } = sent.response;
68
- const parsed = zGetSessionResponse.safeParse(await readJson(sent.response));
74
+ const { status } = sent;
75
+ const parsed = zGetSessionResponse.safeParse(sent.body);
69
76
  if (!parsed.success)
70
77
  return failure('SESSION_UNAVAILABLE', status);
71
78
  const { user, csrf_token, expires_at, absolute_expires_at } = parsed.data;
@@ -98,22 +105,22 @@ export async function createWebSession(options, getIdentityProof, { expectedUser
98
105
  method: 'POST',
99
106
  headers: { Authorization: `Bearer ${proof}` }
100
107
  });
101
- if (!('response' in sent))
108
+ if ('code' in sent)
102
109
  return sent;
103
- const parsed = zCreateSessionResponse.safeParse(await readJson(sent.response));
110
+ const parsed = zCreateSessionResponse.safeParse(sent.body);
104
111
  if (!parsed.success || !parsed.data.success) {
105
- return failure('SESSION_UNAVAILABLE', sent.response.status);
112
+ return failure('SESSION_UNAVAILABLE', sent.status);
106
113
  }
107
114
  return readWebSession(options, { expectedUserId });
108
115
  }
109
116
  /** Sends no CSRF token: sign-out must work against a dead session. */
110
117
  export async function deleteWebSession(options) {
111
118
  const sent = await send(options, '/auth/session', { method: 'DELETE' });
112
- if (!('response' in sent))
119
+ if ('code' in sent)
113
120
  return sent;
114
- const parsed = zDeleteSessionResponse.safeParse(await readJson(sent.response));
121
+ const parsed = zDeleteSessionResponse.safeParse(sent.body);
115
122
  if (!parsed.success || !parsed.data.success) {
116
- return failure('SESSION_UNAVAILABLE', sent.response.status);
123
+ return failure('SESSION_UNAVAILABLE', sent.status);
117
124
  }
118
125
  return { status: 'ok' };
119
126
  }
@@ -127,11 +134,11 @@ export async function revokeAllWebSessions(options, csrfToken, getIdentityProof)
127
134
  method: 'POST',
128
135
  headers: { Authorization: `Bearer ${proof}`, 'X-CSRF-Token': csrfToken }
129
136
  });
130
- if (!('response' in sent))
137
+ if ('code' in sent)
131
138
  return sent;
132
- const parsed = zRevokeAllSessionsResponse.safeParse(await readJson(sent.response));
139
+ const parsed = zRevokeAllSessionsResponse.safeParse(sent.body);
133
140
  if (!parsed.success) {
134
- return failure('SESSION_UNAVAILABLE', sent.response.status);
141
+ return failure('SESSION_UNAVAILABLE', sent.status);
135
142
  }
136
143
  return { status: 'ok' };
137
144
  }
@@ -2,8 +2,7 @@
2
2
  * `unified_web_session` for a site with no sign-in of its own to ask with.
3
3
  * The anonymous `/api/features` document's global `web_session_probe` only
4
4
  * decides whether to ask; the answer is the credentialed read's
5
- * `unified_web_session` (cloud#10689). `web_session_probe` is a backend
6
- * follow-up to cloud#10689 and not served yet. Every failure is `false`.
5
+ * `unified_web_session` (cloud#10689). Every failure is `false`.
7
6
  */
8
7
  export interface FeaturesReadOptions {
9
8
  /** Cloud origin, e.g. `https://cloud.comfy.org`. */
@@ -2,8 +2,7 @@
2
2
  * `unified_web_session` for a site with no sign-in of its own to ask with.
3
3
  * The anonymous `/api/features` document's global `web_session_probe` only
4
4
  * decides whether to ask; the answer is the credentialed read's
5
- * `unified_web_session` (cloud#10689). `web_session_probe` is a backend
6
- * follow-up to cloud#10689 and not served yet. Every failure is `false`.
5
+ * `unified_web_session` (cloud#10689). Every failure is `false`.
7
6
  */
8
7
  const DEFAULT_TIMEOUT_MS = 5000;
9
8
  function readsLiteralTrue(body, key) {
@@ -136,6 +136,12 @@ export interface WebSessionIdentity {
136
136
  /** Only after an interactive sign-in; a token refresh must never call it. */
137
137
  signedIn: (getProof: () => Promise<string>) => Promise<WebSessionResult>;
138
138
  signOut: () => Promise<WebSessionCommandResult>;
139
+ /**
140
+ * Reads the session now, as a heartbeat would, and resolves once the answer
141
+ * and any restore it starts have settled. Reads nothing unless signed in as
142
+ * `expectedUserId` (any user when omitted).
143
+ */
144
+ refresh: (expectedUserId?: string) => Promise<WebSessionIdentityState>;
139
145
  /** Changes with the account; an answer started under an older epoch is stale. */
140
146
  getEpoch: () => number;
141
147
  /** Cancels timers, ignores answers in flight, and returns to idle. */
@@ -261,6 +261,7 @@ function createApiKeyIdentity() {
261
261
  retryable: false
262
262
  }),
263
263
  signOut: async () => ({ status: 'ok' }),
264
+ refresh: async () => state,
264
265
  getEpoch: () => 0,
265
266
  dispose: () => undefined
266
267
  };
@@ -280,6 +281,7 @@ function createAccountIdentity(options, login) {
280
281
  let cancelBeat;
281
282
  let releaseLeadership;
282
283
  let stopWatching = [];
284
+ let restoring = { epoch, settled: Promise.resolve() };
283
285
  function dispatch(event) {
284
286
  const next = transitionWebSessionIdentity(state, event);
285
287
  const changed = next.state !== state;
@@ -311,7 +313,7 @@ function createAccountIdentity(options, login) {
311
313
  function beat() {
312
314
  const started = epoch;
313
315
  const observed = { type: 'session_observed', from: 'this_tab' };
314
- void answer(started, observed, async () => {
316
+ return answer(started, observed, async () => {
315
317
  const result = await readWebSession(options.session);
316
318
  const shared = result.status === 'ok' || result.code === 'SESSION_REVOKED';
317
319
  if (shared && started === epoch)
@@ -343,7 +345,7 @@ function createAccountIdentity(options, login) {
343
345
  }
344
346
  function armBeat() {
345
347
  cancelBeat = schedule(() => {
346
- beat();
348
+ void beat();
347
349
  armBeat();
348
350
  }, heartbeat?.intervalMs ?? HEARTBEAT_INTERVAL_MS);
349
351
  }
@@ -377,7 +379,7 @@ function createAccountIdentity(options, login) {
377
379
  stopWatching = [
378
380
  visibility.onChange((visible) => {
379
381
  if (visible && state.phase === 'signed_in')
380
- beat();
382
+ void beat();
381
383
  syncLeadership();
382
384
  }),
383
385
  ...(crossTab ? [crossTab.onCredential(HEARTBEAT_KEY, adopt)] : [])
@@ -400,7 +402,7 @@ function createAccountIdentity(options, login) {
400
402
  void answer(epoch, { type: 'read_answered' }, () => readWebSession(options.session));
401
403
  return;
402
404
  case 'restore':
403
- void restore(epoch, effect.expectedUserId);
405
+ restoring = { epoch, settled: restore(epoch, effect.expectedUserId) };
404
406
  return;
405
407
  case 'schedule_retry':
406
408
  cancelRetry = schedule(() => {
@@ -477,6 +479,17 @@ function createAccountIdentity(options, login) {
477
479
  publish('sign_out', revoked);
478
480
  return result;
479
481
  },
482
+ refresh: async (expectedUserId) => {
483
+ if (state.phase !== 'signed_in')
484
+ return state;
485
+ if ((expectedUserId ?? state.session.user.id) !== state.session.user.id) {
486
+ return state;
487
+ }
488
+ await beat();
489
+ if (restoring.epoch === epoch)
490
+ await restoring.settled;
491
+ return state;
492
+ },
480
493
  getEpoch: () => epoch,
481
494
  dispose: () => {
482
495
  epoch += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comfyorg/account-core",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Shared account layer: a framework-free session core that exchanges a signed-in identity for the workspace-scoped Cloud JWT with valid-on-read freshness, plus the package-owned Firebase identity entry. It has no view layer and no Vue dependency; the composables and unstyled components ship from @comfyorg/account-ui",
5
5
  "keywords": [],
6
6
  "homepage": "https://comfy.org",