@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,312 @@
|
|
|
1
|
+
import { DEFAULT_FRESH_MARGIN_MS, createCredentialCache, decodeAdopted, isCredentialFresh, selectFreshCredential } from './credentialCache.js';
|
|
2
|
+
import { isAccountIdentity } from './identity.js';
|
|
3
|
+
import { abortable, exchangeToken } from './exchange.js';
|
|
4
|
+
import { createMintCoordinator } from './mintCoordinator.js';
|
|
5
|
+
import { createRefreshScheduler } from './refreshScheduler.js';
|
|
6
|
+
import { arbitrateMint, initialSessionState, scheduledMintHolds, transition } from './sessionState.js';
|
|
7
|
+
export { isPermanentSessionError } from './sessionContracts.js';
|
|
8
|
+
export { isCredentialFresh } from './credentialCache.js';
|
|
9
|
+
/**
|
|
10
|
+
* The session error codes, keys only. Hosts own the copy (the cloud app's
|
|
11
|
+
* workspaceAuth.errors.*); the package ships the vocabulary so a host can
|
|
12
|
+
* narrow an arbitrary error code to one it has a line for. Exhaustive by
|
|
13
|
+
* type: a new SessionErrorCode is a compile error until it is listed here.
|
|
14
|
+
*/
|
|
15
|
+
export const SESSION_ERROR_CODES = {
|
|
16
|
+
NOT_AUTHENTICATED: true,
|
|
17
|
+
INVALID_FIREBASE_TOKEN: true,
|
|
18
|
+
ACCESS_DENIED: true,
|
|
19
|
+
WORKSPACE_NOT_FOUND: true,
|
|
20
|
+
TOKEN_EXCHANGE_FAILED: true
|
|
21
|
+
};
|
|
22
|
+
export { SESSION_TELEMETRY_EVENT } from '../telemetry.js';
|
|
23
|
+
const DEFAULT_MINT_TIMEOUT_MS = 15_000;
|
|
24
|
+
/**
|
|
25
|
+
* The status→code mapping from `requestToken`: 401/403/404 are permanent
|
|
26
|
+
* failures with their own codes; everything else — 5xx, network failure,
|
|
27
|
+
* abort, unparseable body — collapses to TOKEN_EXCHANGE_FAILED, matching
|
|
28
|
+
* production's default branch.
|
|
29
|
+
*/
|
|
30
|
+
export function createSessionClient(clientOptions, identity) {
|
|
31
|
+
const { exchangeUrl, storage, freshMarginMs = DEFAULT_FRESH_MARGIN_MS } = clientOptions;
|
|
32
|
+
let state = initialSessionState();
|
|
33
|
+
let detachCurrent;
|
|
34
|
+
const listeners = new Set();
|
|
35
|
+
const cache = createCredentialCache(storage);
|
|
36
|
+
const mints = createMintCoordinator();
|
|
37
|
+
function getSnapshot() {
|
|
38
|
+
const { user, identitySettled, credential, failure } = state;
|
|
39
|
+
if (!user) {
|
|
40
|
+
return identitySettled
|
|
41
|
+
? { phase: 'signed-out', user: null, session: undefined }
|
|
42
|
+
: { phase: 'pending', user: null, session: undefined };
|
|
43
|
+
}
|
|
44
|
+
if (credential) {
|
|
45
|
+
return { phase: 'authenticated', user, session: credential };
|
|
46
|
+
}
|
|
47
|
+
if (failure) {
|
|
48
|
+
return { phase: 'error', user, session: undefined, failure };
|
|
49
|
+
}
|
|
50
|
+
return { phase: 'minting', user, session: undefined };
|
|
51
|
+
}
|
|
52
|
+
function publish() {
|
|
53
|
+
const snapshot = getSnapshot();
|
|
54
|
+
listeners.forEach((listener) => listener(snapshot));
|
|
55
|
+
}
|
|
56
|
+
// The exchange echoes the requested workspace on success (a non-member 404s),
|
|
57
|
+
// so a scope mismatch is a backend regression to fail closed on — shared by
|
|
58
|
+
// the direct commit and the scheduled-refresh commit.
|
|
59
|
+
function targetMismatch(session, target) {
|
|
60
|
+
return target !== undefined && session.workspace.id !== target
|
|
61
|
+
? { status: 'error', code: 'ACCESS_DENIED' }
|
|
62
|
+
: undefined;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The package's thin wrapper over the pure exchange: resolves the host's
|
|
66
|
+
* transport/timeout/clock defaults and sends the personal-workspace body.
|
|
67
|
+
*/
|
|
68
|
+
function mint(user, options) {
|
|
69
|
+
const workspaceId = options.workspaceId ?? clientOptions.workspaceId;
|
|
70
|
+
if (workspaceId === '') {
|
|
71
|
+
// An explicit empty id is an invalid selection, not personal; fail
|
|
72
|
+
// closed instead of silently minting a personal-scoped session.
|
|
73
|
+
return Promise.resolve({ status: 'error', code: 'WORKSPACE_NOT_FOUND' });
|
|
74
|
+
}
|
|
75
|
+
return exchangeToken(user, {
|
|
76
|
+
exchangeUrl,
|
|
77
|
+
body: workspaceId ? { workspace_id: workspaceId } : {},
|
|
78
|
+
fetchImpl: options.fetchImpl ?? clientOptions.fetchImpl ?? globalThis.fetch,
|
|
79
|
+
signal: options.signal ?? clientOptions.signal,
|
|
80
|
+
timeoutMs: options.timeoutMs ?? clientOptions.timeoutMs ?? DEFAULT_MINT_TIMEOUT_MS,
|
|
81
|
+
now: () => options.now?.() ?? clientOptions.now?.() ?? Date.now()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function hostNow() {
|
|
85
|
+
return clientOptions.now?.() ?? Date.now();
|
|
86
|
+
}
|
|
87
|
+
function runEffect(effect, now) {
|
|
88
|
+
switch (effect.type) {
|
|
89
|
+
case 'persist':
|
|
90
|
+
cache.write(effect.session, effect.target);
|
|
91
|
+
return;
|
|
92
|
+
case 'clearStorage':
|
|
93
|
+
cache.clear();
|
|
94
|
+
return;
|
|
95
|
+
case 'stopScheduler':
|
|
96
|
+
scheduler?.stop();
|
|
97
|
+
return;
|
|
98
|
+
case 'armScheduler':
|
|
99
|
+
scheduler?.armAfterCommit(effect.session, now());
|
|
100
|
+
return;
|
|
101
|
+
case 'abandonInFlight':
|
|
102
|
+
mints.abandon();
|
|
103
|
+
return;
|
|
104
|
+
case 'publish':
|
|
105
|
+
publish();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function commit(event, now = hostNow) {
|
|
110
|
+
const next = transition(state, event);
|
|
111
|
+
state = next.state;
|
|
112
|
+
for (const effect of next.effects)
|
|
113
|
+
runEffect(effect, now);
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
function sharedMint(user, options, forced) {
|
|
117
|
+
const target = options.workspaceId ?? clientOptions.workspaceId;
|
|
118
|
+
return mints.dispatch(user.uid, target, forced, () => {
|
|
119
|
+
const { mintSequence } = commit({ type: 'mint-started' }).state;
|
|
120
|
+
return { mintId: mintSequence, response: mint(user, options) };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function ensureCore(user, options) {
|
|
124
|
+
const now = options.now?.() ?? clientOptions.now?.() ?? Date.now();
|
|
125
|
+
const target = options.workspaceId ?? clientOptions.workspaceId;
|
|
126
|
+
const fresh = selectFreshCredential([
|
|
127
|
+
() => ({
|
|
128
|
+
credential: state.credential,
|
|
129
|
+
target: state.credentialTarget
|
|
130
|
+
}),
|
|
131
|
+
() => cache.read(user.uid)
|
|
132
|
+
], user.uid, target, now, freshMarginMs);
|
|
133
|
+
if (fresh) {
|
|
134
|
+
return {
|
|
135
|
+
mintId: state.mintSequence,
|
|
136
|
+
response: Promise.resolve({ status: 'ok', session: fresh }),
|
|
137
|
+
joined: false
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return sharedMint(user, options, false);
|
|
141
|
+
}
|
|
142
|
+
function remintCore(user, options) {
|
|
143
|
+
cache.clear();
|
|
144
|
+
return sharedMint(user, options, true);
|
|
145
|
+
}
|
|
146
|
+
const scheduler = clientOptions.refreshScheduler
|
|
147
|
+
? createRefreshScheduler(clientOptions.refreshScheduler, {
|
|
148
|
+
now: hostNow,
|
|
149
|
+
getCurrentUser: () => state.user,
|
|
150
|
+
getCredential: () => state.credential,
|
|
151
|
+
captureGuards: () => ({
|
|
152
|
+
epoch: state.identityEpoch,
|
|
153
|
+
invalidation: state.invalidationEpoch
|
|
154
|
+
}),
|
|
155
|
+
guardsHold: (guards, user, mintId) => scheduledMintHolds(state, guards, user.uid, mintId),
|
|
156
|
+
mint: (user) => sharedMint(user, { workspaceId: state.credentialTarget }, true),
|
|
157
|
+
commitRefreshed: (session, mintId) => {
|
|
158
|
+
const mismatch = targetMismatch(session, state.credentialTarget);
|
|
159
|
+
if (mismatch) {
|
|
160
|
+
commit({
|
|
161
|
+
type: 'mint-rejected',
|
|
162
|
+
origin: 'scheduler',
|
|
163
|
+
failure: mismatch
|
|
164
|
+
});
|
|
165
|
+
return mismatch;
|
|
166
|
+
}
|
|
167
|
+
commit({
|
|
168
|
+
type: 'mint-committed',
|
|
169
|
+
origin: 'scheduler',
|
|
170
|
+
session,
|
|
171
|
+
mintId
|
|
172
|
+
});
|
|
173
|
+
return undefined;
|
|
174
|
+
},
|
|
175
|
+
commitPermanentFailure: (failure) => {
|
|
176
|
+
commit({ type: 'mint-rejected', origin: 'scheduler', failure });
|
|
177
|
+
},
|
|
178
|
+
commitExpired: (expiring) => state.credential === expiring
|
|
179
|
+
? commit({ type: 'credential-expired', expiring }).state.failure
|
|
180
|
+
: undefined,
|
|
181
|
+
parseAdopted: decodeAdopted,
|
|
182
|
+
commitAdopted: (session) => {
|
|
183
|
+
commit({ type: 'credential-adopted', session });
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
: undefined;
|
|
187
|
+
async function refreshWith(core, requestedUser, options = {}) {
|
|
188
|
+
const user = requestedUser ?? state.user;
|
|
189
|
+
if (!user)
|
|
190
|
+
return undefined;
|
|
191
|
+
const startEpoch = state.identityEpoch;
|
|
192
|
+
const startInvalidation = state.invalidationEpoch;
|
|
193
|
+
const startedSignedOut = state.user === null;
|
|
194
|
+
const requestedTarget = options.workspaceId ?? clientOptions.workspaceId;
|
|
195
|
+
const { mintId, response, joined } = core(user, options);
|
|
196
|
+
// A caller that joined an in-flight mint still gets its own signal
|
|
197
|
+
// honored: the shared mint runs on, this caller stops waiting for it.
|
|
198
|
+
let result;
|
|
199
|
+
try {
|
|
200
|
+
result = options.signal
|
|
201
|
+
? await abortable(response, options.signal)
|
|
202
|
+
: await response;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return { status: 'error', code: 'TOKEN_EXCHANGE_FAILED' };
|
|
206
|
+
}
|
|
207
|
+
const arbitration = arbitrateMint(state, {
|
|
208
|
+
mintId,
|
|
209
|
+
joined,
|
|
210
|
+
explicitUser: requestedUser !== undefined,
|
|
211
|
+
userUid: user.uid,
|
|
212
|
+
requestedTarget,
|
|
213
|
+
startEpoch,
|
|
214
|
+
startInvalidation,
|
|
215
|
+
startedSignedOut
|
|
216
|
+
});
|
|
217
|
+
if (arbitration.verdict === 'superseded')
|
|
218
|
+
return undefined;
|
|
219
|
+
if (arbitration.verdict === 'reuse') {
|
|
220
|
+
return { status: 'ok', session: arbitration.session };
|
|
221
|
+
}
|
|
222
|
+
return commitCallerMint(result, mintId, requestedTarget, options);
|
|
223
|
+
}
|
|
224
|
+
function commitCallerMint(result, mintId, requestedTarget, options) {
|
|
225
|
+
const rejectMint = (failure) => {
|
|
226
|
+
commit({
|
|
227
|
+
type: 'mint-rejected',
|
|
228
|
+
origin: 'caller',
|
|
229
|
+
failure,
|
|
230
|
+
preserveCredentialOnTransientFailure: options.preserveCredentialOnTransientFailure === true
|
|
231
|
+
});
|
|
232
|
+
return failure;
|
|
233
|
+
};
|
|
234
|
+
if (result.status !== 'ok')
|
|
235
|
+
return rejectMint(result);
|
|
236
|
+
const mismatch = targetMismatch(result.session, requestedTarget);
|
|
237
|
+
if (mismatch)
|
|
238
|
+
return rejectMint(mismatch);
|
|
239
|
+
commit({
|
|
240
|
+
type: 'mint-committed',
|
|
241
|
+
origin: 'caller',
|
|
242
|
+
session: result.session,
|
|
243
|
+
target: requestedTarget,
|
|
244
|
+
mintId
|
|
245
|
+
}, () => options.now?.() ?? hostNow());
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
function subscribeIdentity(port, autoMint) {
|
|
249
|
+
if (!isAccountIdentity(port)) {
|
|
250
|
+
throw new Error('the session client needs the identity from @comfyorg/account-core/firebase (or /testing)');
|
|
251
|
+
}
|
|
252
|
+
detachCurrent?.();
|
|
253
|
+
let unsubscribe = () => undefined;
|
|
254
|
+
const detach = () => {
|
|
255
|
+
if (detachCurrent !== detach)
|
|
256
|
+
return;
|
|
257
|
+
detachCurrent = undefined;
|
|
258
|
+
unsubscribe();
|
|
259
|
+
commit({ type: 'identity-detached' });
|
|
260
|
+
};
|
|
261
|
+
detachCurrent = detach;
|
|
262
|
+
unsubscribe = port.onUserChanged((next) => {
|
|
263
|
+
if (detachCurrent !== detach)
|
|
264
|
+
return;
|
|
265
|
+
commit({ type: 'identity-changed', user: next });
|
|
266
|
+
if (detachCurrent === detach && next && autoMint) {
|
|
267
|
+
void refreshWith(ensureCore, next);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
if (detachCurrent !== detach)
|
|
271
|
+
unsubscribe();
|
|
272
|
+
return detach;
|
|
273
|
+
}
|
|
274
|
+
if (identity)
|
|
275
|
+
subscribeIdentity(identity, clientOptions.autoMint !== false);
|
|
276
|
+
return {
|
|
277
|
+
attachIdentity: (port, attachOptions) => subscribeIdentity(port, attachOptions?.autoMint !== false),
|
|
278
|
+
dispose() {
|
|
279
|
+
detachCurrent?.();
|
|
280
|
+
},
|
|
281
|
+
getSnapshot,
|
|
282
|
+
subscribe(listener) {
|
|
283
|
+
listeners.add(listener);
|
|
284
|
+
// The immediate replay runs host code; if it throws, drop the listener
|
|
285
|
+
// so a failed subscribe leaves nothing behind to publish to.
|
|
286
|
+
try {
|
|
287
|
+
listener(getSnapshot());
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
listeners.delete(listener);
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
return () => listeners.delete(listener);
|
|
294
|
+
},
|
|
295
|
+
getToken() {
|
|
296
|
+
const { user, credential } = state;
|
|
297
|
+
return credential !== undefined &&
|
|
298
|
+
user?.uid === credential.uid &&
|
|
299
|
+
isCredentialFresh(credential, hostNow(), 0)
|
|
300
|
+
? credential.token
|
|
301
|
+
: undefined;
|
|
302
|
+
},
|
|
303
|
+
ensureFresh: (requestedUser, options) => refreshWith(ensureCore, requestedUser, options),
|
|
304
|
+
remint: (requestedUser, options) => refreshWith(remintCore, requestedUser, options),
|
|
305
|
+
invalidate() {
|
|
306
|
+
commit({ type: 'invalidated' });
|
|
307
|
+
},
|
|
308
|
+
clearStoredCredential() {
|
|
309
|
+
cache.clear();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contracts the session client shares with the pieces it composes
|
|
3
|
+
* (`exchange`, `refreshScheduler`). Dependency-neutral by design: `session.ts`
|
|
4
|
+
* depends on those pieces, and they depend only on this module, never back on
|
|
5
|
+
* the client — so no top-level binding is ever read before it initializes.
|
|
6
|
+
*/
|
|
7
|
+
import type { zExchangeTokenResponse } from '@comfyorg/ingest-types/zod';
|
|
8
|
+
import type { z } from 'zod';
|
|
9
|
+
export interface AccountUser {
|
|
10
|
+
readonly uid: string;
|
|
11
|
+
getIdToken: () => Promise<string>;
|
|
12
|
+
}
|
|
13
|
+
/** Field types come from the generated POST /api/auth/token contract. */
|
|
14
|
+
type ExchangeTokenResponse = z.infer<typeof zExchangeTokenResponse>;
|
|
15
|
+
export interface AccountCredential {
|
|
16
|
+
readonly token: string;
|
|
17
|
+
/** ms since epoch */
|
|
18
|
+
readonly expiresAt: number;
|
|
19
|
+
readonly uid: string;
|
|
20
|
+
readonly workspace: ExchangeTokenResponse['workspace'];
|
|
21
|
+
readonly role: ExchangeTokenResponse['role'];
|
|
22
|
+
readonly permissions: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The production store's error taxonomy (`WorkspaceAuthError` codes),
|
|
26
|
+
* emitted as codes only — the host localizes. TOKEN_EXCHANGE_FAILED also
|
|
27
|
+
* covers unparseable bodies, network failures, aborts, and timeouts: a
|
|
28
|
+
* request that produced no usable credential is one failure bucket in
|
|
29
|
+
* production, not three.
|
|
30
|
+
*/
|
|
31
|
+
export type SessionErrorCode = 'NOT_AUTHENTICATED' | 'INVALID_FIREBASE_TOKEN' | 'ACCESS_DENIED' | 'WORKSPACE_NOT_FOUND' | 'TOKEN_EXCHANGE_FAILED';
|
|
32
|
+
export declare function isPermanentSessionError(code: SessionErrorCode): boolean;
|
|
33
|
+
export type SessionResult = {
|
|
34
|
+
readonly status: 'ok';
|
|
35
|
+
readonly session: AccountCredential;
|
|
36
|
+
} | {
|
|
37
|
+
readonly status: 'error';
|
|
38
|
+
readonly code: SessionErrorCode;
|
|
39
|
+
/** Set only when the failure came from an HTTP response, not aborted/network. */
|
|
40
|
+
readonly httpStatus?: number;
|
|
41
|
+
};
|
|
42
|
+
export type SessionFailure = Extract<SessionResult, {
|
|
43
|
+
status: 'error';
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* The result of one SCHEDULED refresh attempt as a single tagged value: the
|
|
47
|
+
* outcomes that committed a failure carry it and the rest structurally cannot,
|
|
48
|
+
* so a permanent failure without its error — or a success with one — cannot be
|
|
49
|
+
* represented.
|
|
50
|
+
*
|
|
51
|
+
* `failure?: never` is what makes "structurally cannot" true rather than
|
|
52
|
+
* aspirational. Without it, excess-property checking still rejects a bad
|
|
53
|
+
* object literal, but a report built in a variable or returned from a helper
|
|
54
|
+
* assigns cleanly.
|
|
55
|
+
*/
|
|
56
|
+
export type ScheduledRefreshReport = {
|
|
57
|
+
readonly outcome: 'succeeded';
|
|
58
|
+
readonly failure?: never;
|
|
59
|
+
} | {
|
|
60
|
+
readonly outcome: 'retry_scheduled';
|
|
61
|
+
readonly failure?: never;
|
|
62
|
+
} | {
|
|
63
|
+
readonly outcome: 'retries_exhausted';
|
|
64
|
+
readonly failure?: never;
|
|
65
|
+
} | {
|
|
66
|
+
readonly outcome: 'permanent_failure';
|
|
67
|
+
readonly failure: SessionFailure;
|
|
68
|
+
}
|
|
69
|
+
/** Retries ran out and the credential reached expiry; the client failed closed. */
|
|
70
|
+
| {
|
|
71
|
+
readonly outcome: 'expired';
|
|
72
|
+
readonly failure: SessionFailure;
|
|
73
|
+
};
|
|
74
|
+
/** The outcome discriminants a scheduled refresh can report. */
|
|
75
|
+
export type SessionRefreshOutcome = ScheduledRefreshReport['outcome'];
|
|
76
|
+
export interface MintHandle {
|
|
77
|
+
readonly mintId: number;
|
|
78
|
+
readonly response: Promise<SessionResult>;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Cross-tab refresh coordination. One tab per (uid, workspace) key holds the
|
|
82
|
+
* lease and performs the proactive refresh; the others adopt its published
|
|
83
|
+
* credential and mint for themselves only when the leader goes quiet past
|
|
84
|
+
* their jittered fallback. Real hosts wrap Web Locks + BroadcastChannel
|
|
85
|
+
* (`createWebCrossTabRefreshPort` from `@comfyorg/account-core/web`); tests pass
|
|
86
|
+
* fakes.
|
|
87
|
+
*/
|
|
88
|
+
export interface CrossTabRefreshPort {
|
|
89
|
+
/**
|
|
90
|
+
* Queue for the key's lease. `onAcquired` fires if and when this tab
|
|
91
|
+
* becomes leader; the returned function abandons the request or releases
|
|
92
|
+
* held leadership.
|
|
93
|
+
*/
|
|
94
|
+
requestLeadership: (key: string, onAcquired: () => void) => () => void;
|
|
95
|
+
publishCredential: (key: string, credential: AccountCredential) => void;
|
|
96
|
+
/** Messages cross a serialization boundary; the client validates them. */
|
|
97
|
+
onCredential: (key: string, callback: (message: unknown) => void) => () => void;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Opt-in proactive refresh, mirroring the cloud store's scheduled-refresh
|
|
101
|
+
* semantics (its buffer, retry base, and retry cap are the defaults): arm at
|
|
102
|
+
* expiry minus the buffer, retry transient failures with doubling backoff,
|
|
103
|
+
* stop on sign-out, detach, or a permanent failure. Hosts whose consumers
|
|
104
|
+
* read the token synchronously need this; valid-on-read hosts do not.
|
|
105
|
+
*/
|
|
106
|
+
export interface RefreshSchedulerOptions {
|
|
107
|
+
readonly bufferMs?: number;
|
|
108
|
+
readonly retryBaseMs?: number;
|
|
109
|
+
readonly maxRetries?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Cross-tab coordination (opt-in): the leader tab refreshes and publishes;
|
|
112
|
+
* followers adopt the published credential and fall back to their own mint
|
|
113
|
+
* only after a bounded random hold past the refresh point.
|
|
114
|
+
*/
|
|
115
|
+
readonly crossTab?: {
|
|
116
|
+
readonly port: CrossTabRefreshPort;
|
|
117
|
+
/** Upper bound for the follower's random hold. Default 15s. */
|
|
118
|
+
readonly followerJitterMs?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Fires when this tab commits a sibling's credential. Adoption is a
|
|
121
|
+
* rotation the tab did not perform itself, so a host that reacts to
|
|
122
|
+
* rotations (cookie refresh, extension hooks) needs this signal.
|
|
123
|
+
*/
|
|
124
|
+
readonly onCredentialAdopted?: (credential: AccountCredential) => void;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Called with the result of every SCHEDULED refresh attempt (never a
|
|
128
|
+
* login or caller-initiated mint), so a host can feed its refresh
|
|
129
|
+
* telemetry without owning the scheduler. A permanent failure and an
|
|
130
|
+
* expiry carry the failure the client committed, so the host never has
|
|
131
|
+
* to read it back out of the snapshot.
|
|
132
|
+
*/
|
|
133
|
+
readonly onScheduledOutcome?: (report: ScheduledRefreshReport) => void;
|
|
134
|
+
}
|
|
135
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's authoritative state and its only commit boundary. Effects
|
|
3
|
+
* come back in the order the client must run them: persist before arming
|
|
4
|
+
* the scheduler, publish last, so a host reacting to a snapshot never reads
|
|
5
|
+
* state a later step would still change.
|
|
6
|
+
*/
|
|
7
|
+
import type { AccountCredential, AccountUser, SessionFailure } from './sessionContracts.js';
|
|
8
|
+
export interface SessionState<TUser extends AccountUser = AccountUser> {
|
|
9
|
+
readonly user: TUser | null;
|
|
10
|
+
readonly identitySettled: boolean;
|
|
11
|
+
readonly credential: AccountCredential | undefined;
|
|
12
|
+
readonly credentialTarget: string | undefined;
|
|
13
|
+
readonly failure: SessionFailure | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* The result the winning mint published, so a caller joining that same mint
|
|
16
|
+
* returns it instead of running the commit/publication path a second time.
|
|
17
|
+
*/
|
|
18
|
+
readonly committedMint: {
|
|
19
|
+
readonly mintId: number;
|
|
20
|
+
readonly session: AccountCredential;
|
|
21
|
+
} | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Bumped on every identity event so a mint can tell "the listener has not
|
|
24
|
+
* settled yet" (the legitimate popup path) from "an identity event
|
|
25
|
+
* happened while I was in flight" (must invalidate).
|
|
26
|
+
*/
|
|
27
|
+
readonly identityEpoch: number;
|
|
28
|
+
readonly invalidationEpoch: number;
|
|
29
|
+
/**
|
|
30
|
+
* Monotonic id taken by every started mint; a commit is allowed only for
|
|
31
|
+
* the newest one. Target-agnostic on purpose — a slower mint for the old
|
|
32
|
+
* workspace resolving after a switch must never revert it. Ports the
|
|
33
|
+
* cloud store's unifiedRefreshRequestId guard.
|
|
34
|
+
*/
|
|
35
|
+
readonly mintSequence: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function initialSessionState<TUser extends AccountUser = AccountUser>(): SessionState<TUser>;
|
|
38
|
+
export type SessionEvent<TUser extends AccountUser = AccountUser> = {
|
|
39
|
+
readonly type: 'identity-changed';
|
|
40
|
+
readonly user: TUser | null;
|
|
41
|
+
} | {
|
|
42
|
+
readonly type: 'identity-detached';
|
|
43
|
+
} | {
|
|
44
|
+
readonly type: 'invalidated';
|
|
45
|
+
} | {
|
|
46
|
+
readonly type: 'mint-started';
|
|
47
|
+
} | {
|
|
48
|
+
readonly type: 'mint-committed';
|
|
49
|
+
readonly origin: 'caller';
|
|
50
|
+
readonly session: AccountCredential;
|
|
51
|
+
readonly target: string | undefined;
|
|
52
|
+
readonly mintId: number;
|
|
53
|
+
} | {
|
|
54
|
+
readonly type: 'mint-committed';
|
|
55
|
+
readonly origin: 'scheduler';
|
|
56
|
+
readonly session: AccountCredential;
|
|
57
|
+
readonly mintId: number;
|
|
58
|
+
} | {
|
|
59
|
+
readonly type: 'mint-rejected';
|
|
60
|
+
readonly origin: 'caller';
|
|
61
|
+
readonly failure: SessionFailure;
|
|
62
|
+
readonly preserveCredentialOnTransientFailure: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
readonly type: 'mint-rejected';
|
|
65
|
+
readonly origin: 'scheduler';
|
|
66
|
+
readonly failure: SessionFailure;
|
|
67
|
+
} | {
|
|
68
|
+
readonly type: 'credential-adopted';
|
|
69
|
+
readonly session: AccountCredential;
|
|
70
|
+
} | {
|
|
71
|
+
readonly type: 'credential-expired';
|
|
72
|
+
readonly expiring: AccountCredential;
|
|
73
|
+
};
|
|
74
|
+
export type SessionEffect = {
|
|
75
|
+
readonly type: 'persist';
|
|
76
|
+
readonly session: AccountCredential;
|
|
77
|
+
readonly target: string | undefined;
|
|
78
|
+
} | {
|
|
79
|
+
readonly type: 'armScheduler';
|
|
80
|
+
readonly session: AccountCredential;
|
|
81
|
+
} | {
|
|
82
|
+
readonly type: 'clearStorage';
|
|
83
|
+
} | {
|
|
84
|
+
readonly type: 'stopScheduler';
|
|
85
|
+
} | {
|
|
86
|
+
readonly type: 'abandonInFlight';
|
|
87
|
+
} | {
|
|
88
|
+
readonly type: 'publish';
|
|
89
|
+
};
|
|
90
|
+
export interface SessionTransition<TUser extends AccountUser = AccountUser> {
|
|
91
|
+
readonly state: SessionState<TUser>;
|
|
92
|
+
readonly effects: readonly SessionEffect[];
|
|
93
|
+
}
|
|
94
|
+
export declare function transition<TUser extends AccountUser>(state: SessionState<TUser>, event: SessionEvent<TUser>): SessionTransition<TUser>;
|
|
95
|
+
export interface MintAttempt {
|
|
96
|
+
readonly mintId: number;
|
|
97
|
+
/** Awaited another owner's in-flight mint instead of starting its own. */
|
|
98
|
+
readonly joined: boolean;
|
|
99
|
+
readonly explicitUser: boolean;
|
|
100
|
+
readonly userUid: string;
|
|
101
|
+
readonly requestedTarget: string | undefined;
|
|
102
|
+
readonly startEpoch: number;
|
|
103
|
+
readonly startInvalidation: number;
|
|
104
|
+
readonly startedSignedOut: boolean;
|
|
105
|
+
}
|
|
106
|
+
export type MintVerdict = {
|
|
107
|
+
readonly verdict: 'commit';
|
|
108
|
+
} | {
|
|
109
|
+
readonly verdict: 'reuse';
|
|
110
|
+
readonly session: AccountCredential;
|
|
111
|
+
} | {
|
|
112
|
+
readonly verdict: 'superseded';
|
|
113
|
+
};
|
|
114
|
+
export declare function arbitrateMint<TUser extends AccountUser>(state: SessionState<TUser>, attempt: MintAttempt): MintVerdict;
|
|
115
|
+
export declare function scheduledMintHolds<TUser extends AccountUser>(state: SessionState<TUser>, guards: {
|
|
116
|
+
readonly epoch: number;
|
|
117
|
+
readonly invalidation: number;
|
|
118
|
+
}, userUid: string, mintId: number): boolean;
|