@c1m-public/platform-auth 0.20.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 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
@@ -1 +1 @@
1
- export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, A as AccessSnapshot, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, P as PermissionCheckOptions, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-DeCYIw0L.js';
1
+ export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, A as AccessSnapshot, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, P as PermissionCheckOptions, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-XWID1Fkg.js';
@@ -66,7 +66,6 @@ var DASH_PERMISSIONS = [
66
66
  "dash.leads.view",
67
67
  "dash.learning.records.view",
68
68
  "dash.learning.view",
69
- "dash.marketing.view",
70
69
  "dash.mcp.access",
71
70
  "dash.members.manage",
72
71
  "dash.members.view",
@@ -1,5 +1,5 @@
1
- import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-DeCYIw0L.js';
2
- export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-DeCYIw0L.js';
1
+ import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-XWID1Fkg.js';
2
+ export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-XWID1Fkg.js';
3
3
 
4
4
  /** Default cache tag for one subject's snapshot: `access:<subjectId>`. */
5
5
  declare function accessCacheTag(subjectId: string): string;
@@ -324,7 +324,6 @@ var DASH_PERMISSIONS = [
324
324
  "dash.leads.view",
325
325
  "dash.learning.records.view",
326
326
  "dash.learning.view",
327
- "dash.marketing.view",
328
327
  "dash.mcp.access",
329
328
  "dash.members.manage",
330
329
  "dash.members.view",
@@ -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
- export { type AsyncResolvable as A, decodeDefaultSession as B, type CookieOptions as C, type DeviceIdentity as D, decodeTokenPayload as E, deleteCookie as F, evaluateSessionGate as G, HANDOFF_ERROR_FALLBACK as H, forwardedRequestHeaders as I, handoffErrorMessage as J, hasSessionShape as K, hasTokenAudience as L, isAuthErrorCode as M, isHandoffErrorCode as N, isTokenExpired as O, isTokenExpiringSoon as P, parseAuthResponse as Q, type Resolvable as R, type SessionData as S, type TokenPayload as T, parseRefreshEnvelope as U, readCookie as V, safeInternalPath as W, serializeCookie as X, setCookie as Y, sourceStartPath as Z, tokenMaxAgeSeconds as _, AUTH_ERROR_MESSAGES as a, type AuthResponse as b, type DeviceIdentityConfig as c, type DeviceIdentityStore as d, HANDOFF_ERROR_MESSAGES as e, type HandoffSource as f, type HandoffSourceConfig as g, type HandoffSourceTarget as h, type HandoffTarget as i, type HandoffTargetConfig as j, type RefreshOutcome as k, type RefreshedSession as l, SSO_ERROR_MESSAGES as m, type SessionGateDecision as n, type SessionGateInput as o, type SessionRefresher as p, type SessionRefresherConfig as q, type SessionUser as r, applyForwardedClientHeaders as s, authErrorMessage as t, brokerPaths as u, createDeviceIdentity as v, createHandoffSource as w, createHandoffTarget as x, createSessionRefresher as y, decodeBase64Url as z };
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 };
@@ -2,7 +2,7 @@
2
2
  declare const PLATFORM_PERMISSIONS: readonly ["affiliates.enrollments.manage", "affiliates.finance.manage", "affiliates.partners.manage", "affiliates.programmes.manage", "affiliates.view", "linkservice.domains.manage", "linkservice.links.manage", "linkservice.qr.manage", "linkservice.view", "platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.billing.view", "platform.channels.manage", "platform.channels.view", "platform.communications.content.view", "platform.communications.manage", "platform.communications.send", "platform.communications.view", "platform.config.manage", "platform.dashes.archive", "platform.dashes.manage", "platform.dashes.purge", "platform.dashes.restore", "platform.dashes.view", "platform.docs.manage", "platform.docs.view", "platform.leads.manage", "platform.leads.view", "platform.mcp.access", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.ops.view", "platform.ownership.transfer", "platform.service_accounts.manage", "platform.service_accounts.view", "platform.site_mode.manage", "platform.support.impersonate", "platform.support.impersonation.delegate", "platform.support.sessions", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.manage_profile", "platform.users.reactivate", "platform.users.view"];
3
3
  type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
4
4
  /** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
5
- declare const DASH_PERMISSIONS: readonly ["dash.activity.view", "dash.apps.manage", "dash.apps.view", "dash.billing.manage", "dash.billing.view", "dash.channels.manage", "dash.customers.manage", "dash.customers.view", "dash.domains.manage", "dash.domains.view", "dash.leads.export", "dash.leads.view", "dash.learning.records.view", "dash.learning.view", "dash.marketing.view", "dash.mcp.access", "dash.members.manage", "dash.members.view", "dash.ops_library.view", "dash.overview.view", "dash.ownership.transfer", "dash.plans.manage", "dash.plans.view", "dash.publish", "dash.settings.manage", "dash.settings.view", "dash.shop.view", "dash.workspaces.admin", "dash.workspaces.manage", "dash.workspaces.view"];
5
+ declare const DASH_PERMISSIONS: readonly ["dash.activity.view", "dash.apps.manage", "dash.apps.view", "dash.billing.manage", "dash.billing.view", "dash.channels.manage", "dash.customers.manage", "dash.customers.view", "dash.domains.manage", "dash.domains.view", "dash.leads.export", "dash.leads.view", "dash.learning.records.view", "dash.learning.view", "dash.mcp.access", "dash.members.manage", "dash.members.view", "dash.ops_library.view", "dash.overview.view", "dash.ownership.transfer", "dash.plans.manage", "dash.plans.view", "dash.publish", "dash.settings.manage", "dash.settings.view", "dash.shop.view", "dash.workspaces.admin", "dash.workspaces.manage", "dash.workspaces.view"];
6
6
  type DashPermissionKey = (typeof DASH_PERMISSIONS)[number];
7
7
  /** Membership-mirrored dash role bundles (grant keys at dash scope). */
8
8
  declare const DASH_ROLES: readonly ["dash.admin", "dash.owner"];
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, C as CookieOptions, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshOutcome, l as RefreshedSession, m as SSO_ERROR_MESSAGES, S as SessionData, n as SessionGateDecision, o as SessionGateInput, p as SessionRefresher, q as SessionRefresherConfig, r as SessionUser, T as TokenPayload, s as applyForwardedClientHeaders, t as authErrorMessage, u as brokerPaths, v as createDeviceIdentity, w as createHandoffSource, x as createHandoffTarget, y as createSessionRefresher, z as decodeBase64Url, B as decodeDefaultSession, E as decodeTokenPayload, F as deleteCookie, G as evaluateSessionGate, I as forwardedRequestHeaders, J as handoffErrorMessage, K as hasSessionShape, L as hasTokenAudience, M as isAuthErrorCode, N as isHandoffErrorCode, O as isTokenExpired, P as isTokenExpiringSoon, Q as parseAuthResponse, U as parseRefreshEnvelope, V as readCookie, W as safeInternalPath, X as serializeCookie, Y as setCookie, Z as sourceStartPath, _ as tokenMaxAgeSeconds } from './index-BxyVV6Wt.js';
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 live = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
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
- export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, applyForwardedClientHeaders2 as applyForwardedClientHeaders, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders2 as forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
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-BxyVV6Wt.js';
2
- export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshOutcome, l as RefreshedSession, m as SSO_ERROR_MESSAGES, n as SessionGateDecision, o as SessionGateInput, p as SessionRefresher, q as SessionRefresherConfig, r as SessionUser, T as TokenPayload, s as applyForwardedClientHeaders, t as authErrorMessage, u as brokerPaths, v as createDeviceIdentity, w as createHandoffSource, x as createHandoffTarget, y as createSessionRefresher, z as decodeBase64Url, B as decodeDefaultSession, E as decodeTokenPayload, F as deleteCookie, G as evaluateSessionGate, I as forwardedRequestHeaders, J as handoffErrorMessage, K as hasSessionShape, L as hasTokenAudience, M as isAuthErrorCode, N as isHandoffErrorCode, O as isTokenExpired, P as isTokenExpiringSoon, Q as parseAuthResponse, U as parseRefreshEnvelope, V as readCookie, W as safeInternalPath, X as serializeCookie, Y as setCookie, Z as sourceStartPath, _ as tokenMaxAgeSeconds } from './index-BxyVV6Wt.js';
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 live = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
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.20.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",