@c1m-public/platform-auth 0.5.0 → 0.7.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.
Files changed (47) hide show
  1. package/README.md +27 -0
  2. package/dist/access/index.d.ts +148 -5
  3. package/dist/access/index.js +2 -5
  4. package/dist/access/next.d.ts +11 -10
  5. package/dist/access/next.js +70 -81
  6. package/dist/access/react.d.ts +59 -12
  7. package/dist/access/react.js +84 -37
  8. package/dist/chunk-FBC2NUH7.js +83 -0
  9. package/dist/chunk-RR5DP3QT.js +261 -0
  10. package/dist/chunk-RVKIDFXM.js +44 -0
  11. package/dist/chunk-SMBKTB5V.js +389 -0
  12. package/dist/index.d.ts +220 -9
  13. package/dist/index.js +3 -9
  14. package/dist/internal-rnEuAE21.d.ts +4 -0
  15. package/dist/next.d.ts +168 -2
  16. package/dist/next.js +306 -3
  17. package/package.json +6 -4
  18. package/dist/access/errors.d.ts +0 -13
  19. package/dist/access/errors.js +0 -19
  20. package/dist/access/fetch.d.ts +0 -23
  21. package/dist/access/fetch.js +0 -43
  22. package/dist/access/generated/keys.d.ts +0 -20
  23. package/dist/access/generated/keys.js +0 -78
  24. package/dist/access/predicates.d.ts +0 -48
  25. package/dist/access/predicates.js +0 -95
  26. package/dist/access/snapshot.d.ts +0 -39
  27. package/dist/access/snapshot.js +0 -92
  28. package/dist/cookies.d.ts +0 -11
  29. package/dist/cookies.js +0 -42
  30. package/dist/device-cookie.d.ts +0 -18
  31. package/dist/device-cookie.js +0 -22
  32. package/dist/forwarding.d.ts +0 -2
  33. package/dist/forwarding.js +0 -32
  34. package/dist/handoff-errors.d.ts +0 -13
  35. package/dist/handoff-errors.js +0 -65
  36. package/dist/handoff.d.ts +0 -55
  37. package/dist/handoff.js +0 -195
  38. package/dist/internal.d.ts +0 -16
  39. package/dist/internal.js +0 -27
  40. package/dist/return-path.d.ts +0 -6
  41. package/dist/return-path.js +0 -33
  42. package/dist/session-gate.d.ts +0 -26
  43. package/dist/session-gate.js +0 -33
  44. package/dist/session-refresh.d.ts +0 -24
  45. package/dist/session-refresh.js +0 -62
  46. package/dist/token.d.ts +0 -51
  47. package/dist/token.js +0 -104
package/dist/next.d.ts CHANGED
@@ -1,2 +1,168 @@
1
- import "server-only";
2
- export * from "./index.js";
1
+ import { SessionData, CookieOptions } from './index.js';
2
+ export { AUTH_ERROR_MESSAGES, AuthResponse, DeviceIdentity, DeviceIdentityConfig, DeviceIdentityStore, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, HandoffSource, HandoffSourceConfig, HandoffSourceTarget, HandoffTarget, HandoffTargetConfig, RefreshedSession, SSO_ERROR_MESSAGES, SessionGateDecision, SessionGateInput, SessionRefresher, SessionRefresherConfig, SessionUser, TokenPayload, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds } from './index.js';
3
+ export { applyForwardedClientHeaders, forwardedRequestHeaders } from '@c1m-public/transport';
4
+ import { R as Resolvable } from './internal-rnEuAE21.js';
5
+
6
+ /**
7
+ * The error vocabulary every first-party platform surface uses at its edge.
8
+ * Platform Admin, SaaSDash, the Affiliate Portal and the Knowledge Library
9
+ * each carried a private copy of these classes; they are the same four
10
+ * everywhere because the backend's 401 codes are the same everywhere.
11
+ */
12
+ declare class SessionExpiredError extends Error {
13
+ constructor();
14
+ }
15
+ /** 401 `recent_authentication_required`: the session is valid but too old for this action. */
16
+ declare class RecentAuthenticationRequiredError extends Error {
17
+ constructor();
18
+ }
19
+ /** 401 `sso_reauthentication_required`: the identity provider must be revisited. */
20
+ declare class SSOReauthenticationRequiredError extends Error {
21
+ constructor();
22
+ }
23
+ /** Any other failed platform call, carrying what the backend said so a log line can be matched. */
24
+ declare class PlatformServerFetchError extends Error {
25
+ readonly path: string;
26
+ readonly status?: number | undefined;
27
+ readonly code?: string | undefined;
28
+ readonly requestId?: string | undefined;
29
+ constructor(message: string, path: string, status?: number | undefined, code?: string | undefined, requestId?: string | undefined);
30
+ }
31
+ /** Structural checks: correct across duplicate package instances and transformed bundles. */
32
+ declare const isSessionExpiredError: (error: unknown) => error is SessionExpiredError;
33
+ declare const isRecentAuthenticationRequiredError: (error: unknown) => error is RecentAuthenticationRequiredError;
34
+ declare const isSSOReauthenticationRequiredError: (error: unknown) => error is SSOReauthenticationRequiredError;
35
+ declare const isPlatformServerFetchError: (error: unknown) => error is PlatformServerFetchError;
36
+ /** Map a 401 body's `code` onto the session error it means. */
37
+ declare function authenticationErrorFor(code: unknown): SessionExpiredError | RecentAuthenticationRequiredError | SSOReauthenticationRequiredError;
38
+ /**
39
+ * Translate a `@c1m-public/control-client` failure into this vocabulary. The
40
+ * backend's code and request id are carried through: dropping them once cost
41
+ * a real diagnosis, where the operator saw only a Next.js digest while the code
42
+ * said which call broke and the request id matched the server log line.
43
+ */
44
+ declare function mapControlError(error: unknown): unknown;
45
+ /** Run a control-client call and rethrow any failure in the platform vocabulary. */
46
+ declare function controlRequest<T>(run: () => Promise<T>): Promise<T>;
47
+
48
+ /** Forwarded client headers for a backend call made from a request context. */
49
+ declare function buildForwardedRequestHeaders(initHeaders?: HeadersInit): Promise<Headers>;
50
+ interface PlatformFetchTimeouts {
51
+ /** GET/HEAD/OPTIONS (default 10000). */
52
+ read?: number;
53
+ /** Everything else (default 30000). */
54
+ write?: number;
55
+ }
56
+ interface PlatformFetchConfig {
57
+ /** Backend origin, no trailing path. */
58
+ apiUrl: Resolvable<string>;
59
+ /** The live bearer for the current request, or null when signed out. */
60
+ getAccessToken: () => Promise<string | null | undefined> | string | null | undefined;
61
+ /** Defaults to forwarding the incoming request's client headers. */
62
+ forwardHeaders?: (init?: HeadersInit) => Promise<Headers> | Headers;
63
+ timeoutMs?: number | PlatformFetchTimeouts;
64
+ fetch?: typeof fetch;
65
+ }
66
+ /**
67
+ * The authenticated server-side fetch every first-party platform app makes:
68
+ * bearer from the session, forwarded client headers, `no-store`, a per-method
69
+ * timeout, and one error vocabulary. Two apps carried near-identical copies
70
+ * that had drifted (one returned null on a missing session, one threw); this
71
+ * throws, so a missing session is never mistaken for an empty result.
72
+ */
73
+ declare function createPlatformFetch(config: PlatformFetchConfig): {
74
+ fetchWithAccessToken: (path: string, accessToken: string | null, init?: RequestInit) => Promise<Response>;
75
+ /** Bearer-authenticated raw response; throws 401 when there is no session. */
76
+ fetchAuthed: (path: string, init?: RequestInit) => Promise<Response>;
77
+ /** JSON body, or null for 204/404. */
78
+ fetchAuthedJson<T>(path: string, init?: RequestInit): Promise<T | null>;
79
+ /** JSON body; every non-2xx (404 included) is an error. */
80
+ fetchAuthedJsonStrict<T>(path: string, init?: RequestInit): Promise<T>;
81
+ fetchJsonWithAccessToken<T>(path: string, accessToken: string, init?: RequestInit): Promise<T | null>;
82
+ /** Unauthenticated JSON read (public endpoints), same headers/timeout/error rules. */
83
+ fetchPublicJson<T>(path: string, init?: RequestInit): Promise<T | null>;
84
+ fetchAuthedField<TEnvelope, TValue>(path: string, select: (data: TEnvelope) => TValue | null | undefined, init?: RequestInit): Promise<TValue | null>;
85
+ fetchAuthedItems<T>(path: string, init?: RequestInit): Promise<T[]>;
86
+ };
87
+ type PlatformFetch = ReturnType<typeof createPlatformFetch>;
88
+
89
+ interface SessionCookieNames {
90
+ session: string;
91
+ refreshToken: string;
92
+ rememberMe: string;
93
+ }
94
+ interface SessionStoreRefreshConfig {
95
+ apiUrl: Resolvable<string>;
96
+ /** e.g. `/api/v1/platform/auth/refresh` */
97
+ refreshPath: string;
98
+ clientType: string;
99
+ timeoutMs?: number;
100
+ fetch?: typeof fetch;
101
+ }
102
+ interface SessionStoreConfig {
103
+ /** The only access-token audience this surface may consume. */
104
+ audience: string;
105
+ cookies: SessionCookieNames;
106
+ /** Persistent-session lifetime cap in seconds (default 7 days); a token's own exp wins when shorter. */
107
+ maxAgeSeconds?: number;
108
+ /**
109
+ * `Secure` on every cookie (default true). false only for plain-http local
110
+ * development, and never honoured for `__Host-` names, which require it.
111
+ */
112
+ secure?: boolean;
113
+ /** Ignored for `__Host-` names, which forbid Domain. */
114
+ cookieDomain?: string;
115
+ /** What `createSession` does when the caller does not say (default true). */
116
+ defaultRememberMe?: boolean;
117
+ /** Refresh-on-read for `getSessionWithRefresh`; omitted means never refresh. */
118
+ refresh?: SessionStoreRefreshConfig;
119
+ decodeToken?: (token: string) => SessionData | null;
120
+ /**
121
+ * Awaited before any cookie read. Pass `connection` from `next/server` so
122
+ * Cache Components treat session state as request-time, never prerendered.
123
+ */
124
+ requestBoundary?: () => Promise<void>;
125
+ /** Called when a cookie read/write fails outside a request (prerender); default swallows. */
126
+ onError?: (error: unknown, operation: string) => void;
127
+ }
128
+ /**
129
+ * Session cookies for a first-party Next.js platform surface, written once.
130
+ *
131
+ * Every consumer had reimplemented this — Platform Admin, SaaSDash (via
132
+ * shared-auth), the Affiliate Portal and the Knowledge Library — around the
133
+ * same primitives, with the same three cookies and the same rules:
134
+ *
135
+ * - rememberMe=false → no Max-Age, so the cookies die with the browser; true →
136
+ * the configured cap, shortened to the token's own expiry.
137
+ * - The remember choice is preserved across refreshes; a session login never
138
+ * silently becomes a persistent one.
139
+ * - Delete with the SAME attributes as set. A `__Host-` cookie is only cleared
140
+ * by an expiring Set-Cookie that itself carries Secure; a Secure-less delete
141
+ * is silently ignored and the session survives logout.
142
+ * - A token with the wrong audience is never stored and never read as a
143
+ * session, so one host's cookie can never impersonate another surface.
144
+ */
145
+ declare function createSessionStore(config: SessionStoreConfig): {
146
+ audience: string;
147
+ cookieNames: SessionCookieNames;
148
+ cookieOptions: (rememberMe: boolean, token?: string) => CookieOptions & {
149
+ httpOnly: true;
150
+ sameSite: "lax";
151
+ path: "/";
152
+ };
153
+ createSession: (accessToken: string, refreshToken?: string, rememberMe?: boolean) => Promise<void>;
154
+ writeSessionCookies: (response: Response, accessToken: string, refreshToken?: string, rememberMe?: boolean) => Response;
155
+ getAccessToken: () => Promise<string | null>;
156
+ getRefreshToken: () => Promise<string | null>;
157
+ getSession: () => Promise<SessionData | null>;
158
+ getSessionWithRefresh: () => Promise<SessionData | null>;
159
+ isRememberMe: () => Promise<boolean>;
160
+ updateAccessToken: (accessToken: string, refreshToken?: string) => Promise<void>;
161
+ destroySession: () => Promise<void>;
162
+ clearSessionCookies: (response: Response) => Response;
163
+ requireSession: () => Promise<SessionData>;
164
+ isAuthenticated(): Promise<boolean>;
165
+ };
166
+ type SessionStore = ReturnType<typeof createSessionStore>;
167
+
168
+ export { CookieOptions, type PlatformFetch, type PlatformFetchConfig, type PlatformFetchTimeouts, PlatformServerFetchError, RecentAuthenticationRequiredError, SSOReauthenticationRequiredError, type SessionCookieNames, SessionData, SessionExpiredError, type SessionStore, type SessionStoreConfig, type SessionStoreRefreshConfig, authenticationErrorFor, buildForwardedRequestHeaders, controlRequest, createPlatformFetch, createSessionStore, isPlatformServerFetchError, isRecentAuthenticationRequiredError, isSSOReauthenticationRequiredError, isSessionExpiredError, mapControlError };
package/dist/next.js CHANGED
@@ -1,3 +1,306 @@
1
- // Server-only entry: root API plus the bundler guard against client bundles.
2
- import "server-only";
3
- export * from "./index.js";
1
+ import { createSessionRefresher, setCookie, deleteCookie } from './chunk-SMBKTB5V.js';
2
+ export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, deleteCookie, evaluateSessionGate, handoffErrorMessage, hasSessionShape, isAuthErrorCode, isHandoffErrorCode, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath } from './chunk-SMBKTB5V.js';
3
+ import { decodeDefaultSession, tokenMaxAgeSeconds, hasTokenAudience, isTokenExpiringSoon, isTokenExpired } from './chunk-FBC2NUH7.js';
4
+ export { decodeBase64Url, decodeDefaultSession, decodeTokenPayload, hasTokenAudience, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, tokenMaxAgeSeconds } from './chunk-FBC2NUH7.js';
5
+ import { applyForwardedClientHeaders, resolveValue } from './chunk-RVKIDFXM.js';
6
+ export { applyForwardedClientHeaders, forwardedRequestHeaders } from './chunk-RVKIDFXM.js';
7
+ import 'server-only';
8
+ import { headers, cookies } from 'next/headers';
9
+
10
+ // src/next/errors.ts
11
+ var SessionExpiredError = class extends Error {
12
+ constructor() {
13
+ super("SESSION_EXPIRED");
14
+ this.name = "SessionExpiredError";
15
+ }
16
+ };
17
+ var RecentAuthenticationRequiredError = class extends Error {
18
+ constructor() {
19
+ super("Sign in again before continuing.");
20
+ this.name = "RecentAuthenticationRequiredError";
21
+ }
22
+ };
23
+ var SSOReauthenticationRequiredError = class extends Error {
24
+ constructor() {
25
+ super("SSO_REAUTHENTICATION_REQUIRED");
26
+ this.name = "SSOReauthenticationRequiredError";
27
+ }
28
+ };
29
+ var PlatformServerFetchError = class extends Error {
30
+ constructor(message, path, status, code, requestId) {
31
+ super(message);
32
+ this.path = path;
33
+ this.status = status;
34
+ this.code = code;
35
+ this.requestId = requestId;
36
+ this.name = "PlatformServerFetchError";
37
+ }
38
+ path;
39
+ status;
40
+ code;
41
+ requestId;
42
+ };
43
+ var named = (error, name) => typeof error === "object" && error !== null && error.name === name;
44
+ var isSessionExpiredError = (error) => named(error, "SessionExpiredError");
45
+ var isRecentAuthenticationRequiredError = (error) => named(error, "RecentAuthenticationRequiredError");
46
+ var isSSOReauthenticationRequiredError = (error) => named(error, "SSOReauthenticationRequiredError");
47
+ var isPlatformServerFetchError = (error) => named(error, "PlatformServerFetchError");
48
+ function authenticationErrorFor(code) {
49
+ if (code === "sso_reauthentication_required") return new SSOReauthenticationRequiredError();
50
+ if (code === "recent_authentication_required") return new RecentAuthenticationRequiredError();
51
+ return new SessionExpiredError();
52
+ }
53
+ function isControlAPIErrorLike(error) {
54
+ return typeof error === "object" && error !== null && error.name === "ControlAPIError" && typeof error.status === "number" && typeof error.code === "string" && typeof error.requestId === "string";
55
+ }
56
+ function mapControlError(error) {
57
+ if (!isControlAPIErrorLike(error)) return error;
58
+ if (error.status === 401) return authenticationErrorFor(error.code);
59
+ return new PlatformServerFetchError(error.message, "control API", error.status, error.code, error.requestId);
60
+ }
61
+ async function controlRequest(run) {
62
+ try {
63
+ return await run();
64
+ } catch (error) {
65
+ throw mapControlError(error);
66
+ }
67
+ }
68
+ async function buildForwardedRequestHeaders(initHeaders) {
69
+ const headers$1 = new Headers(initHeaders);
70
+ try {
71
+ applyForwardedClientHeaders(await headers(), headers$1);
72
+ } catch {
73
+ }
74
+ return headers$1;
75
+ }
76
+ var READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
77
+ function timeoutFor(init, timeouts) {
78
+ const method = (init?.method ?? "GET").toUpperCase();
79
+ if (typeof timeouts === "number") return timeouts;
80
+ return READ_METHODS.has(method) ? timeouts?.read ?? 1e4 : timeouts?.write ?? 3e4;
81
+ }
82
+ function isTimeout(error) {
83
+ return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
84
+ }
85
+ function createPlatformFetch(config) {
86
+ const doFetch = config.fetch ?? globalThis.fetch;
87
+ const forward = config.forwardHeaders ?? buildForwardedRequestHeaders;
88
+ async function fetchWithAccessToken(path, accessToken, init) {
89
+ try {
90
+ const headers = await forward(init?.headers);
91
+ if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`);
92
+ const deadline = AbortSignal.timeout(timeoutFor(init, config.timeoutMs));
93
+ return await doFetch(`${resolveValue(config.apiUrl)}${path}`, {
94
+ ...init,
95
+ headers,
96
+ cache: init?.cache ?? "no-store",
97
+ signal: init?.signal ? AbortSignal.any([init.signal, deadline]) : deadline
98
+ });
99
+ } catch (error) {
100
+ if (error instanceof PlatformServerFetchError) throw error;
101
+ if (isTimeout(error)) throw new PlatformServerFetchError("Platform service timed out", path, 504, "upstream_timeout");
102
+ throw new PlatformServerFetchError("Platform service is unavailable", path);
103
+ }
104
+ }
105
+ async function responseError(response, path) {
106
+ const body = await response.clone().json().catch(() => null);
107
+ if (response.status === 401) return authenticationErrorFor(body?.code);
108
+ const message = body?.message || body?.code || "Platform request failed";
109
+ return new PlatformServerFetchError(message, path, response.status, body?.code, body?.request_id);
110
+ }
111
+ async function parseJson(response, path) {
112
+ try {
113
+ return await response.json();
114
+ } catch {
115
+ throw new PlatformServerFetchError("Platform response was not valid JSON", path, response.status);
116
+ }
117
+ }
118
+ async function jsonOrNull(response, path) {
119
+ if (response.status === 404 || response.status === 204) return null;
120
+ if (!response.ok) throw await responseError(response, path);
121
+ return parseJson(response, path);
122
+ }
123
+ async function fetchAuthed(path, init) {
124
+ const token = await config.getAccessToken();
125
+ if (!token) throw new PlatformServerFetchError("Authentication is required", path, 401);
126
+ return fetchWithAccessToken(path, token, init);
127
+ }
128
+ return {
129
+ fetchWithAccessToken,
130
+ /** Bearer-authenticated raw response; throws 401 when there is no session. */
131
+ fetchAuthed,
132
+ /** JSON body, or null for 204/404. */
133
+ async fetchAuthedJson(path, init) {
134
+ return jsonOrNull(await fetchAuthed(path, init), path);
135
+ },
136
+ /** JSON body; every non-2xx (404 included) is an error. */
137
+ async fetchAuthedJsonStrict(path, init) {
138
+ const response = await fetchAuthed(path, init);
139
+ if (!response.ok) throw await responseError(response, path);
140
+ return parseJson(response, path);
141
+ },
142
+ async fetchJsonWithAccessToken(path, accessToken, init) {
143
+ return jsonOrNull(await fetchWithAccessToken(path, accessToken, init), path);
144
+ },
145
+ /** Unauthenticated JSON read (public endpoints), same headers/timeout/error rules. */
146
+ async fetchPublicJson(path, init) {
147
+ return jsonOrNull(await fetchWithAccessToken(path, null, init), path);
148
+ },
149
+ async fetchAuthedField(path, select, init) {
150
+ const response = await fetchAuthed(path, init);
151
+ const data = await jsonOrNull(response, path);
152
+ return data ? select(data) ?? null : null;
153
+ },
154
+ async fetchAuthedItems(path, init) {
155
+ const response = await fetchAuthed(path, init);
156
+ const data = await jsonOrNull(response, path);
157
+ return data?.items ?? [];
158
+ }
159
+ };
160
+ }
161
+ function createSessionStore(config) {
162
+ const names = config.cookies;
163
+ const hostPrefixed = names.session.startsWith("__Host-") || names.refreshToken.startsWith("__Host-");
164
+ const secure = hostPrefixed || (config.secure ?? true);
165
+ const domain = hostPrefixed ? void 0 : config.cookieDomain;
166
+ const maxAgeCap = config.maxAgeSeconds ?? 7 * 24 * 60 * 60;
167
+ const decode = config.decodeToken ?? decodeDefaultSession;
168
+ const onError = config.onError ?? (() => {
169
+ });
170
+ const refresher = config.refresh ? createSessionRefresher({
171
+ apiUrl: config.refresh.apiUrl,
172
+ refreshPath: config.refresh.refreshPath,
173
+ clientType: config.refresh.clientType,
174
+ audience: config.audience,
175
+ timeoutMs: config.refresh.timeoutMs,
176
+ fetch: config.refresh.fetch
177
+ }) : void 0;
178
+ function cookieOptions(rememberMe, token) {
179
+ const maxAge = token ? tokenMaxAgeSeconds(token, maxAgeCap) : maxAgeCap;
180
+ return {
181
+ httpOnly: true,
182
+ secure,
183
+ sameSite: "lax",
184
+ path: "/",
185
+ ...rememberMe ? { maxAge } : {},
186
+ ...domain ? { domain } : {}
187
+ };
188
+ }
189
+ function assertAudience(token) {
190
+ if (!hasTokenAudience(token, config.audience)) {
191
+ throw new Error(`refusing to store a session token that is not for ${config.audience}`);
192
+ }
193
+ }
194
+ function acceptable(token) {
195
+ return Boolean(token && !isTokenExpired(token) && hasTokenAudience(token, config.audience));
196
+ }
197
+ async function jar(operation) {
198
+ try {
199
+ await config.requestBoundary?.();
200
+ return await cookies();
201
+ } catch (error) {
202
+ onError(error, operation);
203
+ return null;
204
+ }
205
+ }
206
+ function rememberedIn(store) {
207
+ return store?.get(names.rememberMe)?.value === "true";
208
+ }
209
+ async function createSession(accessToken, refreshToken, rememberMe = config.defaultRememberMe ?? true) {
210
+ assertAudience(accessToken);
211
+ const store = await jar("createSession");
212
+ if (!store) throw new Error("Cannot create a session outside a request");
213
+ store.set(names.session, accessToken, cookieOptions(rememberMe, accessToken));
214
+ if (refreshToken) store.set(names.refreshToken, refreshToken, cookieOptions(rememberMe, refreshToken));
215
+ store.set(names.rememberMe, rememberMe ? "true" : "false", cookieOptions(rememberMe));
216
+ }
217
+ function writeSessionCookies(response, accessToken, refreshToken, rememberMe = config.defaultRememberMe ?? true) {
218
+ assertAudience(accessToken);
219
+ setCookie(response, names.session, accessToken, cookieOptions(rememberMe, accessToken));
220
+ if (refreshToken) setCookie(response, names.refreshToken, refreshToken, cookieOptions(rememberMe, refreshToken));
221
+ setCookie(response, names.rememberMe, rememberMe ? "true" : "false", cookieOptions(rememberMe));
222
+ return response;
223
+ }
224
+ async function getAccessToken() {
225
+ const store = await jar("getAccessToken");
226
+ const token = store?.get(names.session)?.value;
227
+ return acceptable(token) ? token : null;
228
+ }
229
+ async function getRefreshToken() {
230
+ const store = await jar("getRefreshToken");
231
+ return store?.get(names.refreshToken)?.value || null;
232
+ }
233
+ async function getSession() {
234
+ const token = await getAccessToken();
235
+ return token ? decode(token) : null;
236
+ }
237
+ async function isRememberMe() {
238
+ return rememberedIn(await jar("isRememberMe"));
239
+ }
240
+ async function updateAccessToken(accessToken, refreshToken) {
241
+ assertAudience(accessToken);
242
+ const store = await jar("updateAccessToken");
243
+ if (!store) throw new Error("Cannot update a session outside a request");
244
+ const rememberMe = rememberedIn(store);
245
+ store.set(names.session, accessToken, cookieOptions(rememberMe, accessToken));
246
+ if (refreshToken) store.set(names.refreshToken, refreshToken, cookieOptions(rememberMe, refreshToken));
247
+ }
248
+ async function getSessionWithRefresh() {
249
+ const store = await jar("getSessionWithRefresh");
250
+ const token = store?.get(names.session)?.value;
251
+ if (!token || !hasTokenAudience(token, config.audience)) return null;
252
+ if (!isTokenExpiringSoon(token)) return decode(token);
253
+ const refreshToken = store?.get(names.refreshToken)?.value;
254
+ if (refresher && refreshToken) {
255
+ const refreshed = await refresher.refresh(refreshToken);
256
+ if (refreshed) {
257
+ await updateAccessToken(refreshed.accessToken, refreshed.refreshToken);
258
+ return decode(refreshed.accessToken);
259
+ }
260
+ }
261
+ return isTokenExpired(token) ? null : decode(token);
262
+ }
263
+ function clearOptions() {
264
+ return { path: "/", secure };
265
+ }
266
+ async function destroySession() {
267
+ const store = await jar("destroySession");
268
+ if (!store) return;
269
+ try {
270
+ const options = cookieOptions(false);
271
+ for (const name of [names.session, names.refreshToken, names.rememberMe]) store.delete({ name, ...options });
272
+ } catch (error) {
273
+ onError(error, "destroySession");
274
+ }
275
+ }
276
+ function clearSessionCookies(response) {
277
+ for (const name of [names.session, names.refreshToken, names.rememberMe]) deleteCookie(response, name, clearOptions());
278
+ return response;
279
+ }
280
+ async function requireSession() {
281
+ const session = await getSession();
282
+ if (!session) throw new Error("Unauthorized");
283
+ return session;
284
+ }
285
+ return {
286
+ audience: config.audience,
287
+ cookieNames: names,
288
+ cookieOptions,
289
+ createSession,
290
+ writeSessionCookies,
291
+ getAccessToken,
292
+ getRefreshToken,
293
+ getSession,
294
+ getSessionWithRefresh,
295
+ isRememberMe,
296
+ updateAccessToken,
297
+ destroySession,
298
+ clearSessionCookies,
299
+ requireSession,
300
+ async isAuthenticated() {
301
+ return await getSession() !== null;
302
+ }
303
+ };
304
+ }
305
+
306
+ export { PlatformServerFetchError, RecentAuthenticationRequiredError, SSOReauthenticationRequiredError, SessionExpiredError, authenticationErrorFor, buildForwardedRequestHeaders, controlRequest, createPlatformFetch, createSessionStore, isPlatformServerFetchError, isRecentAuthenticationRequiredError, isSSOReauthenticationRequiredError, isSessionExpiredError, mapControlError };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@c1m-public/platform-auth",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "description": "Server-side session and authorization helpers for C1M platform surfaces: token/session reads, the first-party handoff (PKCE target + source), proxy session gate and coalesced refresh, plus the resolved access snapshot, typed permission keys, predicates, a Next loader and React bindings under ./access",
6
+ "description": "Session and authorization helpers for C1M platform surfaces. The root is runtime-neutral (Web Request/Response): token/session reads, the first-party handoff (PKCE target + source), proxy session gate and coalesced refresh; ./next adds the server-only guard; ./access carries the resolved access snapshot, typed permission keys, predicates, a Next loader and React bindings",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "git+https://github.com/cloud1mediaLtd/mml-js.git",
@@ -53,7 +53,9 @@
53
53
  "react": "19.2.4",
54
54
  "react-dom": "19.2.4",
55
55
  "typescript": "5.9.3",
56
- "vitest": "^4.1.4"
56
+ "vitest": "^4.1.4",
57
+ "tsup": "^8.5.1",
58
+ "@c1m-public/transport": "0.0.0"
57
59
  },
58
60
  "peerDependencies": {
59
61
  "next": ">=15",
@@ -69,7 +71,7 @@
69
71
  },
70
72
  "scripts": {
71
73
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
72
- "build": "pnpm clean && tsc -p tsconfig.build.json",
74
+ "build": "pnpm clean && tsup",
73
75
  "lint": "tsc -p tsconfig.json --noEmit",
74
76
  "test": "vitest",
75
77
  "test:run": "vitest run",
@@ -1,13 +0,0 @@
1
- export type AccessErrorCode = "unauthenticated" | "forbidden" | "unavailable" | "invalid_snapshot";
2
- /**
3
- * Typed failure for loading or validating an access snapshot. `unauthenticated`
4
- * = no/expired session (401); `forbidden` = the backend refused the snapshot
5
- * (403; e.g. wrong audience); `unavailable` = transport/5xx; `invalid_snapshot`
6
- * = the payload did not match the contract.
7
- */
8
- export declare class AccessSnapshotError extends Error {
9
- readonly code: AccessErrorCode;
10
- readonly status: number | undefined;
11
- constructor(code: AccessErrorCode, message: string, status?: number);
12
- }
13
- export declare function isAccessSnapshotError(error: unknown, code?: AccessErrorCode): error is AccessSnapshotError;
@@ -1,19 +0,0 @@
1
- /**
2
- * Typed failure for loading or validating an access snapshot. `unauthenticated`
3
- * = no/expired session (401); `forbidden` = the backend refused the snapshot
4
- * (403; e.g. wrong audience); `unavailable` = transport/5xx; `invalid_snapshot`
5
- * = the payload did not match the contract.
6
- */
7
- export class AccessSnapshotError extends Error {
8
- code;
9
- status;
10
- constructor(code, message, status) {
11
- super(message);
12
- this.name = "AccessSnapshotError";
13
- this.code = code;
14
- this.status = status;
15
- }
16
- }
17
- export function isAccessSnapshotError(error, code) {
18
- return error instanceof AccessSnapshotError && (code === undefined || error.code === code);
19
- }
@@ -1,23 +0,0 @@
1
- import { type Resolvable } from "../internal.js";
2
- import { type AccessSnapshot } from "./snapshot.js";
3
- export declare const DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
4
- export interface FetchAccessSnapshotOptions {
5
- /** Backend origin, e.g. https://api.example.com (no trailing slash needed). */
6
- apiUrl: Resolvable;
7
- /** Bearer access token for the platform surface. */
8
- accessToken: string;
9
- /** Defaults to /api/v1/platform/access. */
10
- endpointPath?: string;
11
- fetchTimeoutMs?: number;
12
- fetchImpl?: typeof fetch;
13
- /** Extra headers. Authorization and Accept are always set here. */
14
- headers?: HeadersInit;
15
- /** Incoming BFF request; its client identity (IP, geo, UA) is forwarded to the backend. */
16
- request?: Request;
17
- }
18
- /**
19
- * Framework-neutral fetch of the resolved access snapshot. Throws
20
- * AccessSnapshotError: 401 → unauthenticated, 403 → forbidden, other non-2xx or
21
- * network failure → unavailable, malformed body → invalid_snapshot.
22
- */
23
- export declare function fetchAccessSnapshot(opts: FetchAccessSnapshotOptions): Promise<AccessSnapshot>;
@@ -1,43 +0,0 @@
1
- import { forwardedRequestHeaders } from "../forwarding.js";
2
- import { resolveValue, timedFetch } from "../internal.js";
3
- import { AccessSnapshotError } from "./errors.js";
4
- import { parseAccessSnapshot } from "./snapshot.js";
5
- export const DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
6
- /**
7
- * Framework-neutral fetch of the resolved access snapshot. Throws
8
- * AccessSnapshotError: 401 → unauthenticated, 403 → forbidden, other non-2xx or
9
- * network failure → unavailable, malformed body → invalid_snapshot.
10
- */
11
- export async function fetchAccessSnapshot(opts) {
12
- const url = `${resolveValue(opts.apiUrl).replace(/\/$/, "")}${opts.endpointPath ?? DEFAULT_ACCESS_ENDPOINT_PATH}`;
13
- const headers = opts.request
14
- ? forwardedRequestHeaders(opts.request, opts.headers)
15
- : new Headers(opts.headers);
16
- headers.set("Authorization", `Bearer ${opts.accessToken}`);
17
- headers.set("Accept", "application/json");
18
- let response;
19
- try {
20
- response = await timedFetch(url, {
21
- init: { method: "GET", headers },
22
- timeoutMs: opts.fetchTimeoutMs,
23
- fetchImpl: opts.fetchImpl,
24
- });
25
- }
26
- catch (error) {
27
- throw new AccessSnapshotError("unavailable", `access snapshot request failed: ${error.message}`);
28
- }
29
- if (response.status === 401)
30
- throw new AccessSnapshotError("unauthenticated", "session expired", 401);
31
- if (response.status === 403)
32
- throw new AccessSnapshotError("forbidden", "access snapshot refused", 403);
33
- if (!response.ok)
34
- throw new AccessSnapshotError("unavailable", `access snapshot returned ${response.status}`, response.status);
35
- let body;
36
- try {
37
- body = await response.json();
38
- }
39
- catch {
40
- throw new AccessSnapshotError("invalid_snapshot", "access snapshot body is not JSON", response.status);
41
- }
42
- return parseAccessSnapshot(body);
43
- }
@@ -1,20 +0,0 @@
1
- /** Platform-operator permission keys. */
2
- export declare const PLATFORM_PERMISSIONS: readonly ["platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.channels.manage", "platform.channels.view", "platform.config.manage", "platform.docs.manage", "platform.docs.view", "platform.observability.manage", "platform.observability.view", "platform.ops.manage", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
3
- export type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
4
- /** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
5
- export declare const DASH_PERMISSIONS: readonly ["dash.apps.manage", "dash.billing.manage", "dash.channels.manage", "dash.customers.manage", "dash.domains.manage", "dash.members.manage", "dash.ownership.transfer", "dash.plans.manage", "dash.publish", "dash.settings.manage", "dash.workspaces.admin", "dash.workspaces.manage"];
6
- export type DashPermissionKey = (typeof DASH_PERMISSIONS)[number];
7
- /** Membership-mirrored dash role bundles (grant keys at dash scope). */
8
- export declare const DASH_ROLES: readonly ["dash.admin", "dash.owner"];
9
- export type DashRole = (typeof DASH_ROLES)[number];
10
- /** Lead-broker workload permission keys. */
11
- export declare const LEADBROKER_PERMISSIONS: readonly ["leadbroker.finance.manage", "leadbroker.leads.dispatch", "leadbroker.leads.review", "leadbroker.leads.view", "leadbroker.policy.manage"];
12
- export type LeadBrokerPermissionKey = (typeof LEADBROKER_PERMISSIONS)[number];
13
- /** Workspace-staff permission keys. */
14
- export declare const WORKSPACE_PERMISSIONS: readonly ["workspace.channels.manage", "workspace.channels.secrets", "workspace.channels.view", "workspace.crm.manage", "workspace.crm.view", "workspace.customers.manage", "workspace.customers.view", "workspace.orders.manage", "workspace.orders.view", "workspace.payments.record", "workspace.site.edit", "workspace.site.view", "workspace.staff.manage"];
15
- export type WorkspacePermissionKey = (typeof WORKSPACE_PERMISSIONS)[number];
16
- /** Assignable workspace-staff role bundles (grant keys at workspace scope). */
17
- export declare const WORKSPACE_ROLES: readonly ["workspace.editor", "workspace.frontdesk", "workspace.manager"];
18
- export type WorkspaceRole = (typeof WORKSPACE_ROLES)[number];
19
- /** Every permission key the backend can grant, across all scopes. */
20
- export type PermissionKey = PlatformPermissionKey | DashPermissionKey | LeadBrokerPermissionKey | WorkspacePermissionKey;