@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.
- package/README.md +27 -0
- package/dist/access/index.d.ts +148 -5
- package/dist/access/index.js +2 -5
- package/dist/access/next.d.ts +11 -10
- package/dist/access/next.js +70 -81
- package/dist/access/react.d.ts +59 -12
- package/dist/access/react.js +84 -37
- package/dist/chunk-FBC2NUH7.js +83 -0
- package/dist/chunk-RR5DP3QT.js +261 -0
- package/dist/chunk-RVKIDFXM.js +44 -0
- package/dist/chunk-SMBKTB5V.js +389 -0
- package/dist/index.d.ts +220 -9
- package/dist/index.js +3 -9
- package/dist/internal-rnEuAE21.d.ts +4 -0
- package/dist/next.d.ts +168 -2
- package/dist/next.js +306 -3
- package/package.json +6 -4
- package/dist/access/errors.d.ts +0 -13
- package/dist/access/errors.js +0 -19
- package/dist/access/fetch.d.ts +0 -23
- package/dist/access/fetch.js +0 -43
- package/dist/access/generated/keys.d.ts +0 -20
- package/dist/access/generated/keys.js +0 -78
- package/dist/access/predicates.d.ts +0 -48
- package/dist/access/predicates.js +0 -95
- package/dist/access/snapshot.d.ts +0 -39
- package/dist/access/snapshot.js +0 -92
- package/dist/cookies.d.ts +0 -11
- package/dist/cookies.js +0 -42
- package/dist/device-cookie.d.ts +0 -18
- package/dist/device-cookie.js +0 -22
- package/dist/forwarding.d.ts +0 -2
- package/dist/forwarding.js +0 -32
- package/dist/handoff-errors.d.ts +0 -13
- package/dist/handoff-errors.js +0 -65
- package/dist/handoff.d.ts +0 -55
- package/dist/handoff.js +0 -195
- package/dist/internal.d.ts +0 -16
- package/dist/internal.js +0 -27
- package/dist/return-path.d.ts +0 -6
- package/dist/return-path.js +0 -33
- package/dist/session-gate.d.ts +0 -26
- package/dist/session-gate.js +0 -33
- package/dist/session-refresh.d.ts +0 -24
- package/dist/session-refresh.js +0 -62
- package/dist/token.d.ts +0 -51
- package/dist/token.js +0 -104
package/dist/next.d.ts
CHANGED
|
@@ -1,2 +1,168 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"description": "
|
|
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 &&
|
|
74
|
+
"build": "pnpm clean && tsup",
|
|
73
75
|
"lint": "tsc -p tsconfig.json --noEmit",
|
|
74
76
|
"test": "vitest",
|
|
75
77
|
"test:run": "vitest run",
|
package/dist/access/errors.d.ts
DELETED
|
@@ -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;
|
package/dist/access/errors.js
DELETED
|
@@ -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
|
-
}
|
package/dist/access/fetch.d.ts
DELETED
|
@@ -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>;
|
package/dist/access/fetch.js
DELETED
|
@@ -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;
|