@c1m-public/platform-auth 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ /** Platform-operator permission keys. */
2
+ declare const PLATFORM_PERMISSIONS: readonly ["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.channels.manage", "platform.channels.view", "platform.config.manage", "platform.docs.manage", "platform.docs.view", "platform.observability.manage", "platform.observability.view", "platform.offboarding.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
+ type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
4
+ /** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
5
+ 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
+ type DashPermissionKey = (typeof DASH_PERMISSIONS)[number];
7
+ /** Membership-mirrored dash role bundles (grant keys at dash scope). */
8
+ declare const DASH_ROLES: readonly ["dash.admin", "dash.owner"];
9
+ type DashRole = (typeof DASH_ROLES)[number];
10
+ /** Lead-broker workload permission keys. */
11
+ declare const LEADBROKER_PERMISSIONS: readonly ["leadbroker.finance.manage", "leadbroker.leads.dispatch", "leadbroker.leads.review", "leadbroker.leads.view", "leadbroker.policy.manage"];
12
+ type LeadBrokerPermissionKey = (typeof LEADBROKER_PERMISSIONS)[number];
13
+ /** Workspace-staff permission keys. */
14
+ 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
+ type WorkspacePermissionKey = (typeof WORKSPACE_PERMISSIONS)[number];
16
+ /** Assignable workspace-staff role bundles (grant keys at workspace scope). */
17
+ declare const WORKSPACE_ROLES: readonly ["workspace.editor", "workspace.frontdesk", "workspace.manager"];
18
+ type WorkspaceRole = (typeof WORKSPACE_ROLES)[number];
19
+ /** Every permission key the backend can grant, across all scopes. */
20
+ type PermissionKey = PlatformPermissionKey | DashPermissionKey | LeadBrokerPermissionKey | WorkspacePermissionKey;
21
+
22
+ /** Onboarding pipeline stages, in funnel order. Labels and copy stay with the app; the strings are the backend's. */
23
+ declare const PIPELINE_STAGES: readonly ["created", "awaiting_payment", "provisioning", "attention", "active"];
24
+ type PipelineStage = (typeof PIPELINE_STAGES)[number];
25
+ declare function isPipelineStage(value: unknown): value is PipelineStage;
26
+
27
+ type AccessErrorCode = "unauthenticated" | "forbidden" | "unavailable" | "invalid_snapshot";
28
+ /**
29
+ * Typed failure for loading or validating an access snapshot. `unauthenticated`
30
+ * = no/expired session (401); `forbidden` = the backend refused the snapshot
31
+ * (403; e.g. wrong audience); `unavailable` = transport/5xx; `invalid_snapshot`
32
+ * = the payload did not match the contract.
33
+ */
34
+ declare class AccessSnapshotError extends Error {
35
+ readonly code: AccessErrorCode;
36
+ readonly status: number | undefined;
37
+ constructor(code: AccessErrorCode, message: string, status?: number);
38
+ }
39
+ declare function isAccessSnapshotError(error: unknown, code?: AccessErrorCode): error is AccessSnapshotError;
40
+
41
+ /** Scope a permission entry applies to. Mirrors c1m-server authz.Scope*. */
42
+ type AccessScopeType = "platform" | "saas_app" | "workspace";
43
+ interface AccessEntry {
44
+ /** Permission key, e.g. "platform.users.manage_access". */
45
+ key: string;
46
+ scope_type: AccessScopeType;
47
+ /** "" for platform scope; saas_app slug or "*" for saas_app scope; workspace id for workspace scope. */
48
+ scope_id: string;
49
+ }
50
+ /**
51
+ * The resolved authorization snapshot for one subject, as returned by
52
+ * `GET /api/v1/platform/access`. This is the *output* of the backend resolver
53
+ * (base-role grant ∪ scoped grants), never the grant rows themselves.
54
+ */
55
+ interface AccessSnapshot {
56
+ subject_id: string;
57
+ /** Monotonic; bumps on any grant/role change. Use for optimistic concurrency and cache keys. */
58
+ access_version: number;
59
+ is_owner: boolean;
60
+ active: boolean;
61
+ /** Platform operator base role (derived; display only — never authorize on it). */
62
+ platform_role: string;
63
+ permissions: AccessEntry[];
64
+ /**
65
+ * Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
66
+ * the subject's dash membership. This is the admission list for the dash
67
+ * area: the dashes this subject runs. What they may *do* on one is a normal
68
+ * `hasPermission(snapshot, key, { saasApp })` check — platform managers hold
69
+ * their authority platform-wide and so appear here not at all.
70
+ */
71
+ dash_roles: Record<string, string>;
72
+ /** Effective workspace-staff permission keys per workspace id. */
73
+ workspace_permissions: Record<string, string[]>;
74
+ /** Workspace-staff role bundle per workspace id (display only). */
75
+ workspace_roles: Record<string, string>;
76
+ }
77
+ declare function emptySnapshot(subjectId?: string): AccessSnapshot;
78
+ /** Runtime validator for the wire payload. Throws AccessSnapshotError("invalid_snapshot"). */
79
+ declare function parseAccessSnapshot(input: unknown): AccessSnapshot;
80
+
81
+ interface PermissionCheckOptions {
82
+ /**
83
+ * SaaS app (dash) slug the check is about. Omit for "anywhere": platform
84
+ * entries and any saas_app entry satisfy the check.
85
+ */
86
+ saasApp?: string;
87
+ }
88
+ /**
89
+ * Mirrors c1m-server authz.entryCoversApp: platform scope covers every app;
90
+ * saas_app scope covers "*" or a case-insensitive slug match; workspace-scoped
91
+ * entries never satisfy an app-level check.
92
+ */
93
+ declare function entryCoversApp(entry: AccessEntry, saasApp: string | undefined): boolean;
94
+ /** True when the subject is active and holds `key` for the given app (or anywhere). */
95
+ declare function hasPermission(snapshot: AccessSnapshot, key: string, opts?: PermissionCheckOptions): boolean;
96
+ declare function hasAnyPermission(snapshot: AccessSnapshot, keys: readonly string[], opts?: PermissionCheckOptions): boolean;
97
+ declare function hasAllPermissions(snapshot: AccessSnapshot, keys: readonly string[], opts?: PermissionCheckOptions): boolean;
98
+ /**
99
+ * Workspace-staff check. Platform owners always pass (they own every
100
+ * workspace); otherwise the workspace's effective permission list decides.
101
+ * Platform/saas_app entries never confer workspace-staff permissions.
102
+ */
103
+ declare function hasWorkspacePermission(snapshot: AccessSnapshot, workspaceId: string, key: string): boolean;
104
+ /**
105
+ * The dashes this subject runs, from the mirrored dash bundle grants — the
106
+ * admission list for the Admin Center's dash area. Platform managers are not
107
+ * listed: their reach is platform-wide, so admit them with
108
+ * `hasPermission(snapshot, "platform.tenants.manage")` instead.
109
+ */
110
+ declare function managedDashes(snapshot: AccessSnapshot): Array<{
111
+ saasApp: string;
112
+ role: string;
113
+ }>;
114
+ /** The dash bundle held on one dash, or "" when the subject holds none there. */
115
+ declare function dashRole(snapshot: AccessSnapshot, saasApp: string): string;
116
+ declare function isPlatformOwner(snapshot: AccessSnapshot): boolean;
117
+ /**
118
+ * The saas_app scopes under which `key` is held. Returns `{ all: true }` when a
119
+ * platform-scoped or "*" entry grants it everywhere; otherwise the explicit
120
+ * app slugs (empty = nowhere). Mirrors authz.AppPermissionScope.
121
+ */
122
+ declare function appScopes(snapshot: AccessSnapshot, key: string): {
123
+ all: boolean;
124
+ apps: string[];
125
+ };
126
+ /** Distinct permission keys held for `saasApp` (or anywhere). Handy for nav/badges. */
127
+ declare function permissionsFor(snapshot: AccessSnapshot, opts?: PermissionCheckOptions): string[];
128
+
129
+ /** A value that may be fixed or resolved per call (env read at request time). */
130
+ type Resolvable<T = string> = T | (() => T);
131
+
132
+ declare const DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
133
+ interface FetchAccessSnapshotOptions {
134
+ /** Backend origin, e.g. https://api.example.com (no trailing slash needed). */
135
+ apiUrl: Resolvable;
136
+ /** Bearer access token for the platform surface. */
137
+ accessToken: string;
138
+ /** Defaults to /api/v1/platform/access. */
139
+ endpointPath?: string;
140
+ fetchTimeoutMs?: number;
141
+ fetchImpl?: typeof fetch;
142
+ /** Extra headers. Authorization and Accept are always set here. */
143
+ headers?: HeadersInit;
144
+ /** Incoming BFF request; its client identity (IP, geo, UA) is forwarded to the backend. */
145
+ request?: Request;
146
+ }
147
+ /**
148
+ * Framework-neutral fetch of the resolved access snapshot. Throws
149
+ * AccessSnapshotError: 401 → unauthenticated, 403 → forbidden, other non-2xx or
150
+ * network failure → unavailable, malformed body → invalid_snapshot.
151
+ */
152
+ declare function fetchAccessSnapshot(opts: FetchAccessSnapshotOptions): Promise<AccessSnapshot>;
153
+
154
+ export { type AccessSnapshot as A, isAccessSnapshotError as B, isPipelineStage as C, DASH_PERMISSIONS as D, isPlatformOwner as E, type FetchAccessSnapshotOptions as F, managedDashes as G, parseAccessSnapshot as H, permissionsFor as I, LEADBROKER_PERMISSIONS as L, type PermissionCheckOptions as P, type Resolvable as R, WORKSPACE_PERMISSIONS as W, type AccessEntry as a, type AccessErrorCode as b, type AccessScopeType as c, AccessSnapshotError as d, DASH_ROLES as e, DEFAULT_ACCESS_ENDPOINT_PATH as f, type DashPermissionKey as g, type DashRole as h, type LeadBrokerPermissionKey as i, PIPELINE_STAGES as j, PLATFORM_PERMISSIONS as k, type PermissionKey as l, type PipelineStage as m, type PlatformPermissionKey as n, WORKSPACE_ROLES as o, type WorkspacePermissionKey as p, type WorkspaceRole as q, appScopes as r, dashRole as s, emptySnapshot as t, entryCoversApp as u, fetchAccessSnapshot as v, hasAllPermissions as w, hasAnyPermission as x, hasPermission as y, hasWorkspacePermission as z };
package/dist/index.d.ts CHANGED
@@ -1,231 +1,2 @@
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 RefreshedSession, l as SSO_ERROR_MESSAGES, S as SessionData, m as SessionGateDecision, n as SessionGateInput, o as SessionRefresher, p as SessionRefresherConfig, q as SessionUser, T as TokenPayload, r as authErrorMessage, s as brokerPaths, t as createDeviceIdentity, u as createHandoffSource, v as createHandoffTarget, w as createSessionRefresher, x as decodeBase64Url, y as decodeDefaultSession, z as decodeTokenPayload, B as deleteCookie, E as evaluateSessionGate, F as handoffErrorMessage, G as hasSessionShape, I as hasTokenAudience, J as isAuthErrorCode, K as isHandoffErrorCode, L as isTokenExpired, M as isTokenExpiringSoon, N as parseAuthResponse, O as parseRefreshEnvelope, P as readCookie, Q as safeInternalPath, U as serializeCookie, V as setCookie, W as sourceStartPath, X as tokenMaxAgeSeconds } from './index-BnchfjP_.js';
1
2
  export { applyForwardedClientHeaders, forwardedRequestHeaders } from '@c1m-public/transport';
2
- import { R as Resolvable, A as AsyncResolvable } from './internal-D6RafCm2.js';
3
-
4
- interface SessionUser {
5
- id: string;
6
- email: string;
7
- name: string;
8
- role: string;
9
- is_active: boolean;
10
- email_verified: boolean;
11
- created_at: string;
12
- last_login_at?: string;
13
- has_passkey?: boolean;
14
- }
15
- interface SessionData {
16
- user: SessionUser;
17
- token: string;
18
- exp: number;
19
- iat: number;
20
- }
21
- type TokenPayload = Record<string, unknown> & {
22
- sub?: string;
23
- exp?: number;
24
- iat?: number;
25
- actor_id?: string;
26
- effective_user_id?: string;
27
- customer_id?: string;
28
- user_id?: string;
29
- aud?: string | string[];
30
- email?: string;
31
- name?: string;
32
- role?: string;
33
- is_active?: boolean;
34
- email_verified?: boolean;
35
- created_at?: string;
36
- has_passkeys?: boolean;
37
- };
38
- declare function decodeBase64Url(input: string): string | null;
39
- declare function decodeTokenPayload(token: string): TokenPayload | null;
40
- declare function hasTokenAudience(token: string, requiredAudience: string): boolean;
41
- /** Cookie max-age clamped to the token's remaining lifetime. */
42
- declare function tokenMaxAgeSeconds(token: string, configuredMaxAge: number): number;
43
- declare function isTokenExpired(token: string): boolean;
44
- declare function isTokenExpiringSoon(token: string, bufferSeconds?: number): boolean;
45
- declare function decodeDefaultSession(token: string): SessionData | null;
46
- /** Backend auth response shape shared by verify-code, OIDC complete, and handoff exchange. */
47
- interface AuthResponse {
48
- access_token: string;
49
- refresh_token: string;
50
- token_type: string;
51
- expires_at: string;
52
- user: Record<string, unknown>;
53
- }
54
- declare function parseAuthResponse(value: unknown): AuthResponse | null;
55
-
56
- /**
57
- * Strict same-origin return path: must start with `/`, not `//` or contain `://`,
58
- * no encoded slashes or control chars, and round-trip URL parsing unchanged.
59
- * Returns the value verbatim (query/hash kept) or null.
60
- */
61
- declare function safeInternalPath(raw: string | null | undefined): string | null;
62
-
63
- interface CookieOptions {
64
- httpOnly?: boolean;
65
- secure?: boolean;
66
- sameSite?: "lax" | "strict" | "none";
67
- path?: string;
68
- maxAge?: number;
69
- }
70
- declare function readCookie(request: Request, name: string): string | null;
71
- declare function serializeCookie(name: string, value: string, options?: CookieOptions): string;
72
- declare function setCookie(response: Response, name: string, value: string, options?: CookieOptions): Response;
73
- declare function deleteCookie(response: Response, name: string, options?: Pick<CookieOptions, "path" | "secure">): Response;
74
-
75
- /**
76
- * Per-browser identity sent as `X-Client-Device-ID`; c1m-server refuses to
77
- * create a session without it. Informational only, no authority.
78
- */
79
- interface DeviceIdentity {
80
- value: string;
81
- created: boolean;
82
- }
83
- interface DeviceIdentityConfig {
84
- cookieName: string;
85
- secure?: boolean;
86
- maxAgeSeconds?: number;
87
- }
88
- declare function createDeviceIdentity(config: DeviceIdentityConfig): {
89
- read(request: Request): DeviceIdentity;
90
- persist(response: Response, identity: DeviceIdentity): Response;
91
- };
92
- type DeviceIdentityStore = ReturnType<typeof createDeviceIdentity>;
93
-
94
- declare const HANDOFF_ERROR_MESSAGES: Record<string, string>;
95
- declare const HANDOFF_ERROR_FALLBACK = "Sign-in failed \u2014 please try again.";
96
- declare const SSO_ERROR_MESSAGES: Record<string, string>;
97
- /** Every sign-in failure code this package knows: handoff, admission, and SSO. */
98
- declare const AUTH_ERROR_MESSAGES: Record<string, string>;
99
- declare function handoffErrorMessage(code: string | null | undefined): string;
100
- declare function isHandoffErrorCode(code: string | null | undefined): boolean;
101
- /**
102
- * Resolve any sign-in failure code to copy. `overrides` wins, so a surface can
103
- * reword a shared code or add its own without forking the whole map.
104
- */
105
- declare function authErrorMessage(code: string | null | undefined, overrides?: Record<string, string>): string;
106
- declare function isAuthErrorCode(code: string | null | undefined, overrides?: Record<string, string>): boolean;
107
-
108
- declare function sourceStartPath(app: string): string;
109
- declare function brokerPaths(app: string): {
110
- readonly start: `/api/v1/platform/auth/handoffs/${string}/start`;
111
- readonly exchange: `/api/v1/platform/auth/handoffs/${string}/exchange`;
112
- };
113
- interface HandoffTargetConfig {
114
- app: string;
115
- apiUrl: Resolvable;
116
- audience: string;
117
- clientType: string;
118
- sources: Record<string, Resolvable>;
119
- defaultSource: string;
120
- cookiePrefix: string;
121
- loginPath: string;
122
- defaultReturnPath: string;
123
- device: DeviceIdentityStore;
124
- /** Write session cookies onto `response` (or return void if written via a framework store). */
125
- establishSession: (session: AuthResponse, response: Response, request: Request) => Promise<Response | void> | Response | void;
126
- secureCookies?: boolean;
127
- timeoutMs?: number;
128
- /** @deprecated use `timeoutMs`. */
129
- fetchTimeoutMs?: number;
130
- fetch?: typeof fetch;
131
- }
132
- declare function createHandoffTarget(config: HandoffTargetConfig): {
133
- cookies: {
134
- readonly state: `${string}state`;
135
- readonly verifier: `${string}verifier`;
136
- readonly from: `${string}from`;
137
- };
138
- /** GET `?source=&from=` */
139
- start(request: Request): Promise<Response>;
140
- /** GET `?code=&state=` */
141
- callback(request: Request): Promise<Response>;
142
- };
143
- type HandoffTarget = ReturnType<typeof createHandoffTarget>;
144
- interface HandoffSourceTarget {
145
- origin: Resolvable;
146
- callbackPath?: string;
147
- loginPath?: string;
148
- }
149
- interface HandoffSourceConfig {
150
- apiUrl: Resolvable;
151
- clientType: string;
152
- targets: Record<string, HandoffSourceTarget>;
153
- /** Live bearer or null when signed out; refresh an expiring token before returning it. */
154
- getAccessToken: (request: Request) => Promise<string | null> | string | null;
155
- loginPath: string;
156
- timeoutMs?: number;
157
- /** @deprecated use `timeoutMs`. */
158
- fetchTimeoutMs?: number;
159
- fetch?: typeof fetch;
160
- }
161
- declare function createHandoffSource(config: HandoffSourceConfig): {
162
- /** GET `/api/auth/handoffs/{app}/start?code_challenge=&state=` */
163
- start(request: Request, app: string): Promise<Response>;
164
- };
165
- type HandoffSource = ReturnType<typeof createHandoffSource>;
166
-
167
- /**
168
- * Coalesced refresh-token exchange for a BFF proxy: same refresh token within
169
- * a short window shares one backend call. Per-process best effort; backend
170
- * reuse detection is the correctness guarantee.
171
- */
172
- interface RefreshedSession {
173
- accessToken: string;
174
- refreshToken?: string;
175
- }
176
- interface SessionRefresherConfig {
177
- /** Backend origin, no /api/v1. May resolve asynchronously (a tenant-scoped origin). */
178
- apiUrl: AsyncResolvable<string>;
179
- /** e.g. /api/v1/platform/auth/refresh; empty when `apiUrl` already resolves to the full URL. */
180
- refreshPath: string;
181
- /** Extra request headers, e.g. a service credential and its tenant selector. */
182
- headers?: AsyncResolvable<HeadersInit | undefined>;
183
- clientType: string;
184
- audience: string;
185
- singleflightTtlMs?: number;
186
- maxSingleflights?: number;
187
- timeoutMs?: number;
188
- /** @deprecated use `timeoutMs`. */
189
- fetchTimeoutMs?: number;
190
- fetch?: typeof fetch;
191
- }
192
- /**
193
- * Two refresh envelopes exist: the platform one (flat, with token_type and
194
- * expires_at) and the end-user one (`{ data: { access_token, refresh_token? } }`,
195
- * where a rotated refresh token is optional). Both carry the access token the
196
- * same way; that is all a refresh needs.
197
- */
198
- declare function parseRefreshEnvelope(value: unknown): RefreshedSession | null;
199
- declare function createSessionRefresher(config: SessionRefresherConfig): {
200
- refresh(refreshToken: string): Promise<RefreshedSession | null>;
201
- size(): number;
202
- };
203
- type SessionRefresher = ReturnType<typeof createSessionRefresher>;
204
-
205
- declare function hasSessionShape(token: string | undefined | null): boolean;
206
- type SessionGateDecision = {
207
- kind: "pass";
208
- refreshed?: RefreshedSession;
209
- } | {
210
- kind: "redirect_home";
211
- refreshed?: RefreshedSession;
212
- } | {
213
- kind: "anonymous_public";
214
- } | {
215
- kind: "unauthorized_api";
216
- } | {
217
- kind: "redirect_login";
218
- };
219
- interface SessionGateInput {
220
- accessToken?: string | null;
221
- refreshToken?: string | null;
222
- audience: string;
223
- refresher: Pick<SessionRefresher, "refresh">;
224
- isPublic: boolean;
225
- isLoginPath: boolean;
226
- isApiPath: boolean;
227
- expiringSoonBufferSeconds?: number;
228
- }
229
- declare function evaluateSessionGate(input: SessionGateInput): Promise<SessionGateDecision>;
230
-
231
- export { AUTH_ERROR_MESSAGES, type AuthResponse, type CookieOptions, type DeviceIdentity, type DeviceIdentityConfig, type DeviceIdentityStore, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, type HandoffSource, type HandoffSourceConfig, type HandoffSourceTarget, type HandoffTarget, type HandoffTargetConfig, type RefreshedSession, SSO_ERROR_MESSAGES, type SessionData, type SessionGateDecision, type SessionGateInput, type SessionRefresher, type SessionRefresherConfig, type SessionUser, type TokenPayload, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };