@learnorama/mozaik-sdk 0.1.0-staging.12.1 → 0.1.0-staging.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,9 +35,120 @@ The token getter runs for every request, so refreshing a token does not require
35
35
  recreating the client. Clients can also supply default headers and a custom fetch
36
36
  implementation. Request options accept headers and an AbortSignal.
37
37
 
38
- There is no global token storage, automatic sign-in or automatic replay of failed
39
- writes. The application owns its PKCE flow and session refresh. Backend-owned
40
- Keycloak client secrets must never be supplied to this SDK.
38
+ The basic `createClient` transport uses the token getter you provide. For managed
39
+ PKCE and session refresh, use the SSO client below. Neither transport automatically
40
+ replays failed application writes. Backend-owned Keycloak client secrets must
41
+ never be supplied to this SDK.
42
+
43
+ ## Google Workspace and partner SSO
44
+
45
+ ```ts
46
+ // auth.ts — one instance for the browser application
47
+ import { createMozaikAuth } from '@learnorama/mozaik-sdk/auth';
48
+
49
+ export const auth = createMozaikAuth({
50
+ baseUrl: 'https://api.stg.mozaik.co',
51
+ redirectUri: 'https://your-app.example/auth/callback',
52
+ });
53
+
54
+ // A click handler: prepare returns a URL; navigation stays under your control.
55
+ const { url } = await auth.prepareInternalSso({ returnTo: '/requests' });
56
+ window.location.assign(url);
57
+
58
+ // On /auth/callback, using the same configuration after the redirect:
59
+ const { account, returnTo } = await auth.completeSso();
60
+ window.location.replace(returnTo);
61
+ ```
62
+
63
+ Register the application's callback URL in Keycloak and allow its origin in the
64
+ backend's `AUTH_ALLOWED_REDIRECT_ORIGINS`. Replace `your-app.example` with your
65
+ frontend origin. Start login on that origin so the callback can read the same
66
+ tab's transaction storage.
67
+
68
+ | Method | Behavior |
69
+ | --- | --- |
70
+ | `prepareInternalSso({ returnTo? })` | Selects the existing `google` Workspace staff provider. |
71
+ | `prepareProviderSso(alias, { returnTo? })` | Selects a registered, brokered partner; the backend validates the alias. |
72
+ | `prepareSso({ returnTo? })` | Lets the backend resolve the provider from the configured API hostname. |
73
+ | `completeSso(callbackUrl?)` | Validates and consumes the transaction, exchanges the code through the backend, and returns account, principal and return path. Defaults to the current browser URL. |
74
+ | `refresh()` | Refreshes the session; concurrent callers share one exchange. |
75
+ | `logout()` | Clears local authentication immediately and revokes the refresh token. |
76
+ | `dispose()` | Local teardown for the application owner; does not revoke the server session. |
77
+
78
+ Preparation also accepts `prompt: 'login' | 'none' | 'consent'`. Return paths must
79
+ be local absolute paths such as `/requests`, never an external URL. PKCE uses
80
+ S256; state, callback address, transaction expiry, expected provider and any
81
+ returned issuer are checked. The code exchange stays on our backend so first
82
+ login provisions the local account. Google Workspace membership and permissions
83
+ remain enforced by Keycloak and the backend.
84
+
85
+ Only short-lived PKCE transactions are saved to per-tab `sessionStorage` (ten
86
+ minutes). Access and refresh tokens stay in memory and are absent from the auth
87
+ snapshot. **Reloading the page starts without a local session**: initiate SSO again;
88
+ an existing Keycloak/Workspace session may complete it without another credential
89
+ prompt. This implementation uses full-page redirects, not popup messaging or
90
+ silent iframe authentication.
91
+
92
+ The client refreshes before expiry and on demand, coordinating token rotation.
93
+ Transient refresh failures may use an access token only until its expiry; rejected
94
+ or expired sessions clear local authentication. Remote logout errors reject the
95
+ promise while leaving the application signed out. Tokens are attached through
96
+ `auth.client`; its identity changes on login/logout and stays stable across
97
+ refreshes. Old client instances reject requests after a session change.
98
+
99
+ ## React SSO shortcut
100
+
101
+ `MozaikAuthProvider` supplies both authentication and its own TanStack Query
102
+ provider. Put SDK consumers inside it; an additional QueryClientProvider is not
103
+ needed for that subtree. `/react/auth` still contains the generated HTTP hooks;
104
+ the handwritten SSO integration is `/react/sso`.
105
+
106
+ ```tsx
107
+ import { useEffect } from 'react';
108
+ import { MozaikAuthProvider, useMozaikAuth } from '@learnorama/mozaik-sdk/react/sso';
109
+ import { useReadMe } from '@learnorama/mozaik-sdk/react/auth';
110
+ import { auth } from './auth';
111
+
112
+ export function Root() {
113
+ return <MozaikAuthProvider auth={auth}><Account /></MozaikAuthProvider>;
114
+ }
115
+
116
+ function Account() {
117
+ const { client, isAuthenticated, isLoading, error, prepareInternalSso, logout } = useMozaikAuth();
118
+ const me = useReadMe({ request: { client }, query: { enabled: isAuthenticated } });
119
+ async function signIn() {
120
+ try {
121
+ const { url } = await prepareInternalSso({ returnTo: '/requests' });
122
+ window.location.assign(url);
123
+ } catch { /* error is exposed by useMozaikAuth */ }
124
+ }
125
+ if (isAuthenticated) return <button onClick={() => { void logout().catch(() => {}); }}>Sign out {me.data?.account.email}</button>;
126
+ return <><button disabled={isLoading} onClick={() => { void signIn(); }}>Sign in with Google Workspace</button>{error && <p role="alert">{error.message}</p>}</>;
127
+ }
128
+
129
+ // Render this at /auth/callback, also inside MozaikAuthProvider.
130
+ export function SsoCallback() {
131
+ const { completeSso, error } = useMozaikAuth();
132
+ useEffect(() => {
133
+ void completeSso().then(({ returnTo }) => window.location.replace(returnTo)).catch(() => {});
134
+ }, [completeSso]);
135
+ return <p>{error ? error.message : 'Completing sign-in…'}</p>;
136
+ }
137
+ ```
138
+
139
+ Concurrent callback completions share one exchange. The callback parameters are
140
+ removed from browser history; repeated default completion calls on that cleaned
141
+ URL are safe during React remounts. Replaying an explicit consumed callback is
142
+ rejected. Login/logout replace the query cache and remount the session subtree,
143
+ preventing previous-account results from appearing. Refresh retains the cache.
144
+ For chat, use the same auth `client` and `enabled: isAuthenticated` with
145
+ `useTutoringChat`; session changes close the previous connection automatically.
146
+
147
+ Create the auth instance outside component renders, once per browser application
148
+ or per server request. The provider does not call `dispose()` on unmount: this
149
+ would invalidate the shared controller during Strict Mode's cleanup/re-setup.
150
+ The application owner may dispose it on permanent teardown. Browser operations
151
+ require Web Crypto and sessionStorage in a secure context.
41
152
 
42
153
  ## React
43
154
 
package/dist/api/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { getListMessagesUrl, getMintChatTicketUrl, getSendMessageUrl, listMessages, mintChatTicket, sendMessage } from "./tutoring-chat.js";
3
+ import { authorization, createSessionFromCode, createSessionFromCredentials, deleteSession, getAuthorizationUrl, getCreateSessionFromCodeUrl, getCreateSessionFromCredentialsUrl, getDeleteSessionUrl, getReadMeUrl, getRefreshSessionUrl, readMe, refreshSession } from "./auth.js";
3
4
  import { deleteAccount, getDeleteAccountUrl, getListServicesUrl, getReadAccountUrl, getUpdateAccountUrl, listServices, readAccount, updateAccount } from "./account.js";
4
5
  import { amendTheme, createTheme, deleteTheme, getAmendThemeUrl, getCreateThemeUrl, getDeleteThemeUrl, getListThemesUrl, getReadThemeUrl, getReorderThemesUrl, listThemes, readTheme, reorderThemes } from "./admin-catalogue.js";
5
6
  import { forceCloseLease, getForceCloseLeaseUrl, getListLiveLeasesUrl, listLiveLeases } from "./admin-consumption.js";
@@ -10,7 +11,6 @@ import { getListGrantsUrl, getReplaceGrantsUrl, listGrants, replaceGrants } from
10
11
  import { closeSupportSession, createDemoUser, getCloseSupportSessionUrl, getCreateDemoUserUrl, getOpenSupportSessionUrl, openSupportSession } from "./admin-support.js";
11
12
  import { getListTenantsUrl, getReadTenantUrl, getUpdateTenantUrl, listTenants, readTenant, updateTenant } from "./admin-tenants.js";
12
13
  import { adminCloseRequest, amendGoal, amendLevel, amendPractitioner, amendRequest, amendSubject, createGoal, createLevel, createSubject, getAdminCloseRequestUrl, getAmendGoalUrl, getAmendLevelUrl, getAmendPractitionerUrl, getAmendRequestUrl, getAmendSubjectUrl, getCreateGoalUrl, getCreateLevelUrl, getCreateSubjectUrl, getListAllRequestsUrl, getListExpiringDocumentsUrl, getListFeedbackUrl, getListPractitionerDocumentsUrl, getListPractitionersUrl, getModerateFeedbackUrl, getReadDraftUrl, getReadRequestUrl, getReviewDraftUrl, listAllRequests, listExpiringDocuments, listFeedback, listPractitionerDocuments, listPractitioners, moderateFeedback, readDraft, readRequest, reviewDraft } from "./admin-tutoring.js";
13
- import { authorization, createSessionFromCode, createSessionFromCredentials, deleteSession, getAuthorizationUrl, getCreateSessionFromCodeUrl, getCreateSessionFromCredentialsUrl, getDeleteSessionUrl, getReadMeUrl, getRefreshSessionUrl, readMe, refreshSession } from "./auth.js";
14
14
  import { getLikeContentUrl, getListEditorsUrl, getReadCatalogThemeUrl, getReadCatalogUrl, getReadContentUrl, getReadEditorUrl, getReadLearningPathUrl, getReadPackUrl, getReadRecommendationsUrl, getUnlikeContentUrl, likeContent, listEditors, readCatalog, readCatalogTheme, readContent, readEditor, readLearningPath, readPack, readRecommendations, unlikeContent } from "./biblio-catalogue.js";
15
15
  import { getReadHomeFeedUrl, readHomeFeed } from "./biblio-home.js";
16
16
  import { addBiblioFavorite, getAddBiblioFavoriteUrl, getListBiblioCategoriesUrl, getListBiblioFavoritesUrl, getListHistoryUrl, getListPacksUrl, getReadPreferencesUrl, getRemoveBiblioFavoriteUrl, getReplaceCategoryPreferencesUrl, listBiblioCategories, listBiblioFavorites, listHistory, listPacks, readPreferences, removeBiblioFavorite, replaceCategoryPreferences } from "./biblio-library.js";
package/dist/auth.d.ts ADDED
@@ -0,0 +1,10 @@
1
+
2
+ import { a as PreparedSso, i as MozaikAuthOptions, n as CompletedSso, o as SsoOptions, r as MozaikAuth, s as createMozaikAuth, t as AuthSnapshot } from "./chunks/client2.js";
3
+ //#region src/auth/transactions.d.ts
4
+ export declare class AuthError extends Error {
5
+ readonly code: string;
6
+ constructor(code: string, message: string);
7
+ }
8
+ //#endregion
9
+ export { type AuthSnapshot, type CompletedSso, type MozaikAuth, type MozaikAuthOptions, type PreparedSso, type SsoOptions, createMozaikAuth };
10
+ //# sourceMappingURL=auth.d.ts.map
package/dist/auth.js ADDED
@@ -0,0 +1,451 @@
1
+
2
+ import { n as createClient, t as ApiError } from "./chunks/client.js";
3
+ import { authorization, createSessionFromCode, deleteSession, refreshSession } from "./api/auth.js";
4
+ //#region src/auth/transactions.ts
5
+ var AuthError = class extends Error {
6
+ code;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = "AuthError";
11
+ }
12
+ };
13
+ const transactionLifetime = 6e5;
14
+ const oauthParameters = [
15
+ "code",
16
+ "state",
17
+ "iss",
18
+ "session_state",
19
+ "error",
20
+ "error_description",
21
+ "error_uri"
22
+ ];
23
+ function callbackAddress(value) {
24
+ const url = new URL(value);
25
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new TypeError("redirectUri must be an absolute HTTP(S) callback URL without credentials, query or fragment");
26
+ return url;
27
+ }
28
+ function localReturnTo(value, origin) {
29
+ if (!value.startsWith("/") || value.startsWith("//") || /[\\\u0000-\u0020\u007f]/.test(value)) throw new AuthError("invalid_return_to", "returnTo must be a local absolute path");
30
+ const url = new URL(value, origin);
31
+ if (url.origin !== origin) throw new AuthError("invalid_return_to", "returnTo must stay on this application");
32
+ return url.pathname + url.search + url.hash;
33
+ }
34
+ function randomSecret() {
35
+ return base64url(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)));
36
+ }
37
+ function base64url(bytes) {
38
+ return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
39
+ }
40
+ async function challenge(verifier) {
41
+ return base64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
42
+ }
43
+ function transactionStore(baseUrl, redirect, supplied) {
44
+ const prefix = `mozaik:sso:${encodeURIComponent(baseUrl)}:${encodeURIComponent(redirect.href)}:`;
45
+ const storage = () => {
46
+ if (supplied) return supplied;
47
+ if (typeof window === "undefined") throw new AuthError("browser_required", "SSO transactions require browser sessionStorage");
48
+ if (window.location.origin !== redirect.origin) throw new AuthError("invalid_origin", "Start SSO on the callback application origin");
49
+ return window.sessionStorage;
50
+ };
51
+ const keys = (store) => Array.from({ length: store.length }, (_, index) => store.key(index)).filter((key) => key !== null && key.startsWith(prefix));
52
+ return {
53
+ save(state, transaction) {
54
+ const store = storage();
55
+ for (const key of keys(store)) try {
56
+ const old = JSON.parse(store.getItem(key));
57
+ if (!Number.isFinite(old.createdAt) || Date.now() - old.createdAt >= 6e5) store.removeItem(key);
58
+ } catch {
59
+ store.removeItem(key);
60
+ }
61
+ store.setItem(prefix + state, JSON.stringify(transaction));
62
+ },
63
+ consume(state) {
64
+ if (!/^[A-Za-z0-9_-]{43}$/.test(state)) throw new AuthError("invalid_state", "Invalid SSO state");
65
+ const store = storage();
66
+ const saved = store.getItem(prefix + state);
67
+ if (!saved) throw new AuthError("invalid_state", "SSO transaction is missing or already consumed");
68
+ store.removeItem(prefix + state);
69
+ let value;
70
+ try {
71
+ value = JSON.parse(saved);
72
+ } catch {
73
+ throw new AuthError("invalid_state", "Invalid SSO transaction");
74
+ }
75
+ if (!value || !/^[A-Za-z0-9_-]{43}$/.test(value.verifier) || typeof value.issuer !== "string" || typeof value.returnTo !== "string" || !(value.provider === null || typeof value.provider === "string")) throw new AuthError("invalid_state", "Invalid SSO transaction");
76
+ if (!Number.isFinite(value.createdAt) || value.createdAt > Date.now() || Date.now() - value.createdAt >= 6e5) throw new AuthError("expired_transaction", "SSO transaction expired; start sign-in again");
77
+ value.returnTo = localReturnTo(value.returnTo, redirect.origin);
78
+ return value;
79
+ },
80
+ clear() {
81
+ if (!supplied && typeof window === "undefined") return;
82
+ const store = storage();
83
+ for (const key of keys(store)) store.removeItem(key);
84
+ }
85
+ };
86
+ }
87
+ function parseCallback(value, redirect) {
88
+ const url = new URL(value);
89
+ if (url.origin !== redirect.origin || url.pathname !== redirect.pathname || url.username || url.password || url.hash) throw new AuthError("invalid_callback", "SSO callback does not match redirectUri");
90
+ cleanCallback(url);
91
+ if (oauthParameters.some((key) => url.searchParams.getAll(key).length > 1)) throw new AuthError("invalid_callback", "SSO callback contains duplicate parameters");
92
+ return url;
93
+ }
94
+ function cleanCallback(url) {
95
+ const clean = new URL(url);
96
+ for (const key of oauthParameters) clean.searchParams.delete(key);
97
+ if (typeof window !== "undefined" && window.location.href === url.href) window.history.replaceState(window.history.state, "", clean.href);
98
+ return clean.href;
99
+ }
100
+ //#endregion
101
+ //#region src/auth/client.ts
102
+ const toError = (value) => value instanceof Error ? value : new AuthError("request_failed", "Authentication request failed");
103
+ const changed = () => new AuthError("session_changed", "Authentication session changed");
104
+ /** Browser SSO through Mozaik's account-provisioning backend, with no client secret. */
105
+ function createMozaikAuth(options) {
106
+ const redirect = callbackAddress(options.redirectUri);
107
+ const publicClient = createClient({
108
+ baseUrl: options.baseUrl,
109
+ fetch: options.fetch
110
+ });
111
+ const transactions = transactionStore(publicClient.baseUrl, redirect, options.transactionStorage);
112
+ const listeners = /* @__PURE__ */ new Set();
113
+ const pending = /* @__PURE__ */ new Set();
114
+ const callbacks = /* @__PURE__ */ new Map();
115
+ let completed;
116
+ let disposed = false, epoch = 0;
117
+ let session;
118
+ let error = null;
119
+ let abort = new AbortController();
120
+ let timer;
121
+ let flight;
122
+ let client = makeClient();
123
+ let snapshot = makeSnapshot();
124
+ function assertUsable() {
125
+ if (disposed) throw new AuthError("disposed", "Authentication client was disposed");
126
+ }
127
+ function assertEpoch(expected) {
128
+ assertUsable();
129
+ if (epoch !== expected) throw changed();
130
+ }
131
+ function makeSnapshot() {
132
+ return Object.freeze({
133
+ status: pending.size ? "authenticating" : flight?.epoch === epoch ? "refreshing" : session ? "authenticated" : "anonymous",
134
+ isAuthenticated: Boolean(session),
135
+ isLoading: pending.size > 0 || flight?.epoch === epoch,
136
+ account: session?.data.account ?? null,
137
+ principal: session?.data.principal ?? null,
138
+ error,
139
+ client
140
+ });
141
+ }
142
+ function publish() {
143
+ snapshot = makeSnapshot();
144
+ for (const listener of listeners) listener();
145
+ }
146
+ function clearSession() {
147
+ epoch++;
148
+ abort.abort();
149
+ abort = new AbortController();
150
+ clearTimeout(timer);
151
+ timer = void 0;
152
+ session = void 0;
153
+ pending.clear();
154
+ flight = void 0;
155
+ completed = void 0;
156
+ client = makeClient();
157
+ }
158
+ function makeClient() {
159
+ const boundEpoch = epoch, boundSignal = abort.signal;
160
+ return createClient({
161
+ baseUrl: publicClient.baseUrl,
162
+ getAccessToken: async () => {
163
+ assertEpoch(boundEpoch);
164
+ if (session && Date.now() >= session.refreshAt) try {
165
+ await refresh();
166
+ } catch (failure) {
167
+ if (!session || Date.now() >= session.expiresAt) throw failure;
168
+ }
169
+ assertEpoch(boundEpoch);
170
+ return session?.data.access_token;
171
+ },
172
+ fetch: async (url, init) => {
173
+ assertEpoch(boundEpoch);
174
+ const signal = init?.signal ? AbortSignal.any([init.signal, boundSignal]) : boundSignal;
175
+ signal.throwIfAborted();
176
+ const response = await (options.fetch ?? globalThis.fetch)(url, {
177
+ ...init,
178
+ signal
179
+ });
180
+ assertEpoch(boundEpoch);
181
+ return response;
182
+ }
183
+ });
184
+ }
185
+ function validateSession(data) {
186
+ if (!data || typeof data.access_token !== "string" || !data.access_token || !Number.isFinite(data.expires_in) || data.expires_in <= 0 || !data.account || !data.principal || typeof data.principal.subject !== "string" || data.refresh_token != null && (typeof data.refresh_token !== "string" || !data.refresh_token)) throw new AuthError("invalid_response", "Invalid authentication session response");
187
+ }
188
+ function schedule(delay) {
189
+ clearTimeout(timer);
190
+ timer = setTimeout(() => {
191
+ refresh().catch(() => {});
192
+ }, Math.min(2147483647, Math.max(0, delay)));
193
+ timer.unref?.();
194
+ }
195
+ function install(data) {
196
+ validateSession(data);
197
+ const now = Date.now(), ttl = data.expires_in * 1e3;
198
+ session = {
199
+ data,
200
+ expiresAt: now + ttl,
201
+ refreshAt: now + ttl - (data.refresh_token ? Math.min(3e4, ttl / 2) : 0),
202
+ refreshExpiresAt: data.refresh_expires_in && data.refresh_expires_in > 0 ? now + data.refresh_expires_in * 1e3 : void 0
203
+ };
204
+ error = null;
205
+ schedule(session.refreshAt - now);
206
+ }
207
+ async function revoke(token) {
208
+ if (token) await deleteSession({ refresh_token: token }, {
209
+ client: publicClient,
210
+ signal: AbortSignal.timeout(2e4)
211
+ });
212
+ }
213
+ function refresh() {
214
+ assertUsable();
215
+ if (flight?.epoch === epoch) return flight.applied;
216
+ const current = session, expected = epoch;
217
+ if (!current) return Promise.reject(new AuthError("session_expired", "Sign in before refreshing a session"));
218
+ if (!current.data.refresh_token || current.refreshExpiresAt && Date.now() >= current.refreshExpiresAt) {
219
+ const expired = new AuthError("session_expired", "Session expired; sign in again");
220
+ clearSession();
221
+ error = expired;
222
+ publish();
223
+ return Promise.reject(expired);
224
+ }
225
+ const requestAbort = new AbortController();
226
+ const response = refreshSession({ refresh_token: current.data.refresh_token }, {
227
+ client: publicClient,
228
+ signal: AbortSignal.any([requestAbort.signal, AbortSignal.timeout(2e4)])
229
+ });
230
+ const running = {
231
+ epoch: expected,
232
+ response,
233
+ applied: Promise.resolve(),
234
+ abort: requestAbort
235
+ };
236
+ flight = running;
237
+ running.applied = response.then((data) => {
238
+ assertEpoch(expected);
239
+ validateSession(data);
240
+ if (data.principal.subject !== current.data.principal.subject || data.account.id !== current.data.account.id) throw new AuthError("invalid_response", "Refresh changed the authenticated account");
241
+ install(data);
242
+ }).catch((failure) => {
243
+ if (!disposed && expected === epoch) {
244
+ error = toError(failure);
245
+ if (failure instanceof ApiError && [401, 403].includes(failure.status) || failure instanceof AuthError || Date.now() >= current.expiresAt) clearSession();
246
+ else {
247
+ current.refreshAt = Math.min(current.expiresAt, Date.now() + 5e3);
248
+ schedule(current.refreshAt - Date.now());
249
+ }
250
+ }
251
+ throw failure;
252
+ }).finally(() => {
253
+ if (flight === running) flight = void 0;
254
+ if (!disposed && expected === epoch) publish();
255
+ else if (!disposed && !session) publish();
256
+ });
257
+ publish();
258
+ return running.applied;
259
+ }
260
+ async function prepare(provider, settings = {}) {
261
+ assertUsable();
262
+ if (provider !== void 0 && (!provider.trim() || provider.length > 64)) throw new TypeError("A registered provider alias is required");
263
+ const returnTo = localReturnTo(settings.returnTo ?? "/", redirect.origin);
264
+ const expected = epoch, operation = Symbol();
265
+ pending.add(operation);
266
+ error = null;
267
+ publish();
268
+ try {
269
+ const state = randomSecret(), verifier = randomSecret(), codeChallenge = await challenge(verifier);
270
+ assertEpoch(expected);
271
+ const result = await authorization({
272
+ redirect_uri: redirect.href,
273
+ code_challenge: codeChallenge,
274
+ state,
275
+ provider,
276
+ prompt: settings.prompt
277
+ }, {
278
+ client: publicClient,
279
+ signal: AbortSignal.any([abort.signal, AbortSignal.timeout(2e4)])
280
+ });
281
+ assertEpoch(expected);
282
+ const url = new URL(result.authorizationUrl);
283
+ if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.hash || !url.pathname.endsWith("/protocol/openid-connect/auth") || [
284
+ "state",
285
+ "code_challenge",
286
+ "code_challenge_method",
287
+ "redirect_uri",
288
+ "response_type"
289
+ ].some((key) => url.searchParams.getAll(key).length !== 1) || url.searchParams.get("state") !== state || url.searchParams.get("code_challenge") !== codeChallenge || url.searchParams.get("code_challenge_method") !== "S256" || url.searchParams.get("redirect_uri") !== redirect.href || url.searchParams.get("response_type") !== "code" || provider === "google" && result.provider !== "google") throw new AuthError("invalid_response", "Invalid SSO authorization response");
290
+ const createdAt = Date.now();
291
+ transactions.save(state, {
292
+ verifier,
293
+ createdAt,
294
+ returnTo,
295
+ provider: result.provider ?? null,
296
+ issuer: url.origin + url.pathname.slice(0, -29)
297
+ });
298
+ return {
299
+ url: url.href,
300
+ provider: result.provider ?? null,
301
+ expiresAt: createdAt + transactionLifetime
302
+ };
303
+ } catch (failure) {
304
+ if (expected === epoch && !disposed) error = toError(failure);
305
+ throw failure;
306
+ } finally {
307
+ pending.delete(operation);
308
+ if (expected === epoch && !disposed) publish();
309
+ }
310
+ }
311
+ function completeSso(callbackUrl) {
312
+ try {
313
+ assertUsable();
314
+ const url = parseCallback(callbackUrl ?? (typeof window === "undefined" ? "" : window.location.href), redirect);
315
+ const state = url.searchParams.get("state") ?? "";
316
+ if (callbackUrl === void 0 && !state) {
317
+ const pendingCallback = [...callbacks.values()].find((item) => item.cleanUrl === url.href);
318
+ if (pendingCallback) return pendingCallback.promise;
319
+ if (completed?.epoch === epoch && completed.url === url.href) return Promise.resolve(completed.result);
320
+ }
321
+ const existing = callbacks.get(state);
322
+ if (existing) {
323
+ if (existing.url !== url.href) throw new AuthError("invalid_callback", "A different callback is already using this state");
324
+ return existing.promise;
325
+ }
326
+ const cleanUrl = cleanCallback(url);
327
+ const transaction = transactions.consume(state);
328
+ if (url.searchParams.has("iss") && url.searchParams.get("iss") !== transaction.issuer) throw new AuthError("invalid_issuer", "Unexpected SSO callback issuer");
329
+ if (url.searchParams.has("error")) throw new AuthError("provider_error", "Sign-in was refused or cancelled by the identity provider");
330
+ const code = url.searchParams.get("code");
331
+ if (!code || code.length > 4096) throw new AuthError("invalid_callback", "SSO callback is missing a valid authorization code");
332
+ if (session || flight || callbacks.size || pending.size) clearSession();
333
+ const expected = epoch, operation = Symbol();
334
+ pending.add(operation);
335
+ error = null;
336
+ publish();
337
+ const completion = (async () => {
338
+ try {
339
+ const data = await createSessionFromCode({
340
+ code,
341
+ codeVerifier: transaction.verifier,
342
+ redirectUri: redirect.href,
343
+ state
344
+ }, {
345
+ client: publicClient,
346
+ signal: AbortSignal.timeout(2e4)
347
+ });
348
+ if (disposed || expected !== epoch) {
349
+ await revoke(data.refresh_token).catch(() => {});
350
+ throw changed();
351
+ }
352
+ validateSession(data);
353
+ if (transaction.provider && data.principal.provider !== transaction.provider) {
354
+ await revoke(data.refresh_token).catch(() => {});
355
+ throw new AuthError("provider_mismatch", "Sign-in completed with a different identity provider");
356
+ }
357
+ clearSession();
358
+ install(data);
359
+ const result = {
360
+ account: data.account,
361
+ principal: data.principal,
362
+ returnTo: transaction.returnTo
363
+ };
364
+ completed = {
365
+ epoch,
366
+ url: cleanUrl,
367
+ result
368
+ };
369
+ publish();
370
+ return result;
371
+ } catch (failure) {
372
+ if (!disposed && expected === epoch) {
373
+ error = toError(failure);
374
+ publish();
375
+ }
376
+ throw failure;
377
+ } finally {
378
+ callbacks.delete(state);
379
+ pending.delete(operation);
380
+ if (!disposed) publish();
381
+ }
382
+ })();
383
+ callbacks.set(state, {
384
+ url: url.href,
385
+ cleanUrl,
386
+ promise: completion
387
+ });
388
+ return completion;
389
+ } catch (failure) {
390
+ if (!disposed) {
391
+ error = toError(failure);
392
+ publish();
393
+ }
394
+ return Promise.reject(failure);
395
+ }
396
+ }
397
+ return {
398
+ get client() {
399
+ return client;
400
+ },
401
+ getSnapshot: () => snapshot,
402
+ subscribe(listener) {
403
+ listeners.add(listener);
404
+ return () => {
405
+ listeners.delete(listener);
406
+ };
407
+ },
408
+ prepareInternalSso: (settings) => prepare("google", settings),
409
+ prepareProviderSso: (alias, settings) => prepare(alias, settings),
410
+ prepareSso: (settings) => prepare(void 0, settings),
411
+ completeSso,
412
+ refresh,
413
+ async logout() {
414
+ assertUsable();
415
+ const old = session, rotating = flight;
416
+ clearSession();
417
+ error = null;
418
+ publish();
419
+ const expected = epoch;
420
+ let cleanupError;
421
+ try {
422
+ transactions.clear();
423
+ } catch (failure) {
424
+ cleanupError = failure;
425
+ }
426
+ try {
427
+ await revoke((await rotating?.response.catch(() => void 0))?.refresh_token ?? old?.data.refresh_token);
428
+ if (cleanupError) throw cleanupError;
429
+ } catch (failure) {
430
+ if (!disposed && expected === epoch) {
431
+ error = toError(failure);
432
+ publish();
433
+ }
434
+ throw failure;
435
+ }
436
+ },
437
+ dispose() {
438
+ if (disposed) return;
439
+ flight?.abort.abort();
440
+ clearSession();
441
+ disposed = true;
442
+ error = null;
443
+ publish();
444
+ listeners.clear();
445
+ }
446
+ };
447
+ }
448
+ //#endregion
449
+ export { AuthError, createMozaikAuth };
450
+
451
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../src/auth/transactions.ts","../src/auth/client.ts"],"sourcesContent":["// Handwritten. This directory is never an Orval output/cleanup target.\nexport interface Transaction {\n verifier: string;\n createdAt: number;\n returnTo: string;\n issuer: string;\n provider: string | null;\n}\n\nexport class AuthError extends Error {\n constructor(public readonly code: string, message: string) {\n super(message);\n this.name = 'AuthError';\n }\n}\n\nexport const transactionLifetime = 10 * 60_000;\nconst oauthParameters = ['code', 'state', 'iss', 'session_state', 'error', 'error_description', 'error_uri'];\n\nexport function callbackAddress(value: string): URL {\n const url = new URL(value);\n if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {\n throw new TypeError('redirectUri must be an absolute HTTP(S) callback URL without credentials, query or fragment');\n }\n return url;\n}\n\nexport function localReturnTo(value: string, origin: string): string {\n // Reject protocol-relative URLs and browser-normalized slash/control tricks.\n if (!value.startsWith('/') || value.startsWith('//') || /[\\\\\\u0000-\\u0020\\u007f]/.test(value)) {\n throw new AuthError('invalid_return_to', 'returnTo must be a local absolute path');\n }\n const url = new URL(value, origin);\n if (url.origin !== origin) throw new AuthError('invalid_return_to', 'returnTo must stay on this application');\n return url.pathname + url.search + url.hash;\n}\n\nexport function randomSecret(): string {\n return base64url(crypto.getRandomValues(new Uint8Array(32)));\n}\nfunction base64url(bytes: Uint8Array): string {\n return btoa(String.fromCharCode(...bytes)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');\n}\nexport async function challenge(verifier: string): Promise<string> {\n return base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));\n}\n\nexport function transactionStore(baseUrl: string, redirect: URL, supplied?: Storage) {\n const prefix = `mozaik:sso:${encodeURIComponent(baseUrl)}:${encodeURIComponent(redirect.href)}:`;\n const storage = () => {\n if (supplied) return supplied;\n if (typeof window === 'undefined') throw new AuthError('browser_required', 'SSO transactions require browser sessionStorage');\n if (window.location.origin !== redirect.origin) throw new AuthError('invalid_origin', 'Start SSO on the callback application origin');\n return window.sessionStorage;\n };\n const keys = (store: Storage) => Array.from({ length: store.length }, (_, index) => store.key(index))\n .filter((key): key is string => key !== null && key.startsWith(prefix));\n return {\n save(state: string, transaction: Transaction) {\n const store = storage();\n for (const key of keys(store)) {\n try {\n const old = JSON.parse(store.getItem(key)!);\n if (!Number.isFinite(old.createdAt) || Date.now() - old.createdAt >= transactionLifetime) store.removeItem(key);\n } catch { store.removeItem(key); }\n }\n store.setItem(prefix + state, JSON.stringify(transaction));\n },\n consume(state: string): Transaction {\n if (!/^[A-Za-z0-9_-]{43}$/.test(state)) throw new AuthError('invalid_state', 'Invalid SSO state');\n const store = storage();\n const saved = store.getItem(prefix + state);\n if (!saved) throw new AuthError('invalid_state', 'SSO transaction is missing or already consumed');\n store.removeItem(prefix + state);\n let value: Transaction;\n try { value = JSON.parse(saved); }\n catch { throw new AuthError('invalid_state', 'Invalid SSO transaction'); }\n if (!value || !/^[A-Za-z0-9_-]{43}$/.test(value.verifier) || typeof value.issuer !== 'string' ||\n typeof value.returnTo !== 'string' || !(value.provider === null || typeof value.provider === 'string')) {\n throw new AuthError('invalid_state', 'Invalid SSO transaction');\n }\n if (!Number.isFinite(value.createdAt) || value.createdAt > Date.now() || Date.now() - value.createdAt >= transactionLifetime) {\n throw new AuthError('expired_transaction', 'SSO transaction expired; start sign-in again');\n }\n value.returnTo = localReturnTo(value.returnTo, redirect.origin);\n return value;\n },\n clear() {\n // Factory creation/disposal on the server does not access browser globals.\n if (!supplied && typeof window === 'undefined') return;\n const store = storage();\n for (const key of keys(store)) store.removeItem(key);\n },\n };\n}\n\nexport function parseCallback(value: string, redirect: URL): URL {\n const url = new URL(value);\n if (url.origin !== redirect.origin || url.pathname !== redirect.pathname || url.username || url.password || url.hash) {\n throw new AuthError('invalid_callback', 'SSO callback does not match redirectUri');\n }\n cleanCallback(url);\n if (oauthParameters.some(key => url.searchParams.getAll(key).length > 1)) {\n throw new AuthError('invalid_callback', 'SSO callback contains duplicate parameters');\n }\n return url;\n}\n\nexport function cleanCallback(url: URL): string {\n const clean = new URL(url);\n for (const key of oauthParameters) clean.searchParams.delete(key);\n if (typeof window !== 'undefined' && window.location.href === url.href) {\n window.history.replaceState(window.history.state, '', clean.href);\n }\n return clean.href;\n}\n","import { ApiError, createClient, type Client, type ClientOptions } from '../client';\nimport { authorization, createSessionFromCode, deleteSession, refreshSession } from '../api/auth/auth';\nimport type { AccountRead, PrincipalRead, SessionRead } from '../models';\nimport { AuthError, callbackAddress, challenge, cleanCallback, localReturnTo, parseCallback, randomSecret, transactionLifetime, transactionStore } from './transactions';\n\nexport interface MozaikAuthOptions {\n baseUrl: string;\n redirectUri: string;\n /** Only PKCE transactions are persisted. Access/refresh tokens stay in memory. */\n transactionStorage?: Storage;\n fetch?: ClientOptions['fetch'];\n}\nexport interface SsoOptions {\n returnTo?: string;\n prompt?: 'login' | 'none' | 'consent';\n}\nexport interface PreparedSso { url: string; provider: string | null; expiresAt: number }\nexport interface CompletedSso { account: AccountRead; principal: PrincipalRead; returnTo: string }\nexport interface AuthSnapshot {\n status: 'anonymous' | 'authenticating' | 'authenticated' | 'refreshing';\n isAuthenticated: boolean;\n isLoading: boolean;\n account: AccountRead | null;\n principal: PrincipalRead | null;\n error: Error | null;\n /** Stable across refreshes; replaced on login/logout to isolate account caches. */\n client: Client;\n}\nexport interface MozaikAuth {\n readonly client: Client;\n getSnapshot(): AuthSnapshot;\n subscribe(listener: () => void): () => void;\n prepareInternalSso(options?: SsoOptions): Promise<PreparedSso>;\n prepareProviderSso(alias: string, options?: SsoOptions): Promise<PreparedSso>;\n prepareSso(options?: SsoOptions): Promise<PreparedSso>;\n completeSso(callbackUrl?: string): Promise<CompletedSso>;\n refresh(): Promise<void>;\n logout(): Promise<void>;\n /** Local teardown only. Use logout() to revoke the server session. */\n dispose(): void;\n}\n\ninterface Session { data: SessionRead; expiresAt: number; refreshAt: number; refreshExpiresAt?: number }\nconst toError = (value: unknown) => value instanceof Error ? value : new AuthError('request_failed', 'Authentication request failed');\nconst changed = () => new AuthError('session_changed', 'Authentication session changed');\n\n/** Browser SSO through Mozaik's account-provisioning backend, with no client secret. */\nexport function createMozaikAuth(options: MozaikAuthOptions): MozaikAuth {\n const redirect = callbackAddress(options.redirectUri);\n const publicClient = createClient({ baseUrl: options.baseUrl, fetch: options.fetch });\n const transactions = transactionStore(publicClient.baseUrl, redirect, options.transactionStorage);\n const listeners = new Set<() => void>();\n const pending = new Set<symbol>();\n const callbacks = new Map<string, { url: string; cleanUrl: string; promise: Promise<CompletedSso> }>();\n let completed: { epoch: number; url: string; result: CompletedSso } | undefined;\n let disposed = false, epoch = 0;\n let session: Session | undefined;\n let error: Error | null = null;\n let abort = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let flight: { epoch: number; response: Promise<SessionRead>; applied: Promise<void>; abort: AbortController } | undefined;\n let client = makeClient();\n let snapshot = makeSnapshot();\n\n function assertUsable() { if (disposed) throw new AuthError('disposed', 'Authentication client was disposed'); }\n function assertEpoch(expected: number) { assertUsable(); if (epoch !== expected) throw changed(); }\n function makeSnapshot(): AuthSnapshot {\n return Object.freeze({\n status: pending.size ? 'authenticating' : flight?.epoch === epoch ? 'refreshing' : session ? 'authenticated' : 'anonymous',\n isAuthenticated: Boolean(session), isLoading: pending.size > 0 || flight?.epoch === epoch,\n account: session?.data.account ?? null, principal: session?.data.principal ?? null, error, client,\n });\n }\n function publish() { snapshot = makeSnapshot(); for (const listener of listeners) listener(); }\n function clearSession() {\n epoch++;\n abort.abort(); abort = new AbortController();\n clearTimeout(timer); timer = undefined;\n session = undefined; pending.clear(); flight = undefined; completed = undefined;\n client = makeClient();\n }\n function makeClient(): Client {\n const boundEpoch = epoch, boundSignal = abort.signal;\n return createClient({\n baseUrl: publicClient.baseUrl,\n getAccessToken: async () => {\n assertEpoch(boundEpoch);\n if (session && Date.now() >= session.refreshAt) {\n try { await refresh(); }\n catch (failure) {\n // A transient early-refresh failure can still use a valid token.\n if (!session || Date.now() >= session.expiresAt) throw failure;\n }\n }\n assertEpoch(boundEpoch);\n return session?.data.access_token;\n },\n fetch: async (url, init) => {\n assertEpoch(boundEpoch);\n const signal = init?.signal ? AbortSignal.any([init.signal, boundSignal]) : boundSignal;\n signal.throwIfAborted();\n const response = await (options.fetch ?? globalThis.fetch)(url, { ...init, signal });\n assertEpoch(boundEpoch);\n return response;\n },\n });\n }\n function validateSession(data: SessionRead): void {\n if (!data || typeof data.access_token !== 'string' || !data.access_token ||\n !Number.isFinite(data.expires_in) || data.expires_in <= 0 ||\n !data.account || !data.principal || typeof data.principal.subject !== 'string' ||\n (data.refresh_token != null && (typeof data.refresh_token !== 'string' || !data.refresh_token))) {\n throw new AuthError('invalid_response', 'Invalid authentication session response');\n }\n }\n function schedule(delay: number) {\n clearTimeout(timer);\n timer = setTimeout(() => { void refresh().catch(() => {}); }, Math.min(2_147_483_647, Math.max(0, delay)));\n // Native/server consumers must be able to terminate without an auth timer keeping Node alive.\n (timer as unknown as { unref?: () => void }).unref?.();\n }\n function install(data: SessionRead) {\n validateSession(data);\n const now = Date.now(), ttl = data.expires_in * 1_000;\n session = {\n data, expiresAt: now + ttl,\n refreshAt: now + ttl - (data.refresh_token ? Math.min(30_000, ttl / 2) : 0),\n refreshExpiresAt: data.refresh_expires_in && data.refresh_expires_in > 0 ? now + data.refresh_expires_in * 1_000 : undefined,\n };\n error = null;\n schedule(session.refreshAt - now);\n }\n async function revoke(token?: string | null) {\n if (token) await deleteSession({ refresh_token: token }, { client: publicClient, signal: AbortSignal.timeout(20_000) });\n }\n\n function refresh(): Promise<void> {\n assertUsable();\n if (flight?.epoch === epoch) return flight.applied;\n const current = session, expected = epoch;\n if (!current) return Promise.reject(new AuthError('session_expired', 'Sign in before refreshing a session'));\n if (!current.data.refresh_token || (current.refreshExpiresAt && Date.now() >= current.refreshExpiresAt)) {\n const expired = new AuthError('session_expired', 'Session expired; sign in again');\n clearSession(); error = expired; publish();\n return Promise.reject(expired);\n }\n const requestAbort = new AbortController();\n const response = refreshSession({ refresh_token: current.data.refresh_token }, {\n client: publicClient, signal: AbortSignal.any([requestAbort.signal, AbortSignal.timeout(20_000)]),\n });\n const running = { epoch: expected, response, applied: Promise.resolve(), abort: requestAbort };\n flight = running;\n running.applied = response.then(data => {\n assertEpoch(expected);\n validateSession(data);\n if (data.principal.subject !== current.data.principal.subject || data.account.id !== current.data.account.id) {\n throw new AuthError('invalid_response', 'Refresh changed the authenticated account');\n }\n install(data);\n }).catch(failure => {\n if (!disposed && expected === epoch) {\n error = toError(failure);\n if ((failure instanceof ApiError && [401, 403].includes(failure.status)) ||\n failure instanceof AuthError || Date.now() >= current.expiresAt) {\n clearSession();\n } else {\n current.refreshAt = Math.min(current.expiresAt, Date.now() + 5_000);\n schedule(current.refreshAt - Date.now());\n }\n }\n throw failure;\n }).finally(() => {\n if (flight === running) flight = undefined;\n if (!disposed && expected === epoch) publish();\n else if (!disposed && !session) publish();\n });\n publish();\n return running.applied;\n }\n\n async function prepare(provider: string | undefined, settings: SsoOptions = {}): Promise<PreparedSso> {\n assertUsable();\n if (provider !== undefined && (!provider.trim() || provider.length > 64)) throw new TypeError('A registered provider alias is required');\n const returnTo = localReturnTo(settings.returnTo ?? '/', redirect.origin);\n const expected = epoch, operation = Symbol();\n pending.add(operation); error = null; publish();\n try {\n const state = randomSecret(), verifier = randomSecret(), codeChallenge = await challenge(verifier);\n assertEpoch(expected);\n const result = await authorization({\n redirect_uri: redirect.href, code_challenge: codeChallenge, state, provider, prompt: settings.prompt,\n }, { client: publicClient, signal: AbortSignal.any([abort.signal, AbortSignal.timeout(20_000)]) });\n assertEpoch(expected);\n const url = new URL(result.authorizationUrl);\n const suffix = '/protocol/openid-connect/auth';\n if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.hash || !url.pathname.endsWith(suffix) ||\n ['state', 'code_challenge', 'code_challenge_method', 'redirect_uri', 'response_type'].some(key => url.searchParams.getAll(key).length !== 1) ||\n url.searchParams.get('state') !== state || url.searchParams.get('code_challenge') !== codeChallenge ||\n url.searchParams.get('code_challenge_method') !== 'S256' || url.searchParams.get('redirect_uri') !== redirect.href ||\n url.searchParams.get('response_type') !== 'code' || (provider === 'google' && result.provider !== 'google')) {\n throw new AuthError('invalid_response', 'Invalid SSO authorization response');\n }\n const createdAt = Date.now();\n transactions.save(state, { verifier, createdAt, returnTo, provider: result.provider ?? null, issuer: url.origin + url.pathname.slice(0, -suffix.length) });\n return { url: url.href, provider: result.provider ?? null, expiresAt: createdAt + transactionLifetime };\n } catch (failure) {\n if (expected === epoch && !disposed) error = toError(failure);\n throw failure;\n } finally {\n pending.delete(operation);\n if (expected === epoch && !disposed) publish();\n }\n }\n\n function completeSso(callbackUrl?: string): Promise<CompletedSso> {\n try {\n assertUsable();\n const url = parseCallback(callbackUrl ?? (typeof window === 'undefined' ? '' : window.location.href), redirect);\n const state = url.searchParams.get('state') ?? '';\n // Strict Mode can call the default callback again after the first call\n // removed its OAuth parameters, but before the exchange has completed.\n if (callbackUrl === undefined && !state) {\n const pendingCallback = [...callbacks.values()].find(item => item.cleanUrl === url.href);\n if (pendingCallback) return pendingCallback.promise;\n if (completed?.epoch === epoch && completed.url === url.href) return Promise.resolve(completed.result);\n }\n const existing = callbacks.get(state);\n if (existing) {\n if (existing.url !== url.href) throw new AuthError('invalid_callback', 'A different callback is already using this state');\n return existing.promise;\n }\n const cleanUrl = cleanCallback(url);\n const transaction = transactions.consume(state);\n if (url.searchParams.has('iss') && url.searchParams.get('iss') !== transaction.issuer) throw new AuthError('invalid_issuer', 'Unexpected SSO callback issuer');\n if (url.searchParams.has('error')) throw new AuthError('provider_error', 'Sign-in was refused or cancelled by the identity provider');\n const code = url.searchParams.get('code');\n if (!code || code.length > 4096) throw new AuthError('invalid_callback', 'SSO callback is missing a valid authorization code');\n if (session || flight || callbacks.size || pending.size) clearSession();\n const expected = epoch, operation = Symbol();\n pending.add(operation); error = null; publish();\n const completion = (async () => {\n try {\n const data = await createSessionFromCode({ code, codeVerifier: transaction.verifier, redirectUri: redirect.href, state }, {\n client: publicClient, signal: AbortSignal.timeout(20_000),\n });\n if (disposed || expected !== epoch) { await revoke(data.refresh_token).catch(() => {}); throw changed(); }\n validateSession(data);\n if (transaction.provider && data.principal.provider !== transaction.provider) {\n await revoke(data.refresh_token).catch(() => {});\n throw new AuthError('provider_mismatch', 'Sign-in completed with a different identity provider');\n }\n // Anonymous requests made during the callback cannot inherit the new identity.\n clearSession(); install(data);\n const result = { account: data.account, principal: data.principal, returnTo: transaction.returnTo };\n completed = { epoch, url: cleanUrl, result };\n publish();\n return result;\n } catch (failure) {\n if (!disposed && expected === epoch) { error = toError(failure); publish(); }\n throw failure;\n } finally {\n callbacks.delete(state); pending.delete(operation);\n if (!disposed) publish();\n }\n })();\n callbacks.set(state, { url: url.href, cleanUrl, promise: completion });\n return completion;\n } catch (failure) {\n if (!disposed) { error = toError(failure); publish(); }\n return Promise.reject(failure);\n }\n }\n\n return {\n get client() { return client; }, getSnapshot: () => snapshot,\n subscribe(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; },\n prepareInternalSso: settings => prepare('google', settings),\n prepareProviderSso: (alias, settings) => prepare(alias, settings),\n prepareSso: settings => prepare(undefined, settings), completeSso, refresh,\n async logout() {\n assertUsable();\n const old = session, rotating = flight;\n clearSession(); error = null; publish();\n const expected = epoch;\n // Local logout is immediate, even if storage or the revocation request fails.\n let cleanupError: unknown;\n try { transactions.clear(); } catch (failure) { cleanupError = failure; }\n try {\n // A refresh already sent may rotate the token. Revoke its result, not an\n // obsolete token, without ever restoring it to the local session.\n const rotated = await rotating?.response.catch(() => undefined);\n await revoke(rotated?.refresh_token ?? old?.data.refresh_token);\n if (cleanupError) throw cleanupError;\n } catch (failure) {\n if (!disposed && expected === epoch) { error = toError(failure); publish(); }\n throw failure;\n }\n },\n dispose() {\n if (disposed) return;\n flight?.abort.abort();\n clearSession(); disposed = true; error = null; publish(); listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+
2
+ import { si as AccountRead, sn as PrincipalRead } from "./index.js";
3
+ import { i as ClientOptions, r as Client } from "./client.js";
4
+ //#region src/auth/client.d.ts
5
+ interface MozaikAuthOptions {
6
+ baseUrl: string;
7
+ redirectUri: string;
8
+ /** Only PKCE transactions are persisted. Access/refresh tokens stay in memory. */
9
+ transactionStorage?: Storage;
10
+ fetch?: ClientOptions['fetch'];
11
+ }
12
+ interface SsoOptions {
13
+ returnTo?: string;
14
+ prompt?: 'login' | 'none' | 'consent';
15
+ }
16
+ interface PreparedSso {
17
+ url: string;
18
+ provider: string | null;
19
+ expiresAt: number;
20
+ }
21
+ interface CompletedSso {
22
+ account: AccountRead;
23
+ principal: PrincipalRead;
24
+ returnTo: string;
25
+ }
26
+ interface AuthSnapshot {
27
+ status: 'anonymous' | 'authenticating' | 'authenticated' | 'refreshing';
28
+ isAuthenticated: boolean;
29
+ isLoading: boolean;
30
+ account: AccountRead | null;
31
+ principal: PrincipalRead | null;
32
+ error: Error | null;
33
+ /** Stable across refreshes; replaced on login/logout to isolate account caches. */
34
+ client: Client;
35
+ }
36
+ interface MozaikAuth {
37
+ readonly client: Client;
38
+ getSnapshot(): AuthSnapshot;
39
+ subscribe(listener: () => void): () => void;
40
+ prepareInternalSso(options?: SsoOptions): Promise<PreparedSso>;
41
+ prepareProviderSso(alias: string, options?: SsoOptions): Promise<PreparedSso>;
42
+ prepareSso(options?: SsoOptions): Promise<PreparedSso>;
43
+ completeSso(callbackUrl?: string): Promise<CompletedSso>;
44
+ refresh(): Promise<void>;
45
+ logout(): Promise<void>;
46
+ /** Local teardown only. Use logout() to revoke the server session. */
47
+ dispose(): void;
48
+ }
49
+ /** Browser SSO through Mozaik's account-provisioning backend, with no client secret. */
50
+ declare function createMozaikAuth(options: MozaikAuthOptions): MozaikAuth;
51
+ //#endregion
52
+ export { PreparedSso as a, MozaikAuthOptions as i, CompletedSso as n, SsoOptions as o, MozaikAuth as r, createMozaikAuth as s, AuthSnapshot as t };
53
+ //# sourceMappingURL=client2.d.ts.map
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { c as request, i as ClientOptions, o as RequestOptions, r as Client, s as createClient, t as ApiError } from "./chunks/client.js";
3
3
  //#region src/metadata.d.ts
4
4
  export declare const apiVersion = "0.1.0";
5
- export declare const backendRevision = "b73109673f401ca048a1f5dd49156df240186580";
5
+ export declare const backendRevision = "4a542020f6ebb9aa6460023ff6a39cfde89aa82b";
6
6
  export declare const schemaSha256 = "991e12e9668ae517fc606191d8374089ebba40e7d21a5394c0d5b6b5d445b22e";
7
7
  //#endregion
8
8
  export { ApiError, type Client, type ClientOptions, type RequestOptions, createClient, request };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { n as createClient, r as request, t as ApiError } from "./chunks/client.js";
3
3
  //#region src/metadata.ts
4
4
  const apiVersion = "0.1.0";
5
- const backendRevision = "b73109673f401ca048a1f5dd49156df240186580";
5
+ const backendRevision = "4a542020f6ebb9aa6460023ff6a39cfde89aa82b";
6
6
  const schemaSha256 = "991e12e9668ae517fc606191d8374089ebba40e7d21a5394c0d5b6b5d445b22e";
7
7
  //#endregion
8
8
  export { ApiError, apiVersion, backendRevision, createClient, request, schemaSha256 };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/metadata.ts"],"sourcesContent":["// Generated from the deployed OpenAPI snapshot.\nexport const apiVersion = \"0.1.0\";\nexport const backendRevision = \"b73109673f401ca048a1f5dd49156df240186580\";\nexport const schemaSha256 = \"991e12e9668ae517fc606191d8374089ebba40e7d21a5394c0d5b6b5d445b22e\";\n"],"mappings":";;;AACA,MAAa,aAAa;AAC1B,MAAa,kBAAkB;AAC/B,MAAa,eAAe"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/metadata.ts"],"sourcesContent":["// Generated from the deployed OpenAPI snapshot.\nexport const apiVersion = \"0.1.0\";\nexport const backendRevision = \"4a542020f6ebb9aa6460023ff6a39cfde89aa82b\";\nexport const schemaSha256 = \"991e12e9668ae517fc606191d8374089ebba40e7d21a5394c0d5b6b5d445b22e\";\n"],"mappings":";;;AACA,MAAa,aAAa;AAC1B,MAAa,kBAAkB;AAC/B,MAAa,eAAe"}
@@ -0,0 +1,15 @@
1
+
2
+ import { r as MozaikAuth, t as AuthSnapshot } from "../chunks/client2.js";
3
+ import { ReactNode } from "react";
4
+ //#region src/auth/react.d.ts
5
+ export type MozaikAuthContext = AuthSnapshot & Pick<MozaikAuth, 'prepareInternalSso' | 'prepareProviderSso' | 'prepareSso' | 'completeSso' | 'refresh' | 'logout'>;
6
+ export interface MozaikAuthProviderProps {
7
+ /** Create once per application (or per SSR request), outside component renders. */
8
+ auth: MozaikAuth;
9
+ children?: ReactNode;
10
+ }
11
+ /** Provides auth and a TanStack Query cache isolated to the current session. */
12
+ export declare function MozaikAuthProvider({ auth, children }: MozaikAuthProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<MozaikAuthContext | null>>;
13
+ export declare function useMozaikAuth(): MozaikAuthContext;
14
+ //#endregion
15
+ //# sourceMappingURL=sso.d.ts.map
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ //#region src/auth/react.ts
5
+ const Context = createContext(null);
6
+ let cacheSequence = 0;
7
+ /** Provides auth and a TanStack Query cache isolated to the current session. */
8
+ function MozaikAuthProvider({ auth, children }) {
9
+ const snapshot = useSyncExternalStore(auth.subscribe, auth.getSnapshot, auth.getSnapshot);
10
+ const cache = useMemo(() => ({
11
+ queryClient: new QueryClient(),
12
+ key: ++cacheSequence,
13
+ lifetime: 0
14
+ }), [auth, snapshot.client]);
15
+ const { queryClient, key } = cache;
16
+ useEffect(() => {
17
+ const lifetime = ++cache.lifetime;
18
+ return () => {
19
+ queueMicrotask(() => {
20
+ if (cache.lifetime !== lifetime) return;
21
+ queryClient.cancelQueries();
22
+ queryClient.clear();
23
+ });
24
+ };
25
+ }, [cache, queryClient]);
26
+ const value = useMemo(() => ({
27
+ ...snapshot,
28
+ prepareInternalSso: auth.prepareInternalSso,
29
+ prepareProviderSso: auth.prepareProviderSso,
30
+ prepareSso: auth.prepareSso,
31
+ completeSso: auth.completeSso,
32
+ refresh: auth.refresh,
33
+ logout: auth.logout
34
+ }), [auth, snapshot]);
35
+ return createElement(Context.Provider, { value }, createElement(QueryClientProvider, {
36
+ client: queryClient,
37
+ key
38
+ }, children));
39
+ }
40
+ function useMozaikAuth() {
41
+ const value = useContext(Context);
42
+ if (!value) throw new Error("useMozaikAuth must be used inside MozaikAuthProvider");
43
+ return value;
44
+ }
45
+ //#endregion
46
+ export { MozaikAuthProvider, useMozaikAuth };
47
+
48
+ //# sourceMappingURL=sso.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sso.js","names":[],"sources":["../../src/auth/react.ts"],"sourcesContent":["import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore, type ReactNode } from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport type { AuthSnapshot, MozaikAuth } from './client';\n\nexport type MozaikAuthContext = AuthSnapshot & Pick<MozaikAuth,\n 'prepareInternalSso' | 'prepareProviderSso' | 'prepareSso' | 'completeSso' | 'refresh' | 'logout'>;\nconst Context = createContext<MozaikAuthContext | null>(null);\nlet cacheSequence = 0;\n\nexport interface MozaikAuthProviderProps {\n /** Create once per application (or per SSR request), outside component renders. */\n auth: MozaikAuth;\n children?: ReactNode;\n}\n\n/** Provides auth and a TanStack Query cache isolated to the current session. */\nexport function MozaikAuthProvider({ auth, children }: MozaikAuthProviderProps) {\n const snapshot = useSyncExternalStore(auth.subscribe, auth.getSnapshot, auth.getSnapshot);\n // New account/anonymous state gets a new cache before children render. An\n // effect clearing a shared cache would permit one render of the old account.\n const cache = useMemo(() => ({ queryClient: new QueryClient(), key: ++cacheSequence, lifetime: 0 }), [auth, snapshot.client]);\n const { queryClient, key } = cache;\n useEffect(() => {\n const lifetime = ++cache.lifetime;\n return () => {\n // Child observers unsubscribe during cleanup and may schedule GC timers.\n // Clear afterwards; Strict Mode's immediate re-setup keeps its cache.\n queueMicrotask(() => {\n if (cache.lifetime !== lifetime) return;\n void queryClient.cancelQueries(); queryClient.clear();\n });\n };\n }, [cache, queryClient]);\n const value = useMemo<MozaikAuthContext>(() => ({\n ...snapshot,\n prepareInternalSso: auth.prepareInternalSso, prepareProviderSso: auth.prepareProviderSso,\n prepareSso: auth.prepareSso, completeSso: auth.completeSso, refresh: auth.refresh, logout: auth.logout,\n }), [auth, snapshot]);\n // Query observers bind to their initial QueryClient. Remount the session\n // subtree too, so an existing observer cannot retain the previous cache.\n return createElement(Context.Provider, { value }, createElement(QueryClientProvider, { client: queryClient, key }, children));\n}\n\nexport function useMozaikAuth(): MozaikAuthContext {\n const value = useContext(Context);\n if (!value) throw new Error('useMozaikAuth must be used inside MozaikAuthProvider');\n return value;\n}\n"],"mappings":";;;;AAMA,MAAM,UAAU,cAAwC,IAAI;AAC5D,IAAI,gBAAgB;;AASpB,SAAgB,mBAAmB,EAAE,MAAM,YAAqC;CAC9E,MAAM,WAAW,qBAAqB,KAAK,WAAW,KAAK,aAAa,KAAK,WAAW;CAGxF,MAAM,QAAQ,eAAe;EAAE,aAAa,IAAI,YAAY;EAAG,KAAK,EAAE;EAAe,UAAU;CAAE,IAAI,CAAC,MAAM,SAAS,MAAM,CAAC;CAC5H,MAAM,EAAE,aAAa,QAAQ;CAC7B,gBAAgB;EACd,MAAM,WAAW,EAAE,MAAM;EACzB,aAAa;GAGX,qBAAqB;IACnB,IAAI,MAAM,aAAa,UAAU;IACjC,YAAiB,cAAc;IAAG,YAAY,MAAM;GACtD,CAAC;EACH;CACF,GAAG,CAAC,OAAO,WAAW,CAAC;CACvB,MAAM,QAAQ,eAAkC;EAC9C,GAAG;EACH,oBAAoB,KAAK;EAAoB,oBAAoB,KAAK;EACtE,YAAY,KAAK;EAAY,aAAa,KAAK;EAAa,SAAS,KAAK;EAAS,QAAQ,KAAK;CAClG,IAAI,CAAC,MAAM,QAAQ,CAAC;CAGpB,OAAO,cAAc,QAAQ,UAAU,EAAE,MAAM,GAAG,cAAc,qBAAqB;EAAE,QAAQ;EAAa;CAAI,GAAG,QAAQ,CAAC;AAC9H;AAEA,SAAgB,gBAAmC;CACjD,MAAM,QAAQ,WAAW,OAAO;CAChC,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,sDAAsD;CAClF,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learnorama/mozaik-sdk",
3
- "version": "0.1.0-staging.12.1",
3
+ "version": "0.1.0-staging.13.1",
4
4
  "description": "Typed Mozaik API client and optional TanStack Query hooks.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -27,6 +27,14 @@
27
27
  "types": "./dist/chat.d.ts",
28
28
  "import": "./dist/chat.js"
29
29
  },
30
+ "./auth": {
31
+ "types": "./dist/auth.d.ts",
32
+ "import": "./dist/auth.js"
33
+ },
34
+ "./react/sso": {
35
+ "types": "./dist/react/sso.d.ts",
36
+ "import": "./dist/react/sso.js"
37
+ },
30
38
  "./react/chat": {
31
39
  "types": "./dist/react/chat.d.ts",
32
40
  "import": "./dist/react/chat.js"
@@ -65,14 +73,14 @@
65
73
  },
66
74
  "mozaik": {
67
75
  "apiVersion": "0.1.0",
68
- "backendRevision": "b73109673f401ca048a1f5dd49156df240186580",
76
+ "backendRevision": "4a542020f6ebb9aa6460023ff6a39cfde89aa82b",
69
77
  "schemaSha256": "991e12e9668ae517fc606191d8374089ebba40e7d21a5394c0d5b6b5d445b22e",
70
78
  "sdkFingerprintVersion": 2,
71
- "sdkFingerprint": "4f860f2619105c8491e799e77df3aec27ceadaa4837de5f9ed2cd005e33956e0",
79
+ "sdkFingerprint": "85c8b55560bc78f9b426ffeb2d5d7d10d7585c2a3e293c27e0d90e9e14705015",
72
80
  "sdkContent": {
73
- "runtime": "56625842edf623a0de3a6eaf589d4c4156912df5b4557f6601ba2a3249a1a5f2",
74
- "types": "9ff0a9d27b8653fd630c7141169b159668c39a7ee6a5f2b8b25ece6b7234e035",
75
- "package": "04bdc52a7fa2a72729f344d01ff3d954923245debceae159b8afa25cbb5bb563"
81
+ "runtime": "d2eb442e13ac3d0102d763c69a0c2cebceaa5bd58da1e2a3df1382f1cf6929ee",
82
+ "types": "8dfdefdeef650b3491da6a6408d7af210fe9f12d848af5e6d23bdbc094bd1946",
83
+ "package": "99a714f26e86b40c6eabd5248366c7c0efb397f4a6e7cfb7adbca689382e2f11"
76
84
  }
77
85
  }
78
86
  }