@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
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Workspace tokens for services that cannot read the session cookie. The
3
+ * session mints one at `POST /auth/token` on first use, and each token is
4
+ * reused per (session user, workspace) until it nears expiry. Failures come
5
+ * back as web-session codes; nothing here retries or signs out.
6
+ */
7
+ import { zErrorResponse, zExchangeTokenResponse } from '@comfyorg/ingest-types/zod';
8
+ import { isCredentialFresh } from './credentialCache.js';
9
+ import { COMFY_CLIENT } from './requestAuth.js';
10
+ import { timedSignal } from './requestTimeout.js';
11
+ export class SessionTokenError extends Error {
12
+ failure;
13
+ constructor(failure) {
14
+ super(`Workspace token mint failed: ${failure.code}`);
15
+ this.failure = failure;
16
+ this.name = 'SessionTokenError';
17
+ }
18
+ }
19
+ export const DEFAULT_REFRESH_BUFFER_MS = 60_000;
20
+ const MAX_RETRY_AFTER_MS = 10 * 60 * 1000;
21
+ const STATUS_RULES = {
22
+ 401: {
23
+ byServerCode: {
24
+ session_expired: 'SESSION_EXPIRED',
25
+ session_revoked: 'SESSION_REVOKED',
26
+ TOKEN_REVOKED: 'SESSION_REVOKED'
27
+ },
28
+ fallback: 'NO_SESSION'
29
+ },
30
+ 403: {
31
+ byServerCode: {
32
+ csrf_invalid: 'CSRF_STALE',
33
+ workspace_access_denied: 'WORKSPACE_ACCESS_DENIED'
34
+ },
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'
41
+ }
42
+ };
43
+ const zServerCode = zErrorResponse.pick({ code: true });
44
+ function failure(code, httpStatus, serverCode) {
45
+ return {
46
+ status: 'error',
47
+ code,
48
+ retryable: code === 'SESSION_UNAVAILABLE',
49
+ ...(httpStatus === undefined ? {} : { httpStatus }),
50
+ ...(serverCode === undefined ? {} : { serverCode })
51
+ };
52
+ }
53
+ async function readJson(response) {
54
+ try {
55
+ return await response.json();
56
+ }
57
+ catch {
58
+ return undefined;
59
+ }
60
+ }
61
+ /** Retry-After is delay-seconds or an HTTP-date; either is capped. */
62
+ function retryAfterMs(value, nowMs) {
63
+ if (value === null)
64
+ return undefined;
65
+ const seconds = Number(value);
66
+ const delayMs = Number.isFinite(seconds)
67
+ ? seconds * 1000
68
+ : Date.parse(value) - nowMs;
69
+ return delayMs > 0 ? Math.min(delayMs, MAX_RETRY_AFTER_MS) : undefined;
70
+ }
71
+ function rateLimited(response, nowMs) {
72
+ const limited = failure('SESSION_UNAVAILABLE', response.status);
73
+ const waitMs = retryAfterMs(response.headers.get('Retry-After'), nowMs);
74
+ return waitMs === undefined ? limited : { ...limited, retryAfterMs: waitMs };
75
+ }
76
+ function codeFor(status, serverCode) {
77
+ const rule = STATUS_RULES[status];
78
+ if (rule === undefined)
79
+ return 'SESSION_REQUEST_REFUSED';
80
+ return rule.byServerCode[serverCode ?? ''] ?? rule.fallback;
81
+ }
82
+ function classifyFailure(response, body, nowMs) {
83
+ const { status } = response;
84
+ if (status === 429)
85
+ return rateLimited(response, nowMs);
86
+ if (status >= 500)
87
+ return failure('SESSION_UNAVAILABLE', status);
88
+ const parsed = zServerCode.safeParse(body);
89
+ const serverCode = parsed.success ? parsed.data.code : undefined;
90
+ return failure(codeFor(status, serverCode), status, serverCode);
91
+ }
92
+ export function createSessionTokenMint({ apiBaseUrl, fetchImpl, getSession, now = Date.now, refreshBufferMs = DEFAULT_REFRESH_BUFFER_MS, timeoutMs }) {
93
+ const tokenUrl = `${apiBaseUrl.replace(/\/+$/, '')}/auth/token`;
94
+ /** Keyed by workspace; every entry belongs to `cacheOwner`. */
95
+ const cache = new Map();
96
+ const inFlight = new Map();
97
+ let cacheOwner;
98
+ /** Bumped on every owner change, so an older owner's answer never lands. */
99
+ let ownerGeneration = 0;
100
+ let rateLimit;
101
+ function adoptOwner(userId) {
102
+ if (cacheOwner === userId)
103
+ return;
104
+ cacheOwner = userId;
105
+ ownerGeneration += 1;
106
+ cache.clear();
107
+ inFlight.clear();
108
+ rateLimit = undefined;
109
+ }
110
+ async function request(session, workspaceId) {
111
+ const { signal, release } = timedSignal(undefined, timeoutMs);
112
+ try {
113
+ const response = await fetchImpl(tokenUrl, {
114
+ method: 'POST',
115
+ credentials: 'include',
116
+ headers: {
117
+ 'Content-Type': 'application/json',
118
+ 'X-Comfy-Client': COMFY_CLIENT,
119
+ 'X-CSRF-Token': session.csrfToken,
120
+ ...(workspaceId === undefined
121
+ ? {}
122
+ : { 'X-Comfy-Workspace-ID': workspaceId })
123
+ },
124
+ body: JSON.stringify(workspaceId === undefined ? {} : { workspace_id: workspaceId }),
125
+ signal
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());
133
+ }
134
+ catch {
135
+ return failure('SESSION_UNAVAILABLE');
136
+ }
137
+ finally {
138
+ release();
139
+ }
140
+ }
141
+ function credentialFrom(session, status, body) {
142
+ const parsed = zExchangeTokenResponse.safeParse(body);
143
+ const expiresAt = parsed.success
144
+ ? Date.parse(parsed.data.expires_at)
145
+ : Number.NaN;
146
+ if (!parsed.success || parsed.data.token === '' || !(expiresAt > now())) {
147
+ return failure('SESSION_UNAVAILABLE', status);
148
+ }
149
+ return {
150
+ status: 'ok',
151
+ credential: {
152
+ token: parsed.data.token,
153
+ expiresAt: Math.min(expiresAt, session.expiresAt),
154
+ uid: session.user.id,
155
+ workspace: parsed.data.workspace,
156
+ role: parsed.data.role,
157
+ permissions: parsed.data.permissions
158
+ }
159
+ };
160
+ }
161
+ function commit(workspaceId, generation, result) {
162
+ if (generation !== ownerGeneration)
163
+ return;
164
+ if (result.status === 'ok') {
165
+ cache.set(workspaceId, result.credential);
166
+ return;
167
+ }
168
+ if (result.httpStatus === 401)
169
+ cache.clear();
170
+ if (result.retryAfterMs !== undefined) {
171
+ rateLimit = { until: now() + result.retryAfterMs, failure: result };
172
+ }
173
+ }
174
+ async function mint(workspaceId) {
175
+ const session = getSession();
176
+ adoptOwner(session?.user.id);
177
+ if (session === undefined)
178
+ return failure('NO_SESSION');
179
+ const userId = session.user.id;
180
+ // The cache is checked before the backoff on purpose: a caller already
181
+ // holding a fresh token is still served while new mints wait.
182
+ const cached = cache.get(workspaceId);
183
+ if (cached && isCredentialFresh(cached, now(), refreshBufferMs)) {
184
+ return { status: 'ok', credential: cached };
185
+ }
186
+ if (rateLimit && now() < rateLimit.until) {
187
+ return rateLimit.failure;
188
+ }
189
+ const joined = inFlight.get(workspaceId);
190
+ if (joined)
191
+ return joined;
192
+ const generation = ownerGeneration;
193
+ const running = request(session, workspaceId).then((result) => {
194
+ if (inFlight.get(workspaceId) === running)
195
+ inFlight.delete(workspaceId);
196
+ const current = getSession();
197
+ if (current === undefined)
198
+ return failure('NO_SESSION');
199
+ if (current.user.id !== userId)
200
+ return failure('IDENTITY_CHANGED');
201
+ commit(workspaceId, generation, result);
202
+ return result;
203
+ });
204
+ inFlight.set(workspaceId, running);
205
+ return running;
206
+ }
207
+ function remint(workspaceId) {
208
+ if (!inFlight.has(workspaceId))
209
+ cache.delete(workspaceId);
210
+ return mint(workspaceId);
211
+ }
212
+ return {
213
+ mint,
214
+ remint,
215
+ async getWorkspaceToken(workspaceId) {
216
+ const result = await mint(workspaceId);
217
+ if (result.status === 'ok')
218
+ return result.credential.token;
219
+ throw new SessionTokenError(result);
220
+ }
221
+ };
222
+ }
@@ -0,0 +1,27 @@
1
+ import type { WebSessionCommandResult, WebSessionResult } from './sessionContracts.js';
2
+ export type { WebSession, WebSessionCommandResult, WebSessionErrorCode, WebSessionFailure, WebSessionResult, WebSessionUser } from './sessionContracts.js';
3
+ export interface WebSessionOptions {
4
+ /** Ingest API root, e.g. `https://cloud.comfy.org/api`. */
5
+ readonly apiBaseUrl: string;
6
+ readonly fetchImpl: typeof fetch;
7
+ readonly signal?: AbortSignal;
8
+ /** Caps each request, body included; a timeout is `SESSION_UNAVAILABLE`. Default: none. */
9
+ readonly timeoutMs?: number;
10
+ }
11
+ export declare function readWebSession(options: WebSessionOptions, { expectedUserId }?: {
12
+ readonly expectedUserId?: string;
13
+ }): Promise<WebSessionResult>;
14
+ /**
15
+ * Creates the session, then reads it back for the user and CSRF token. A
16
+ * failure to produce the proof is the provider's error and propagates.
17
+ */
18
+ export declare function createWebSession(options: WebSessionOptions, getIdentityProof: () => Promise<string>, { expectedUserId }?: {
19
+ readonly expectedUserId?: string;
20
+ }): Promise<WebSessionResult>;
21
+ /** Sends no CSRF token: sign-out must work against a dead session. */
22
+ export declare function deleteWebSession(options: WebSessionOptions): Promise<WebSessionCommandResult>;
23
+ /**
24
+ * Signs the user out of every device. Only a body the contract recognises is
25
+ * ok: the caller tells the user every device is signed out on that answer.
26
+ */
27
+ export declare function revokeAllWebSessions(options: WebSessionOptions, csrfToken: string, getIdentityProof: () => Promise<string>): Promise<WebSessionCommandResult>;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Client for the shared web session on ingest (`/api/auth/session*`). It only
3
+ * sends and classifies: retries, boot rules, and sign-out belong to callers.
4
+ * Identity proof is an opaque string from the caller, so no provider leaks in.
5
+ */
6
+ import { zCreateSessionResponse, zDeleteSessionResponse, zErrorResponse, zGetSessionResponse, zRevokeAllSessionsResponse } from '@comfyorg/ingest-types/zod';
7
+ import { timedSignal } from './requestTimeout.js';
8
+ const UNAUTHORIZED_CODES = {
9
+ no_session: 'NO_SESSION',
10
+ session_expired: 'SESSION_EXPIRED',
11
+ session_revoked: 'SESSION_REVOKED',
12
+ TOKEN_REVOKED: 'SESSION_REVOKED'
13
+ };
14
+ const FORBIDDEN_CODES = {
15
+ csrf_invalid: 'CSRF_STALE',
16
+ workspace_access_denied: 'WORKSPACE_ACCESS_DENIED'
17
+ };
18
+ function failure(code, httpStatus, serverCode) {
19
+ return {
20
+ status: 'error',
21
+ code,
22
+ retryable: code === 'SESSION_UNAVAILABLE',
23
+ ...(httpStatus === undefined ? {} : { httpStatus }),
24
+ ...(serverCode === undefined ? {} : { serverCode })
25
+ };
26
+ }
27
+ async function readJson(response) {
28
+ try {
29
+ return await response.json();
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ function classifyFailure(status, body) {
36
+ if (status === 429 || status >= 500) {
37
+ return failure('SESSION_UNAVAILABLE', status);
38
+ }
39
+ const parsed = zErrorResponse.safeParse(body);
40
+ if (!parsed.success)
41
+ return failure('SESSION_REQUEST_REFUSED', status);
42
+ const serverCode = parsed.data.code;
43
+ const byServerCode = status === 401
44
+ ? (UNAUTHORIZED_CODES[serverCode] ?? 'NO_SESSION')
45
+ : status === 403
46
+ ? (FORBIDDEN_CODES[serverCode] ?? 'SESSION_REQUEST_REFUSED')
47
+ : 'SESSION_REQUEST_REFUSED';
48
+ return failure(byServerCode, status, serverCode);
49
+ }
50
+ async function send(options, path, init) {
51
+ const { signal, release } = timedSignal(options.signal, options.timeoutMs);
52
+ try {
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);
59
+ }
60
+ catch {
61
+ return failure('SESSION_UNAVAILABLE');
62
+ }
63
+ finally {
64
+ release();
65
+ }
66
+ }
67
+ export async function readWebSession(options, { expectedUserId } = {}) {
68
+ const sent = await send(options, '/auth/session', {
69
+ method: 'GET',
70
+ cache: 'no-store'
71
+ });
72
+ if ('code' in sent)
73
+ return sent;
74
+ const { status } = sent;
75
+ const parsed = zGetSessionResponse.safeParse(sent.body);
76
+ if (!parsed.success)
77
+ return failure('SESSION_UNAVAILABLE', status);
78
+ const { user, csrf_token, expires_at, absolute_expires_at } = parsed.data;
79
+ if (expectedUserId !== undefined && user.id !== expectedUserId) {
80
+ return failure('IDENTITY_CHANGED', status);
81
+ }
82
+ return {
83
+ status: 'ok',
84
+ session: {
85
+ user: {
86
+ id: user.id,
87
+ email: user.email,
88
+ name: user.name,
89
+ emailVerified: user.email_verified,
90
+ signInProvider: user.sign_in_provider
91
+ },
92
+ csrfToken: csrf_token,
93
+ expiresAt: Date.parse(expires_at),
94
+ absoluteExpiresAt: Date.parse(absolute_expires_at)
95
+ }
96
+ };
97
+ }
98
+ /**
99
+ * Creates the session, then reads it back for the user and CSRF token. A
100
+ * failure to produce the proof is the provider's error and propagates.
101
+ */
102
+ export async function createWebSession(options, getIdentityProof, { expectedUserId } = {}) {
103
+ const proof = await getIdentityProof();
104
+ const sent = await send(options, '/auth/session', {
105
+ method: 'POST',
106
+ headers: { Authorization: `Bearer ${proof}` }
107
+ });
108
+ if ('code' in sent)
109
+ return sent;
110
+ const parsed = zCreateSessionResponse.safeParse(sent.body);
111
+ if (!parsed.success || !parsed.data.success) {
112
+ return failure('SESSION_UNAVAILABLE', sent.status);
113
+ }
114
+ return readWebSession(options, { expectedUserId });
115
+ }
116
+ /** Sends no CSRF token: sign-out must work against a dead session. */
117
+ export async function deleteWebSession(options) {
118
+ const sent = await send(options, '/auth/session', { method: 'DELETE' });
119
+ if ('code' in sent)
120
+ return sent;
121
+ const parsed = zDeleteSessionResponse.safeParse(sent.body);
122
+ if (!parsed.success || !parsed.data.success) {
123
+ return failure('SESSION_UNAVAILABLE', sent.status);
124
+ }
125
+ return { status: 'ok' };
126
+ }
127
+ /**
128
+ * Signs the user out of every device. Only a body the contract recognises is
129
+ * ok: the caller tells the user every device is signed out on that answer.
130
+ */
131
+ export async function revokeAllWebSessions(options, csrfToken, getIdentityProof) {
132
+ const proof = await getIdentityProof();
133
+ const sent = await send(options, '/auth/sessions/revoke-all', {
134
+ method: 'POST',
135
+ headers: { Authorization: `Bearer ${proof}`, 'X-CSRF-Token': csrfToken }
136
+ });
137
+ if ('code' in sent)
138
+ return sent;
139
+ const parsed = zRevokeAllSessionsResponse.safeParse(sent.body);
140
+ if (!parsed.success) {
141
+ return failure('SESSION_UNAVAILABLE', sent.status);
142
+ }
143
+ return { status: 'ok' };
144
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `unified_web_session` for a site with no sign-in of its own to ask with.
3
+ * The anonymous `/api/features` document's global `web_session_probe` only
4
+ * decides whether to ask; the answer is the credentialed read's
5
+ * `unified_web_session` (cloud#10689). Every failure is `false`.
6
+ */
7
+ export interface FeaturesReadOptions {
8
+ /** Cloud origin, e.g. `https://cloud.comfy.org`. */
9
+ readonly cloudBaseUrl: string;
10
+ readonly fetchImpl: typeof fetch;
11
+ readonly timeoutMs?: number;
12
+ }
13
+ export interface UnifiedWebSessionOptions extends FeaturesReadOptions {
14
+ /** Settles the anonymous document's `web_session_probe`. */
15
+ readonly probe: () => Promise<boolean>;
16
+ }
17
+ /** A plain anonymous GET: no cookie, no custom header, so no CORS preflight. */
18
+ export declare function readWebSessionProbe(options: FeaturesReadOptions): Promise<boolean>;
19
+ export declare function resolveUnifiedWebSession({ probe, ...options }: UnifiedWebSessionOptions): Promise<boolean>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `unified_web_session` for a site with no sign-in of its own to ask with.
3
+ * The anonymous `/api/features` document's global `web_session_probe` only
4
+ * decides whether to ask; the answer is the credentialed read's
5
+ * `unified_web_session` (cloud#10689). Every failure is `false`.
6
+ */
7
+ const DEFAULT_TIMEOUT_MS = 5000;
8
+ function readsLiteralTrue(body, key) {
9
+ return (typeof body === 'object' && body !== null && Reflect.get(body, key) === true);
10
+ }
11
+ async function readFeatures({ cloudBaseUrl, fetchImpl, timeoutMs = DEFAULT_TIMEOUT_MS }, init) {
12
+ const controller = new AbortController();
13
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
14
+ try {
15
+ const response = await fetchImpl(`${cloudBaseUrl}/api/features`, {
16
+ ...init,
17
+ signal: controller.signal
18
+ });
19
+ return response.ok ? await response.json() : undefined;
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ finally {
25
+ clearTimeout(timeout);
26
+ }
27
+ }
28
+ /** A plain anonymous GET: no cookie, no custom header, so no CORS preflight. */
29
+ export async function readWebSessionProbe(options) {
30
+ return readsLiteralTrue(await readFeatures(options, { credentials: 'omit' }), 'web_session_probe');
31
+ }
32
+ async function probeIsOn(probe) {
33
+ try {
34
+ return await probe();
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ async function loadClientHeader() {
41
+ try {
42
+ const { COMFY_CLIENT } = await import('./requestAuth.js');
43
+ return COMFY_CLIENT;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ export async function resolveUnifiedWebSession({ probe, ...options }) {
50
+ if (!(await probeIsOn(probe)))
51
+ return false;
52
+ const client = await loadClientHeader();
53
+ if (client === undefined)
54
+ return false;
55
+ const body = await readFeatures(options, {
56
+ credentials: 'include',
57
+ cache: 'no-store',
58
+ headers: { 'X-Comfy-Client': client }
59
+ });
60
+ return readsLiteralTrue(body, 'unified_web_session');
61
+ }