@c1m-public/platform-auth 0.21.0 → 0.22.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 +4 -2
- package/dist/{index-BxyVV6Wt.d.ts → index-Bc_E5HWA.d.ts} +103 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +84 -4
- package/dist/next.d.ts +2 -2
- package/dist/next.js +84 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,11 +28,13 @@ sessions never needs them.
|
|
|
28
28
|
| `readCookie`, `serializeCookie`, `setCookie`, `deleteCookie` | Cookie helpers over Web `Request`/`Response`. `__Host-` names get `Secure` automatically. |
|
|
29
29
|
| `createDeviceIdentity` | The `X-Client-Device-ID` cookie the backend requires to create a session. |
|
|
30
30
|
| `applyForwardedClientHeaders`, `forwardedRequestHeaders` | Copy client identity (IP, geo, UA) onto a backend call. Never used for auth. |
|
|
31
|
-
| `safeInternalPath` | Strict same-origin return-path sanitiser (rejects `//`, `://`, encoded slashes, control chars, anything that does not round-trip). |
|
|
31
|
+
| `safeInternalPath(raw, { stripParams? })` | Strict same-origin return-path sanitiser (rejects `//`, `://`, encoded slashes, control chars, anything that does not round-trip). `stripParams` drops named query params (e.g. a one-shot `auth`/`from` flag) from the accepted value so it doesn't survive into the redirect target. |
|
|
32
32
|
| `createSessionRefresher` | Coalesced refresh-token exchange: one backend call per token per window. |
|
|
33
|
-
| `evaluateSessionGate`, `hasSessionShape` | Pure proxy decision — `pass` / `redirect_home` / `anonymous_public` / `unauthorized_api` / `redirect_login`. The proxy owns response construction. |
|
|
33
|
+
| `evaluateSessionGate`, `hasSessionShape` | Pure proxy decision — `pass` / `redirect_home` / `anonymous_public` / `unauthorized_api` / `redirect_login`. The proxy owns response construction. `eagerReauthBufferSeconds` demotes a token expiring within that window to not-live, so a failed refresh rejects the session instead of passing through on the still-valid token. |
|
|
34
34
|
| `createHandoffTarget`, `createHandoffSource`, `brokerPaths`, `sourceStartPath` | Browser halves of the first-party session handoff (`/api/v1/platform/auth/handoffs/{app}`): PKCE S256 + state, one-time code, host-local cookies, no tokens in URLs. |
|
|
35
35
|
| `HANDOFF_ERROR_MESSAGES`, `SSO_ERROR_MESSAGES`, `AUTH_ERROR_MESSAGES`, `authErrorMessage(code, overrides?)`, `isAuthErrorCode`, `handoffErrorMessage`, `isHandoffErrorCode` | One sign-in error vocabulary — handoff, admission and SSO codes. Surfaces pass their own copy as `overrides` instead of forking the map. |
|
|
36
|
+
| `ADMIN_HOST_ROOT`, `isAdminRootHost`, `adminSlugFromHost`, `recoveryAdminOrigin`, `brandedAdminHost` | The `<slug>.saasdash.app` / `admin.<domain>` admin-host rules, mirroring the backend's `internal/platform/domainmanagement/admin_dash_hostnames.go`. |
|
|
37
|
+
| `SIGN_IN_HINT_COOKIE`, `signInHintDomain`, `signInHintCookie`, `hasSignInHint` | The non-secret "signed in" display hint an admin host sets on its own domain so the Dash's public site can show "Open your workspace" instead of "Sign in". `signInHintDomain`/`signInHintCookie` are the pure writer half; `hasSignInHint` the reader. The hint carries no identity and must never gate access. |
|
|
36
38
|
|
|
37
39
|
```ts
|
|
38
40
|
// proxy.ts
|
|
@@ -50,12 +50,22 @@ interface AuthResponse {
|
|
|
50
50
|
}
|
|
51
51
|
declare function parseAuthResponse(value: unknown): AuthResponse | null;
|
|
52
52
|
|
|
53
|
+
interface SafeInternalPathOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Query params to drop from the accepted value before returning it — e.g. a
|
|
56
|
+
* one-shot `auth`/`from` flag that must not survive into the redirect
|
|
57
|
+
* target and reopen a login modal. Omitted or empty: the value comes back
|
|
58
|
+
* verbatim, as before this option existed.
|
|
59
|
+
*/
|
|
60
|
+
stripParams?: readonly string[];
|
|
61
|
+
}
|
|
53
62
|
/**
|
|
54
63
|
* Strict same-origin return path: must start with `/`, not `//` or contain `://`,
|
|
55
64
|
* no encoded slashes or control chars, and round-trip URL parsing unchanged.
|
|
56
|
-
* Returns the value verbatim (query/hash kept) or null
|
|
65
|
+
* Returns the value verbatim (query/hash kept) or null, unless `stripParams`
|
|
66
|
+
* asks for named query params to be dropped from the accepted value.
|
|
57
67
|
*/
|
|
58
|
-
declare function safeInternalPath(raw: string | null | undefined): string | null;
|
|
68
|
+
declare function safeInternalPath(raw: string | null | undefined, options?: SafeInternalPathOptions): string | null;
|
|
59
69
|
|
|
60
70
|
declare function applyForwardedClientHeaders(incoming: Headers, headers?: Headers): Headers;
|
|
61
71
|
declare function forwardedRequestHeaders(request: Request, init?: HeadersInit): Headers;
|
|
@@ -269,7 +279,97 @@ interface SessionGateInput {
|
|
|
269
279
|
isLoginPath: boolean;
|
|
270
280
|
isApiPath: boolean;
|
|
271
281
|
expiringSoonBufferSeconds?: number;
|
|
282
|
+
/**
|
|
283
|
+
* Demotes a token expiring within this many seconds to not-live, so refresh
|
|
284
|
+
* becomes mandatory rather than best-effort: a failed exchange rejects the
|
|
285
|
+
* session (`redirect_login` / `anonymous_public` / `unauthorized_api`)
|
|
286
|
+
* instead of passing through on the still-valid access token. Unset, a
|
|
287
|
+
* live token always passes even when its pre-emptive refresh fails — the
|
|
288
|
+
* default, and what `expiringSoonBufferSeconds` alone gives you. Set this
|
|
289
|
+
* where a surface wants to force reauthentication near expiry rather than
|
|
290
|
+
* ride out the old token (saasdash-app's proxy did this by hand, passing
|
|
291
|
+
* `accessToken: undefined` once inside its own buffer).
|
|
292
|
+
*/
|
|
293
|
+
eagerReauthBufferSeconds?: number;
|
|
272
294
|
}
|
|
273
295
|
declare function evaluateSessionGate(input: SessionGateInput): Promise<SessionGateDecision>;
|
|
274
296
|
|
|
275
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Root of the shared `<slug>.saasdash.app` admin hosts every Dash gets
|
|
299
|
+
* (`c1m-server` `internal/platform/domainmanagement/admin_dash_hostnames.go`
|
|
300
|
+
* `AdminHostRoot`), alongside its own `admin.<domain>` once it has a managed
|
|
301
|
+
* domain.
|
|
302
|
+
*/
|
|
303
|
+
declare const ADMIN_HOST_ROOT = "saasdash.app";
|
|
304
|
+
/**
|
|
305
|
+
* True for any host under the shared admin root, excluding the bare root
|
|
306
|
+
* itself (`saasdash.app` is the shared app, not an admin host).
|
|
307
|
+
*/
|
|
308
|
+
declare function isAdminRootHost(hostname: string): boolean;
|
|
309
|
+
/**
|
|
310
|
+
* The Dash slug a `<slug>.saasdash.app` admin host is locked to, or null for
|
|
311
|
+
* every other host — including the bare root, a nested label
|
|
312
|
+
* (`a.b.saasdash.app`), and a `www` label, which is a real subdomain here and
|
|
313
|
+
* never stripped: `www.saasdash.app` and `www.<slug>.saasdash.app` are both
|
|
314
|
+
* rejected rather than aliased to `<slug>`.
|
|
315
|
+
*/
|
|
316
|
+
declare function adminSlugFromHost(hostname: string): string | null;
|
|
317
|
+
/**
|
|
318
|
+
* The Dash's platform-owned recovery admin origin
|
|
319
|
+
* (`https://<slug>.saasdash.app`). Unlike `admin.<customer domain>` it cannot
|
|
320
|
+
* be broken by the customer's DNS, so links the platform sends itself
|
|
321
|
+
* (billing and security mail, hosted-checkout returns) use it. Mirrors
|
|
322
|
+
* `DashRecoveryBaseURL`. Returns `""` for an empty slug, exactly as the Go
|
|
323
|
+
* source does.
|
|
324
|
+
*/
|
|
325
|
+
declare function recoveryAdminOrigin(slug: string): string;
|
|
326
|
+
/**
|
|
327
|
+
* The branded admin host for a Dash's own managed domain (`admin.<domain>`).
|
|
328
|
+
* Mirrors the second entry `DashAdminHostnames` returns once a Dash has a
|
|
329
|
+
* managed domain.
|
|
330
|
+
*/
|
|
331
|
+
declare function brandedAdminHost(domain: string): string;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Set by a Dash's admin host on the Dash's own domain while someone is signed
|
|
335
|
+
* in there. A display hint only ("Open your workspace" instead of "Sign
|
|
336
|
+
* in"): it carries no identity and must never gate access.
|
|
337
|
+
*/
|
|
338
|
+
declare const SIGN_IN_HINT_COOKIE = "c1m_signed_in";
|
|
339
|
+
interface SignInHintDomainOptions {
|
|
340
|
+
/**
|
|
341
|
+
* Extra hosts (and their subdomains) that must never receive the hint's
|
|
342
|
+
* cookie Domain because their siblings are other Dashes — e.g. the shared
|
|
343
|
+
* app's own host. `ADMIN_HOST_ROOT` is always excluded, in addition to
|
|
344
|
+
* these.
|
|
345
|
+
*/
|
|
346
|
+
excludedRoots?: readonly string[];
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The cookie Domain for an admin host's sign-in hint: the host minus its
|
|
350
|
+
* first label (`admin.simplevan.co.uk` → `simplevan.co.uk`). null for the
|
|
351
|
+
* shared admin root (and any `excludedRoots`) or one of their subdomains —
|
|
352
|
+
* their siblings are other Dashes — and for a host with fewer than three
|
|
353
|
+
* labels, which has nothing to strip.
|
|
354
|
+
*/
|
|
355
|
+
declare function signInHintDomain(hostname: string, options?: SignInHintDomainOptions): string | null;
|
|
356
|
+
interface SignInHintCookieAttributes {
|
|
357
|
+
name: string;
|
|
358
|
+
value: string;
|
|
359
|
+
domain: string;
|
|
360
|
+
path: "/";
|
|
361
|
+
sameSite: "lax";
|
|
362
|
+
secure: true;
|
|
363
|
+
httpOnly: false;
|
|
364
|
+
maxAge: number;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The cookie attributes to set (`signedIn: true`) or clear (`signedIn:
|
|
368
|
+
* false`) the hint with. Pure data — the caller applies it to its own
|
|
369
|
+
* response.
|
|
370
|
+
*/
|
|
371
|
+
declare function signInHintCookie(domain: string, signedIn: boolean): SignInHintCookieAttributes;
|
|
372
|
+
/** Reads the hint from a `Cookie` header or `document.cookie` string. */
|
|
373
|
+
declare function hasSignInHint(cookieHeader: string | null | undefined): boolean;
|
|
374
|
+
|
|
375
|
+
export { isTokenExpiringSoon as $, type AsyncResolvable as A, brandedAdminHost as B, type CookieOptions as C, type DeviceIdentity as D, brokerPaths as E, createDeviceIdentity as F, createHandoffSource as G, HANDOFF_ERROR_FALLBACK as H, createHandoffTarget as I, createSessionRefresher as J, decodeBase64Url as K, decodeDefaultSession as L, decodeTokenPayload as M, deleteCookie as N, evaluateSessionGate as O, forwardedRequestHeaders as P, handoffErrorMessage as Q, type Resolvable as R, type SessionData as S, type TokenPayload as T, hasSessionShape as U, hasSignInHint as V, hasTokenAudience as W, isAdminRootHost as X, isAuthErrorCode as Y, isHandoffErrorCode as Z, isTokenExpired as _, ADMIN_HOST_ROOT as a, parseAuthResponse as a0, parseRefreshEnvelope as a1, readCookie as a2, recoveryAdminOrigin as a3, safeInternalPath as a4, serializeCookie as a5, setCookie as a6, signInHintCookie as a7, signInHintDomain as a8, sourceStartPath as a9, tokenMaxAgeSeconds as aa, AUTH_ERROR_MESSAGES as b, type AuthResponse as c, type DeviceIdentityConfig as d, type DeviceIdentityStore as e, HANDOFF_ERROR_MESSAGES as f, type HandoffSource as g, type HandoffSourceConfig as h, type HandoffSourceTarget as i, type HandoffTarget as j, type HandoffTargetConfig as k, type RefreshOutcome as l, type RefreshedSession as m, SIGN_IN_HINT_COOKIE as n, SSO_ERROR_MESSAGES as o, type SafeInternalPathOptions as p, type SessionGateDecision as q, type SessionGateInput as r, type SessionRefresher as s, type SessionRefresherConfig as t, type SessionUser as u, type SignInHintCookieAttributes as v, type SignInHintDomainOptions as w, adminSlugFromHost as x, applyForwardedClientHeaders as y, authErrorMessage as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { a as
|
|
1
|
+
export { a as ADMIN_HOST_ROOT, b as AUTH_ERROR_MESSAGES, c as AuthResponse, C as CookieOptions, D as DeviceIdentity, d as DeviceIdentityConfig, e as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, f as HANDOFF_ERROR_MESSAGES, g as HandoffSource, h as HandoffSourceConfig, i as HandoffSourceTarget, j as HandoffTarget, k as HandoffTargetConfig, l as RefreshOutcome, m as RefreshedSession, n as SIGN_IN_HINT_COOKIE, o as SSO_ERROR_MESSAGES, p as SafeInternalPathOptions, S as SessionData, q as SessionGateDecision, r as SessionGateInput, s as SessionRefresher, t as SessionRefresherConfig, u as SessionUser, v as SignInHintCookieAttributes, w as SignInHintDomainOptions, T as TokenPayload, x as adminSlugFromHost, y as applyForwardedClientHeaders, z as authErrorMessage, B as brandedAdminHost, E as brokerPaths, F as createDeviceIdentity, G as createHandoffSource, I as createHandoffTarget, J as createSessionRefresher, K as decodeBase64Url, L as decodeDefaultSession, M as decodeTokenPayload, N as deleteCookie, O as evaluateSessionGate, P as forwardedRequestHeaders, Q as handoffErrorMessage, U as hasSessionShape, V as hasSignInHint, W as hasTokenAudience, X as isAdminRootHost, Y as isAuthErrorCode, Z as isHandoffErrorCode, _ as isTokenExpired, $ as isTokenExpiringSoon, a0 as parseAuthResponse, a1 as parseRefreshEnvelope, a2 as readCookie, a3 as recoveryAdminOrigin, a4 as safeInternalPath, a5 as serializeCookie, a6 as setCookie, a7 as signInHintCookie, a8 as signInHintDomain, a9 as sourceStartPath, aa as tokenMaxAgeSeconds } from './index-Bc_E5HWA.js';
|
package/dist/index.js
CHANGED
|
@@ -81,7 +81,7 @@ function parseAuthResponse(value) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// src/return-path.ts
|
|
84
|
-
function safeInternalPath(raw) {
|
|
84
|
+
function safeInternalPath(raw, options) {
|
|
85
85
|
if (raw == null) return null;
|
|
86
86
|
const candidate = raw.trim();
|
|
87
87
|
if (candidate.length === 0) return null;
|
|
@@ -94,7 +94,11 @@ function safeInternalPath(raw) {
|
|
|
94
94
|
if (parsed.origin !== "https://internal.invalid") return null;
|
|
95
95
|
const roundtrip = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
96
96
|
if (roundtrip !== candidate) return null;
|
|
97
|
-
return candidate;
|
|
97
|
+
if (!options?.stripParams?.length) return candidate;
|
|
98
|
+
const params = new URLSearchParams(parsed.search);
|
|
99
|
+
for (const key of options.stripParams) params.delete(key);
|
|
100
|
+
const query = params.toString();
|
|
101
|
+
return `${parsed.pathname}${query ? `?${query}` : ""}${parsed.hash}`;
|
|
98
102
|
} catch {
|
|
99
103
|
return null;
|
|
100
104
|
}
|
|
@@ -564,7 +568,9 @@ function hasSessionShape(token) {
|
|
|
564
568
|
}
|
|
565
569
|
async function evaluateSessionGate(input) {
|
|
566
570
|
const { accessToken, refreshToken, audience, refresher, isPublic, isLoginPath, isApiPath } = input;
|
|
567
|
-
const
|
|
571
|
+
const rawLive = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
|
|
572
|
+
const eagerlyExpiring = rawLive && input.eagerReauthBufferSeconds != null && isTokenExpiringSoon(accessToken, input.eagerReauthBufferSeconds);
|
|
573
|
+
const live = rawLive && !eagerlyExpiring;
|
|
568
574
|
const attempt = async (token) => refresher.attemptRefresh ? await refresher.attemptRefresh(token) : await refresher.refresh(token).then(
|
|
569
575
|
(session) => session ? { status: "refreshed", session } : { status: "rejected" }
|
|
570
576
|
);
|
|
@@ -588,4 +594,78 @@ async function evaluateSessionGate(input) {
|
|
|
588
594
|
return { kind: "redirect_login" };
|
|
589
595
|
}
|
|
590
596
|
|
|
591
|
-
|
|
597
|
+
// src/hosts.ts
|
|
598
|
+
var ADMIN_HOST_ROOT = "saasdash.app";
|
|
599
|
+
function normalizeRequestHost(hostname) {
|
|
600
|
+
let host = hostname.trim().toLowerCase();
|
|
601
|
+
if (host.startsWith("[")) {
|
|
602
|
+
const end = host.indexOf("]");
|
|
603
|
+
host = end >= 0 ? host.slice(1, end) : host;
|
|
604
|
+
} else {
|
|
605
|
+
const colon = host.lastIndexOf(":");
|
|
606
|
+
if (colon >= 0) host = host.slice(0, colon);
|
|
607
|
+
}
|
|
608
|
+
while (host.endsWith(".")) host = host.slice(0, -1);
|
|
609
|
+
return host;
|
|
610
|
+
}
|
|
611
|
+
function normalizeGeneratedHost(raw) {
|
|
612
|
+
let host = raw.trim().toLowerCase();
|
|
613
|
+
host = host.replace(/^https?:\/\//, "");
|
|
614
|
+
host = host.replace(/^www\./, "");
|
|
615
|
+
host = host.replace(/\/$/, "");
|
|
616
|
+
return host;
|
|
617
|
+
}
|
|
618
|
+
function isAdminRootHost(hostname) {
|
|
619
|
+
const host = normalizeRequestHost(hostname);
|
|
620
|
+
return host !== ADMIN_HOST_ROOT && host.endsWith(`.${ADMIN_HOST_ROOT}`);
|
|
621
|
+
}
|
|
622
|
+
function adminSlugFromHost(hostname) {
|
|
623
|
+
if (!isAdminRootHost(hostname)) return null;
|
|
624
|
+
const host = normalizeRequestHost(hostname);
|
|
625
|
+
const slug = host.slice(0, host.length - `.${ADMIN_HOST_ROOT}`.length);
|
|
626
|
+
if (!slug || slug === "www" || slug.includes(".")) return null;
|
|
627
|
+
return slug;
|
|
628
|
+
}
|
|
629
|
+
function recoveryAdminOrigin(slug) {
|
|
630
|
+
const normalized = normalizeGeneratedHost(slug);
|
|
631
|
+
if (!normalized) return "";
|
|
632
|
+
return `https://${normalized}.${ADMIN_HOST_ROOT}`;
|
|
633
|
+
}
|
|
634
|
+
function brandedAdminHost(domain) {
|
|
635
|
+
return `admin.${normalizeGeneratedHost(domain)}`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/sign-in-hint.ts
|
|
639
|
+
var SIGN_IN_HINT_COOKIE = "c1m_signed_in";
|
|
640
|
+
function normalizeHost(hostname) {
|
|
641
|
+
return hostname.split(":")[0]?.trim().toLowerCase() ?? "";
|
|
642
|
+
}
|
|
643
|
+
function signInHintDomain(hostname, options = {}) {
|
|
644
|
+
const host = normalizeHost(hostname);
|
|
645
|
+
const roots = [ADMIN_HOST_ROOT, ...options.excludedRoots ?? []];
|
|
646
|
+
for (const raw of roots) {
|
|
647
|
+
const root = normalizeHost(raw);
|
|
648
|
+
if (!host || !root || host === root || host.endsWith(`.${root}`)) return null;
|
|
649
|
+
}
|
|
650
|
+
const labels = host.split(".");
|
|
651
|
+
if (labels.length < 3) return null;
|
|
652
|
+
return labels.slice(1).join(".");
|
|
653
|
+
}
|
|
654
|
+
function signInHintCookie(domain, signedIn) {
|
|
655
|
+
return {
|
|
656
|
+
name: SIGN_IN_HINT_COOKIE,
|
|
657
|
+
value: signedIn ? "1" : "",
|
|
658
|
+
domain,
|
|
659
|
+
path: "/",
|
|
660
|
+
sameSite: "lax",
|
|
661
|
+
secure: true,
|
|
662
|
+
httpOnly: false,
|
|
663
|
+
maxAge: signedIn ? 30 * 24 * 60 * 60 : 0
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function hasSignInHint(cookieHeader) {
|
|
667
|
+
if (!cookieHeader) return false;
|
|
668
|
+
return cookieHeader.split(";").some((pair) => pair.trim() === `${SIGN_IN_HINT_COOKIE}=1`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export { ADMIN_HOST_ROOT, AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SIGN_IN_HINT_COOKIE, SSO_ERROR_MESSAGES, adminSlugFromHost, applyForwardedClientHeaders2 as applyForwardedClientHeaders, authErrorMessage, brandedAdminHost, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders2 as forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasSignInHint, hasTokenAudience, isAdminRootHost, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, recoveryAdminOrigin, safeInternalPath, serializeCookie, setCookie, signInHintCookie, signInHintDomain, sourceStartPath, tokenMaxAgeSeconds };
|
package/dist/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-
|
|
2
|
-
export { a as
|
|
1
|
+
import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-Bc_E5HWA.js';
|
|
2
|
+
export { a as ADMIN_HOST_ROOT, b as AUTH_ERROR_MESSAGES, c as AuthResponse, D as DeviceIdentity, d as DeviceIdentityConfig, e as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, f as HANDOFF_ERROR_MESSAGES, g as HandoffSource, h as HandoffSourceConfig, i as HandoffSourceTarget, j as HandoffTarget, k as HandoffTargetConfig, l as RefreshOutcome, m as RefreshedSession, n as SIGN_IN_HINT_COOKIE, o as SSO_ERROR_MESSAGES, p as SafeInternalPathOptions, q as SessionGateDecision, r as SessionGateInput, s as SessionRefresher, t as SessionRefresherConfig, u as SessionUser, v as SignInHintCookieAttributes, w as SignInHintDomainOptions, T as TokenPayload, x as adminSlugFromHost, y as applyForwardedClientHeaders, z as authErrorMessage, B as brandedAdminHost, E as brokerPaths, F as createDeviceIdentity, G as createHandoffSource, I as createHandoffTarget, J as createSessionRefresher, K as decodeBase64Url, L as decodeDefaultSession, M as decodeTokenPayload, N as deleteCookie, O as evaluateSessionGate, P as forwardedRequestHeaders, Q as handoffErrorMessage, U as hasSessionShape, V as hasSignInHint, W as hasTokenAudience, X as isAdminRootHost, Y as isAuthErrorCode, Z as isHandoffErrorCode, _ as isTokenExpired, $ as isTokenExpiringSoon, a0 as parseAuthResponse, a1 as parseRefreshEnvelope, a2 as readCookie, a3 as recoveryAdminOrigin, a4 as safeInternalPath, a5 as serializeCookie, a6 as setCookie, a7 as signInHintCookie, a8 as signInHintDomain, a9 as sourceStartPath, aa as tokenMaxAgeSeconds } from './index-Bc_E5HWA.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The error vocabulary every first-party platform surface uses at its edge,
|
package/dist/next.js
CHANGED
|
@@ -86,7 +86,7 @@ function parseAuthResponse(value) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// src/return-path.ts
|
|
89
|
-
function safeInternalPath(raw) {
|
|
89
|
+
function safeInternalPath(raw, options) {
|
|
90
90
|
if (raw == null) return null;
|
|
91
91
|
const candidate = raw.trim();
|
|
92
92
|
if (candidate.length === 0) return null;
|
|
@@ -99,7 +99,11 @@ function safeInternalPath(raw) {
|
|
|
99
99
|
if (parsed.origin !== "https://internal.invalid") return null;
|
|
100
100
|
const roundtrip = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
101
101
|
if (roundtrip !== candidate) return null;
|
|
102
|
-
return candidate;
|
|
102
|
+
if (!options?.stripParams?.length) return candidate;
|
|
103
|
+
const params = new URLSearchParams(parsed.search);
|
|
104
|
+
for (const key of options.stripParams) params.delete(key);
|
|
105
|
+
const query = params.toString();
|
|
106
|
+
return `${parsed.pathname}${query ? `?${query}` : ""}${parsed.hash}`;
|
|
103
107
|
} catch {
|
|
104
108
|
return null;
|
|
105
109
|
}
|
|
@@ -569,7 +573,9 @@ function hasSessionShape(token) {
|
|
|
569
573
|
}
|
|
570
574
|
async function evaluateSessionGate(input) {
|
|
571
575
|
const { accessToken, refreshToken, audience, refresher, isPublic, isLoginPath, isApiPath } = input;
|
|
572
|
-
const
|
|
576
|
+
const rawLive = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
|
|
577
|
+
const eagerlyExpiring = rawLive && input.eagerReauthBufferSeconds != null && isTokenExpiringSoon(accessToken, input.eagerReauthBufferSeconds);
|
|
578
|
+
const live = rawLive && !eagerlyExpiring;
|
|
573
579
|
const attempt = async (token) => refresher.attemptRefresh ? await refresher.attemptRefresh(token) : await refresher.refresh(token).then(
|
|
574
580
|
(session) => session ? { status: "refreshed", session } : { status: "rejected" }
|
|
575
581
|
);
|
|
@@ -593,6 +599,80 @@ async function evaluateSessionGate(input) {
|
|
|
593
599
|
return { kind: "redirect_login" };
|
|
594
600
|
}
|
|
595
601
|
|
|
602
|
+
// src/hosts.ts
|
|
603
|
+
var ADMIN_HOST_ROOT = "saasdash.app";
|
|
604
|
+
function normalizeRequestHost(hostname) {
|
|
605
|
+
let host = hostname.trim().toLowerCase();
|
|
606
|
+
if (host.startsWith("[")) {
|
|
607
|
+
const end = host.indexOf("]");
|
|
608
|
+
host = end >= 0 ? host.slice(1, end) : host;
|
|
609
|
+
} else {
|
|
610
|
+
const colon = host.lastIndexOf(":");
|
|
611
|
+
if (colon >= 0) host = host.slice(0, colon);
|
|
612
|
+
}
|
|
613
|
+
while (host.endsWith(".")) host = host.slice(0, -1);
|
|
614
|
+
return host;
|
|
615
|
+
}
|
|
616
|
+
function normalizeGeneratedHost(raw) {
|
|
617
|
+
let host = raw.trim().toLowerCase();
|
|
618
|
+
host = host.replace(/^https?:\/\//, "");
|
|
619
|
+
host = host.replace(/^www\./, "");
|
|
620
|
+
host = host.replace(/\/$/, "");
|
|
621
|
+
return host;
|
|
622
|
+
}
|
|
623
|
+
function isAdminRootHost(hostname) {
|
|
624
|
+
const host = normalizeRequestHost(hostname);
|
|
625
|
+
return host !== ADMIN_HOST_ROOT && host.endsWith(`.${ADMIN_HOST_ROOT}`);
|
|
626
|
+
}
|
|
627
|
+
function adminSlugFromHost(hostname) {
|
|
628
|
+
if (!isAdminRootHost(hostname)) return null;
|
|
629
|
+
const host = normalizeRequestHost(hostname);
|
|
630
|
+
const slug = host.slice(0, host.length - `.${ADMIN_HOST_ROOT}`.length);
|
|
631
|
+
if (!slug || slug === "www" || slug.includes(".")) return null;
|
|
632
|
+
return slug;
|
|
633
|
+
}
|
|
634
|
+
function recoveryAdminOrigin(slug) {
|
|
635
|
+
const normalized = normalizeGeneratedHost(slug);
|
|
636
|
+
if (!normalized) return "";
|
|
637
|
+
return `https://${normalized}.${ADMIN_HOST_ROOT}`;
|
|
638
|
+
}
|
|
639
|
+
function brandedAdminHost(domain) {
|
|
640
|
+
return `admin.${normalizeGeneratedHost(domain)}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/sign-in-hint.ts
|
|
644
|
+
var SIGN_IN_HINT_COOKIE = "c1m_signed_in";
|
|
645
|
+
function normalizeHost(hostname) {
|
|
646
|
+
return hostname.split(":")[0]?.trim().toLowerCase() ?? "";
|
|
647
|
+
}
|
|
648
|
+
function signInHintDomain(hostname, options = {}) {
|
|
649
|
+
const host = normalizeHost(hostname);
|
|
650
|
+
const roots = [ADMIN_HOST_ROOT, ...options.excludedRoots ?? []];
|
|
651
|
+
for (const raw of roots) {
|
|
652
|
+
const root = normalizeHost(raw);
|
|
653
|
+
if (!host || !root || host === root || host.endsWith(`.${root}`)) return null;
|
|
654
|
+
}
|
|
655
|
+
const labels = host.split(".");
|
|
656
|
+
if (labels.length < 3) return null;
|
|
657
|
+
return labels.slice(1).join(".");
|
|
658
|
+
}
|
|
659
|
+
function signInHintCookie(domain, signedIn) {
|
|
660
|
+
return {
|
|
661
|
+
name: SIGN_IN_HINT_COOKIE,
|
|
662
|
+
value: signedIn ? "1" : "",
|
|
663
|
+
domain,
|
|
664
|
+
path: "/",
|
|
665
|
+
sameSite: "lax",
|
|
666
|
+
secure: true,
|
|
667
|
+
httpOnly: false,
|
|
668
|
+
maxAge: signedIn ? 30 * 24 * 60 * 60 : 0
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function hasSignInHint(cookieHeader) {
|
|
672
|
+
if (!cookieHeader) return false;
|
|
673
|
+
return cookieHeader.split(";").some((pair) => pair.trim() === `${SIGN_IN_HINT_COOKIE}=1`);
|
|
674
|
+
}
|
|
675
|
+
|
|
596
676
|
// src/next/errors.ts
|
|
597
677
|
var SessionExpiredError = class extends Error {
|
|
598
678
|
constructor() {
|
|
@@ -900,4 +980,4 @@ function createSessionStore(config) {
|
|
|
900
980
|
};
|
|
901
981
|
}
|
|
902
982
|
|
|
903
|
-
export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, PlatformServerFetchError, RecentAuthenticationRequiredError, SSOReauthenticationRequiredError, SSO_ERROR_MESSAGES, SessionExpiredError, applyForwardedClientHeaders2 as applyForwardedClientHeaders, authErrorMessage, authenticationErrorFor, brokerPaths, buildForwardedRequestHeaders, controlRequest, createDeviceIdentity, createHandoffSource, createHandoffTarget, createPlatformFetch, createSessionRefresher, createSessionStore, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders2 as forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isPlatformServerFetchError, isRecentAuthenticationRequiredError, isSSOReauthenticationRequiredError, isSessionExpiredError, isTokenExpired, isTokenExpiringSoon, mapControlError, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
|
|
983
|
+
export { ADMIN_HOST_ROOT, AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, PlatformServerFetchError, RecentAuthenticationRequiredError, SIGN_IN_HINT_COOKIE, SSOReauthenticationRequiredError, SSO_ERROR_MESSAGES, SessionExpiredError, adminSlugFromHost, applyForwardedClientHeaders2 as applyForwardedClientHeaders, authErrorMessage, authenticationErrorFor, brandedAdminHost, brokerPaths, buildForwardedRequestHeaders, controlRequest, createDeviceIdentity, createHandoffSource, createHandoffTarget, createPlatformFetch, createSessionRefresher, createSessionStore, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders2 as forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasSignInHint, hasTokenAudience, isAdminRootHost, isAuthErrorCode, isHandoffErrorCode, isPlatformServerFetchError, isRecentAuthenticationRequiredError, isSSOReauthenticationRequiredError, isSessionExpiredError, isTokenExpired, isTokenExpiringSoon, mapControlError, parseAuthResponse, parseRefreshEnvelope, readCookie, recoveryAdminOrigin, safeInternalPath, serializeCookie, setCookie, signInHintCookie, signInHintDomain, sourceStartPath, tokenMaxAgeSeconds };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c1m-public/platform-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
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",
|