@comfyorg/account-core 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/core/billing/balanceWatch.d.ts +35 -0
- package/dist/core/billing/balanceWatch.js +83 -0
- package/dist/core/billing/billingContracts.d.ts +123 -0
- package/dist/core/billing/billingContracts.js +11 -0
- package/dist/core/billing/billingErrorBody.d.ts +8 -0
- package/dist/core/billing/billingErrorBody.js +11 -0
- package/dist/core/billing/billingScope.d.ts +39 -0
- package/dist/core/billing/billingScope.js +45 -0
- package/dist/core/billing/capabilities.d.ts +70 -0
- package/dist/core/billing/capabilities.js +137 -0
- package/dist/core/billing/capabilityDenials.d.ts +26 -0
- package/dist/core/billing/capabilityDenials.js +43 -0
- package/dist/core/billing/challengeDriver.d.ts +22 -0
- package/dist/core/billing/challengeDriver.js +38 -0
- package/dist/core/billing/credentialedTransport.d.ts +35 -0
- package/dist/core/billing/credentialedTransport.js +53 -0
- package/dist/core/billing/credits.d.ts +43 -0
- package/dist/core/billing/credits.js +29 -0
- package/dist/core/billing/httpStatus.d.ts +7 -0
- package/dist/core/billing/httpStatus.js +25 -0
- package/dist/core/billing/index.d.ts +52 -0
- package/dist/core/billing/index.js +23 -0
- package/dist/core/billing/operationLifecycle.d.ts +77 -0
- package/dist/core/billing/operationLifecycle.js +547 -0
- package/dist/core/billing/operationPointer.d.ts +48 -0
- package/dist/core/billing/operationPointer.js +89 -0
- package/dist/core/billing/operationPolicy.d.ts +26 -0
- package/dist/core/billing/operationPolicy.js +51 -0
- package/dist/core/billing/operationState.d.ts +128 -0
- package/dist/core/billing/operationState.js +187 -0
- package/dist/core/billing/paymentCopy.d.ts +18 -0
- package/dist/core/billing/paymentCopy.js +47 -0
- package/dist/core/billing/paymentMethods.d.ts +40 -0
- package/dist/core/billing/paymentMethods.js +37 -0
- package/dist/core/billing/paymentProjection.d.ts +34 -0
- package/dist/core/billing/paymentProjection.js +56 -0
- package/dist/core/billing/plans.d.ts +44 -0
- package/dist/core/billing/plans.js +29 -0
- package/dist/core/billing/presentation.d.ts +20 -0
- package/dist/core/billing/presentation.js +10 -0
- package/dist/core/billing/scopedReader.d.ts +61 -0
- package/dist/core/billing/scopedReader.js +108 -0
- package/dist/core/billing/sharedRead.d.ts +33 -0
- package/dist/core/billing/sharedRead.js +59 -0
- package/dist/core/billing/status.d.ts +27 -0
- package/dist/core/billing/status.js +21 -0
- package/dist/core/billing/subscriptionCommands.d.ts +367 -0
- package/dist/core/billing/subscriptionCommands.js +263 -0
- package/dist/core/billing/topup.d.ts +113 -0
- package/dist/core/billing/topup.js +209 -0
- package/dist/core/billing/transport.d.ts +14 -0
- package/dist/core/billing/transport.js +106 -0
- package/dist/core/billing/transportExchange.d.ts +24 -0
- package/dist/core/billing/transportExchange.js +79 -0
- package/dist/core/boundedOperation.d.ts +16 -0
- package/dist/core/boundedOperation.js +12 -0
- package/dist/core/credentialCache.d.ts +41 -0
- package/dist/core/credentialCache.js +97 -0
- package/dist/core/customerRecovery.d.ts +39 -0
- package/dist/core/customerRecovery.js +79 -0
- package/dist/core/exchange.d.ts +50 -0
- package/dist/core/exchange.js +137 -0
- package/dist/core/identity.d.ts +21 -0
- package/dist/core/identity.js +16 -0
- package/dist/core/mintCoordinator.d.ts +18 -0
- package/dist/core/mintCoordinator.js +31 -0
- package/dist/core/refreshScheduler.d.ts +46 -0
- package/dist/core/refreshScheduler.js +262 -0
- package/dist/core/session.d.ts +146 -0
- package/dist/core/session.js +312 -0
- package/dist/core/sessionContracts.d.ts +135 -0
- package/dist/core/sessionContracts.js +9 -0
- package/dist/core/sessionState.d.ts +118 -0
- package/dist/core/sessionState.js +235 -0
- package/dist/firebase/index.d.ts +53 -0
- package/dist/firebase/index.js +74 -0
- package/dist/firebaseAuthError.d.ts +52 -0
- package/dist/firebaseAuthError.js +88 -0
- package/dist/loadExternalScript.d.ts +7 -0
- package/dist/loadExternalScript.js +88 -0
- package/dist/provisioning.d.ts +53 -0
- package/dist/provisioning.js +76 -0
- package/dist/redirect.d.ts +13 -0
- package/dist/redirect.js +38 -0
- package/dist/signInSchemas.d.ts +88 -0
- package/dist/signInSchemas.js +71 -0
- package/dist/telemetry.d.ts +39 -0
- package/dist/telemetry.js +26 -0
- package/dist/testing.d.ts +7 -0
- package/dist/testing.js +3 -0
- package/dist/turnstile.d.ts +14 -0
- package/dist/turnstile.js +17 -0
- package/dist/turnstileScript.d.ts +18 -0
- package/dist/turnstileScript.js +6 -0
- package/dist/web/crossTabRefresh.d.ts +18 -0
- package/dist/web/crossTabRefresh.js +121 -0
- package/dist/webviewDetection.d.ts +5 -0
- package/dist/webviewDetection.js +62 -0
- package/package.json +114 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { isPermanentSessionError } from './sessionContracts.js';
|
|
2
|
+
export const DEFAULT_BUFFER_MS = 5 * 60 * 1000;
|
|
3
|
+
function disposeQuietly(dispose, label) {
|
|
4
|
+
try {
|
|
5
|
+
dispose?.();
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
console.warn(`Cross-tab refresh ${label} teardown failed:`, error);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function createRefreshScheduler(options, host) {
|
|
12
|
+
const bufferMs = options.bufferMs ?? DEFAULT_BUFFER_MS;
|
|
13
|
+
const retryBaseMs = options.retryBaseMs ?? 5000;
|
|
14
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
15
|
+
const reportOutcome = options.onScheduledOutcome;
|
|
16
|
+
const crossTab = options.crossTab;
|
|
17
|
+
const followerJitterMs = crossTab?.followerJitterMs ?? 15_000;
|
|
18
|
+
let scheduledTimer;
|
|
19
|
+
/**
|
|
20
|
+
* The hard fail-close for a credential whose refresh chain died. Its own
|
|
21
|
+
* timer on purpose: re-arming the refresh (a retry, a promotion) must not
|
|
22
|
+
* cancel it; only a committed credential or a teardown does.
|
|
23
|
+
*/
|
|
24
|
+
let expiryTimer;
|
|
25
|
+
let scheduledRetryCount = 0;
|
|
26
|
+
/** Without coordination every tab is its own leader. */
|
|
27
|
+
let isRefreshLeader = crossTab === undefined;
|
|
28
|
+
let coordinationKey;
|
|
29
|
+
/**
|
|
30
|
+
* Bumped on every coordination teardown. A leadership grant landing on an
|
|
31
|
+
* abandoned request carries the generation it was issued under — the key
|
|
32
|
+
* string alone cannot distinguish an abandoned request from its same-key
|
|
33
|
+
* successor after a sign-out/sign-in round trip.
|
|
34
|
+
*/
|
|
35
|
+
let coordinationGeneration = 0;
|
|
36
|
+
let armingScheduledRefresh = false;
|
|
37
|
+
let releaseLeadership;
|
|
38
|
+
let stopCredentialFeed;
|
|
39
|
+
function stopScheduledRefresh() {
|
|
40
|
+
if (scheduledTimer !== undefined) {
|
|
41
|
+
clearTimeout(scheduledTimer);
|
|
42
|
+
scheduledTimer = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function clearExpiry() {
|
|
46
|
+
if (expiryTimer !== undefined) {
|
|
47
|
+
clearTimeout(expiryTimer);
|
|
48
|
+
expiryTimer = undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function teardownCoordination() {
|
|
52
|
+
coordinationGeneration += 1;
|
|
53
|
+
disposeQuietly(releaseLeadership, 'leadership release');
|
|
54
|
+
releaseLeadership = undefined;
|
|
55
|
+
disposeQuietly(stopCredentialFeed, 'credential feed');
|
|
56
|
+
stopCredentialFeed = undefined;
|
|
57
|
+
coordinationKey = undefined;
|
|
58
|
+
isRefreshLeader = crossTab === undefined;
|
|
59
|
+
}
|
|
60
|
+
function ensureCoordination() {
|
|
61
|
+
const credential = host.getCredential();
|
|
62
|
+
if (crossTab === undefined || credential === undefined)
|
|
63
|
+
return;
|
|
64
|
+
// Keyed by the SERVER-RESOLVED workspace, never the requested target: a
|
|
65
|
+
// personal {} mint resolves to a concrete workspace the target string
|
|
66
|
+
// cannot name, and tabs on one workspace must share one lease however
|
|
67
|
+
// they reached it.
|
|
68
|
+
const key = `comfy-account-refresh:${credential.uid}:${credential.workspace.id}`;
|
|
69
|
+
if (key === coordinationKey)
|
|
70
|
+
return;
|
|
71
|
+
teardownCoordination();
|
|
72
|
+
coordinationKey = key;
|
|
73
|
+
const generationAtRequest = coordinationGeneration;
|
|
74
|
+
// Cross-tab coordination is optional: a port that throws during setup
|
|
75
|
+
// must never propagate past a committed credential. Tear down whatever
|
|
76
|
+
// partial wiring landed and lead this tab on its own schedule.
|
|
77
|
+
try {
|
|
78
|
+
stopCredentialFeed = crossTab.port.onCredential(key, adoptPublishedCredential);
|
|
79
|
+
releaseLeadership = crossTab.port.requestLeadership(key, () => {
|
|
80
|
+
// A grant is honored only for the coordination generation that issued
|
|
81
|
+
// the request: abandoned requests can still win the grant race in the
|
|
82
|
+
// lock manager, and after a same-user round trip their key is
|
|
83
|
+
// byte-identical to the live request's.
|
|
84
|
+
if (generationAtRequest !== coordinationGeneration)
|
|
85
|
+
return;
|
|
86
|
+
isRefreshLeader = true;
|
|
87
|
+
// Promotion retakes the schedule unconditionally — the follower timer
|
|
88
|
+
// may be jittered, mid-retry, or already dead from exhausted retries.
|
|
89
|
+
// The latch skips the redundant re-arm when the grant fires
|
|
90
|
+
// synchronously inside armScheduledRefresh itself.
|
|
91
|
+
const live = host.getCredential();
|
|
92
|
+
if (live !== undefined && !armingScheduledRefresh) {
|
|
93
|
+
armScheduledRefresh(live.expiresAt, host.now());
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.warn('Cross-tab refresh coordination setup failed:', error);
|
|
99
|
+
teardownCoordination();
|
|
100
|
+
isRefreshLeader = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Every committed mint is published once a coordination key exists —
|
|
105
|
+
* leadership gates adoption, never publication — so the reactive 401
|
|
106
|
+
* re-mint and a leaderless follower's fallback reach siblings too: those
|
|
107
|
+
* are exactly the rotations they must not miss. Adoption itself never
|
|
108
|
+
* republishes, and the monotonic-expiry guard makes redelivery a no-op,
|
|
109
|
+
* so the channel cannot loop.
|
|
110
|
+
*/
|
|
111
|
+
function publishToSiblings(session) {
|
|
112
|
+
if (coordinationKey === undefined)
|
|
113
|
+
return;
|
|
114
|
+
// Publication is optional: a throwing port must not unwind a mint that
|
|
115
|
+
// already committed and persisted its credential.
|
|
116
|
+
try {
|
|
117
|
+
crossTab?.port.publishCredential(coordinationKey, session);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.warn('Cross-tab refresh credential publish failed:', error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function adoptPublishedCredential(message) {
|
|
124
|
+
// Any tab adopts a strictly newer credential, the leader included: a
|
|
125
|
+
// leader whose own chain died recovers from a sibling's fallback mint,
|
|
126
|
+
// and the monotonic-expiry check keeps the channel from looping.
|
|
127
|
+
const next = host.parseAdopted(message);
|
|
128
|
+
if (next === undefined)
|
|
129
|
+
return;
|
|
130
|
+
if (host.getCurrentUser()?.uid !== next.uid)
|
|
131
|
+
return;
|
|
132
|
+
// Scope check: the channel key cannot fully encode the workspace (the
|
|
133
|
+
// personal target is server-resolved), so a same-user credential minted
|
|
134
|
+
// for a DIFFERENT workspace must never switch this tab.
|
|
135
|
+
const credential = host.getCredential();
|
|
136
|
+
if (credential === undefined)
|
|
137
|
+
return;
|
|
138
|
+
if (next.workspace.id !== credential.workspace.id)
|
|
139
|
+
return;
|
|
140
|
+
if (next.expiresAt <= credential.expiresAt)
|
|
141
|
+
return;
|
|
142
|
+
clearExpiry();
|
|
143
|
+
host.commitAdopted(next);
|
|
144
|
+
armScheduledRefresh(next.expiresAt, host.now());
|
|
145
|
+
crossTab?.onCredentialAdopted?.(next);
|
|
146
|
+
}
|
|
147
|
+
function armScheduledRefresh(expiresAt, now) {
|
|
148
|
+
stopScheduledRefresh();
|
|
149
|
+
scheduledRetryCount = 0;
|
|
150
|
+
armingScheduledRefresh = true;
|
|
151
|
+
try {
|
|
152
|
+
ensureCoordination();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
armingScheduledRefresh = false;
|
|
156
|
+
}
|
|
157
|
+
// A follower holds past the leader's refresh point by bounded random
|
|
158
|
+
// jitter; when no published credential has arrived by then, the leader
|
|
159
|
+
// is gone and this tab refreshes for itself. Never tighter than one
|
|
160
|
+
// retry interval: a token already inside the buffer (a host buffer at
|
|
161
|
+
// or above the TTL, a skewed clock) would otherwise re-mint in a loop.
|
|
162
|
+
const jitter = isRefreshLeader ? 0 : Math.random() * followerJitterMs;
|
|
163
|
+
scheduledTimer = setTimeout(() => {
|
|
164
|
+
scheduledTimer = undefined;
|
|
165
|
+
void runScheduledRefresh();
|
|
166
|
+
}, Math.max(retryBaseMs, expiresAt - bufferMs - now) + jitter);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Retries are spent and the credential still has time on it: keep serving
|
|
170
|
+
* it until its expiry instant, then fail closed and tell the host, as the
|
|
171
|
+
* cloud store's clear-at-expiry does. A dead scheduler must never leave an
|
|
172
|
+
* expired token in circulation.
|
|
173
|
+
*/
|
|
174
|
+
function armClearAtExpiry(expiring) {
|
|
175
|
+
const now = host.now();
|
|
176
|
+
clearExpiry();
|
|
177
|
+
expiryTimer = setTimeout(() => {
|
|
178
|
+
expiryTimer = undefined;
|
|
179
|
+
const committed = host.commitExpired(expiring);
|
|
180
|
+
if (!committed)
|
|
181
|
+
return;
|
|
182
|
+
stopScheduledRefresh();
|
|
183
|
+
reportOutcome?.({ outcome: 'expired', failure: committed });
|
|
184
|
+
}, Math.max(0, expiring.expiresAt - now));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The scheduled re-mint mirrors the cloud store's refresh semantics: a
|
|
188
|
+
* transient failure keeps the still-valid credential and retries with
|
|
189
|
+
* doubling backoff; a permanent failure commits the error; exhausted
|
|
190
|
+
* retries keep the credential until it expires, then fail closed. Every
|
|
191
|
+
* commit runs publish() before reporting its outcome — host outcome
|
|
192
|
+
* handlers read state the publish just wrote.
|
|
193
|
+
*/
|
|
194
|
+
async function runScheduledRefresh() {
|
|
195
|
+
const user = host.getCurrentUser();
|
|
196
|
+
if (!user)
|
|
197
|
+
return;
|
|
198
|
+
const guards = host.captureGuards();
|
|
199
|
+
// Refresh with the target that produced the live credential, so a
|
|
200
|
+
// scheduled refresh reproduces the same session AND coalesces with any
|
|
201
|
+
// concurrent reactive re-mint for it.
|
|
202
|
+
const { mintId, response } = host.mint(user);
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
result = await response;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
result = { status: 'error', code: 'TOKEN_EXCHANGE_FAILED' };
|
|
209
|
+
}
|
|
210
|
+
if (!host.guardsHold(guards, user, mintId))
|
|
211
|
+
return;
|
|
212
|
+
if (result.status === 'ok') {
|
|
213
|
+
const rejected = host.commitRefreshed(result.session, mintId);
|
|
214
|
+
if (rejected) {
|
|
215
|
+
reportOutcome?.({ outcome: 'permanent_failure', failure: rejected });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
clearExpiry();
|
|
219
|
+
armScheduledRefresh(result.session.expiresAt, host.now());
|
|
220
|
+
publishToSiblings(result.session);
|
|
221
|
+
reportOutcome?.({ outcome: 'succeeded' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (isPermanentSessionError(result.code)) {
|
|
225
|
+
host.commitPermanentFailure(result);
|
|
226
|
+
reportOutcome?.({ outcome: 'permanent_failure', failure: result });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (scheduledRetryCount >= maxRetries) {
|
|
230
|
+
const live = host.getCredential();
|
|
231
|
+
if (live !== undefined)
|
|
232
|
+
armClearAtExpiry(live);
|
|
233
|
+
// A leader with a dead chain must not sit on the lease: release it so
|
|
234
|
+
// a sibling can lead, and queue again as a follower so this tab can
|
|
235
|
+
// adopt what that sibling mints or be promoted back if nobody does.
|
|
236
|
+
teardownCoordination();
|
|
237
|
+
ensureCoordination();
|
|
238
|
+
reportOutcome?.({ outcome: 'retries_exhausted' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const delay = retryBaseMs * 2 ** scheduledRetryCount;
|
|
242
|
+
scheduledRetryCount += 1;
|
|
243
|
+
stopScheduledRefresh();
|
|
244
|
+
scheduledTimer = setTimeout(() => {
|
|
245
|
+
scheduledTimer = undefined;
|
|
246
|
+
void runScheduledRefresh();
|
|
247
|
+
}, delay);
|
|
248
|
+
reportOutcome?.({ outcome: 'retry_scheduled' });
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
armAfterCommit(session, now) {
|
|
252
|
+
clearExpiry();
|
|
253
|
+
armScheduledRefresh(session.expiresAt, now);
|
|
254
|
+
publishToSiblings(session);
|
|
255
|
+
},
|
|
256
|
+
stop() {
|
|
257
|
+
stopScheduledRefresh();
|
|
258
|
+
clearExpiry();
|
|
259
|
+
teardownCoordination();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace session: a short-lived JWT minted from the signed-in
|
|
3
|
+
* identity, which is what actually authorizes runs and balance reads.
|
|
4
|
+
*
|
|
5
|
+
* The exchange contract, response schema, and error taxonomy are extracted
|
|
6
|
+
* from the cloud app's production implementation of this same POST
|
|
7
|
+
* /auth/token call (`requestToken` in
|
|
8
|
+
* src/platform/workspace/stores/workspaceAuthStore.ts) — keep the two in
|
|
9
|
+
* step. Only the freshness strategy differs: valid-on-read (callers await
|
|
10
|
+
* `ensureFresh` at the moment they need a token; it never resolves with
|
|
11
|
+
* less than `freshMarginMs` of validity) rather than a proactive refresh
|
|
12
|
+
* timer, because background tabs throttle timers and this package's callers
|
|
13
|
+
* can await. A host that needs proactive refresh wraps this client with its
|
|
14
|
+
* own scheduler.
|
|
15
|
+
*
|
|
16
|
+
* The credential cache sits behind the host's storage adapter keyed to the
|
|
17
|
+
* signed-in uid, so a token survives a reload but can never be served to a
|
|
18
|
+
* different signed-in user.
|
|
19
|
+
*/
|
|
20
|
+
import type { CredentialStorage } from './credentialCache.js';
|
|
21
|
+
import type { AccountIdentity } from './identity.js';
|
|
22
|
+
import type { AccountCredential, AccountUser, RefreshSchedulerOptions, SessionErrorCode, SessionFailure, SessionResult } from './sessionContracts.js';
|
|
23
|
+
export type { AccountIdentity } from './identity.js';
|
|
24
|
+
export type { AccountCredential, AccountUser, CrossTabRefreshPort, MintHandle, RefreshSchedulerOptions, ScheduledRefreshReport, SessionErrorCode, SessionFailure, SessionRefreshOutcome, SessionResult } from './sessionContracts.js';
|
|
25
|
+
export { isPermanentSessionError } from './sessionContracts.js';
|
|
26
|
+
export type { CredentialStorage } from './credentialCache.js';
|
|
27
|
+
export { isCredentialFresh } from './credentialCache.js';
|
|
28
|
+
/**
|
|
29
|
+
* The session error codes, keys only. Hosts own the copy (the cloud app's
|
|
30
|
+
* workspaceAuth.errors.*); the package ships the vocabulary so a host can
|
|
31
|
+
* narrow an arbitrary error code to one it has a line for. Exhaustive by
|
|
32
|
+
* type: a new SessionErrorCode is a compile error until it is listed here.
|
|
33
|
+
*/
|
|
34
|
+
export declare const SESSION_ERROR_CODES: Readonly<Record<SessionErrorCode, true>>;
|
|
35
|
+
export { SESSION_TELEMETRY_EVENT } from '../telemetry.js';
|
|
36
|
+
export interface SessionRequestOptions {
|
|
37
|
+
readonly fetchImpl?: typeof fetch;
|
|
38
|
+
readonly now?: () => number;
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
readonly timeoutMs?: number;
|
|
41
|
+
/** Mint for this workspace instead of the server-resolved personal one. */
|
|
42
|
+
readonly workspaceId?: string;
|
|
43
|
+
/**
|
|
44
|
+
* On a transient remint failure, keep the currently published credential
|
|
45
|
+
* instead of committing the error — for hosts whose still-valid token
|
|
46
|
+
* must survive a failed proactive or reactive refresh. Permanent
|
|
47
|
+
* failures always commit.
|
|
48
|
+
*/
|
|
49
|
+
readonly preserveCredentialOnTransientFailure?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface SessionClientOptions extends SessionRequestOptions {
|
|
52
|
+
readonly exchangeUrl: string;
|
|
53
|
+
readonly storage: CredentialStorage;
|
|
54
|
+
readonly freshMarginMs?: number;
|
|
55
|
+
readonly refreshScheduler?: RefreshSchedulerOptions;
|
|
56
|
+
/** Applies to the identity passed at construction; see `AttachIdentityOptions.autoMint`. */
|
|
57
|
+
readonly autoMint?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `pending` is the initial phase, before the attached identity has delivered
|
|
61
|
+
* even once, so a host can tell "Firebase has not answered yet" (pending)
|
|
62
|
+
* from "nobody is signed in" (a delivered null) without wrapping the port.
|
|
63
|
+
*/
|
|
64
|
+
export type SessionSnapshot<TUser extends AccountUser = AccountUser> = {
|
|
65
|
+
readonly phase: 'pending';
|
|
66
|
+
readonly user: null;
|
|
67
|
+
readonly session: undefined;
|
|
68
|
+
} | {
|
|
69
|
+
readonly phase: 'signed-out';
|
|
70
|
+
readonly user: null;
|
|
71
|
+
readonly session: undefined;
|
|
72
|
+
} | {
|
|
73
|
+
readonly phase: 'minting';
|
|
74
|
+
readonly user: TUser;
|
|
75
|
+
readonly session: undefined;
|
|
76
|
+
} | {
|
|
77
|
+
readonly phase: 'authenticated';
|
|
78
|
+
readonly user: TUser;
|
|
79
|
+
readonly session: AccountCredential;
|
|
80
|
+
} | {
|
|
81
|
+
readonly phase: 'error';
|
|
82
|
+
readonly user: TUser;
|
|
83
|
+
readonly session: undefined;
|
|
84
|
+
readonly failure: SessionFailure;
|
|
85
|
+
};
|
|
86
|
+
export interface AttachIdentityOptions {
|
|
87
|
+
/**
|
|
88
|
+
* When false, an identity event sets the user and publishes without
|
|
89
|
+
* starting a warm-up mint — for hosts that drive every mint explicitly.
|
|
90
|
+
*/
|
|
91
|
+
readonly autoMint?: boolean;
|
|
92
|
+
}
|
|
93
|
+
export interface SessionClient<TUser extends AccountUser = AccountUser> {
|
|
94
|
+
/** @deprecated Transitional Pinia-adapter seam; pass the identity as `createSessionClient`'s second argument instead (FE-2171, PoC #16639). */
|
|
95
|
+
attachIdentity: (identity: AccountIdentity<TUser>, options?: AttachIdentityOptions) => () => void;
|
|
96
|
+
/**
|
|
97
|
+
* Detaches the current identity; the persisted credential stays until
|
|
98
|
+
* `clearStoredCredential`. Not terminal, like a detach: an explicit-user
|
|
99
|
+
* mint issued after `dispose()` still commits and can re-arm the scheduler
|
|
100
|
+
* and cross-tab lease, so dispose and then stop calling the client.
|
|
101
|
+
*/
|
|
102
|
+
dispose: () => void;
|
|
103
|
+
getSnapshot: () => SessionSnapshot<TUser>;
|
|
104
|
+
subscribe: (listener: (snapshot: SessionSnapshot<TUser>) => void) => () => void;
|
|
105
|
+
/** The current credential's token, uid-guarded and withheld once expired (zero margin); no `freshMarginMs` or proactive-refresh promise. */
|
|
106
|
+
getToken: () => string | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* The valid-on-read entry point: resolves with a session that has more
|
|
109
|
+
* than `freshMarginMs` of validity, minting inside the call when the
|
|
110
|
+
* cache cannot promise that. Resolves undefined when nobody is signed in,
|
|
111
|
+
* when the identity changed while the mint was in flight, or when a
|
|
112
|
+
* concurrent mint for a different target superseded this call.
|
|
113
|
+
*
|
|
114
|
+
* Concurrent callers for one user share one in-flight mint, which runs
|
|
115
|
+
* with the first caller's `timeoutMs`; a later caller's own `signal`
|
|
116
|
+
* still releases that caller (with a transient failure) without
|
|
117
|
+
* cancelling the shared mint.
|
|
118
|
+
*
|
|
119
|
+
* An explicit-user call made before the identity port has ever fired
|
|
120
|
+
* (the popup path) resolves with the result, and the credential is
|
|
121
|
+
* cached — but the snapshot and getToken() stay signed-out until the
|
|
122
|
+
* port delivers that user, since the snapshot's user is the port's.
|
|
123
|
+
*/
|
|
124
|
+
ensureFresh: (requestedUser?: AccountUser, options?: SessionRequestOptions) => Promise<SessionResult | undefined>;
|
|
125
|
+
/** A mint that ignores the cache — for the one 401-retry a read allows. */
|
|
126
|
+
remint: (requestedUser?: AccountUser, options?: SessionRequestOptions) => Promise<SessionResult | undefined>;
|
|
127
|
+
/**
|
|
128
|
+
* Fail closed on a host scope change: drop the published credential,
|
|
129
|
+
* cancel scheduled work, and invalidate in-flight mints. The identity
|
|
130
|
+
* stays, because it belongs to the port, so a targeted re-mint can
|
|
131
|
+
* follow at once. Identity changes fail closed through the port itself.
|
|
132
|
+
*/
|
|
133
|
+
invalidate: () => void;
|
|
134
|
+
/**
|
|
135
|
+
* Drops only the persisted copy of the credential. The published session
|
|
136
|
+
* stays live; a host that must end it detaches or invalidates as well.
|
|
137
|
+
*/
|
|
138
|
+
clearStoredCredential: () => void;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The status→code mapping from `requestToken`: 401/403/404 are permanent
|
|
142
|
+
* failures with their own codes; everything else — 5xx, network failure,
|
|
143
|
+
* abort, unparseable body — collapses to TOKEN_EXCHANGE_FAILED, matching
|
|
144
|
+
* production's default branch.
|
|
145
|
+
*/
|
|
146
|
+
export declare function createSessionClient<TUser extends AccountUser = AccountUser>(clientOptions: SessionClientOptions, identity?: AccountIdentity<TUser>): SessionClient<TUser>;
|