@c1m-public/platform-auth 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/access/index.d.ts +148 -5
- package/dist/access/index.js +2 -5
- package/dist/access/next.d.ts +11 -10
- package/dist/access/next.js +70 -81
- package/dist/access/react.d.ts +59 -12
- package/dist/access/react.js +84 -37
- package/dist/chunk-FBC2NUH7.js +83 -0
- package/dist/chunk-RR5DP3QT.js +261 -0
- package/dist/chunk-RVKIDFXM.js +44 -0
- package/dist/chunk-SMBKTB5V.js +389 -0
- package/dist/index.d.ts +220 -9
- package/dist/index.js +3 -9
- package/dist/internal-rnEuAE21.d.ts +4 -0
- package/dist/next.d.ts +168 -2
- package/dist/next.js +306 -3
- package/package.json +6 -4
- package/dist/access/errors.d.ts +0 -13
- package/dist/access/errors.js +0 -19
- package/dist/access/fetch.d.ts +0 -23
- package/dist/access/fetch.js +0 -43
- package/dist/access/generated/keys.d.ts +0 -20
- package/dist/access/generated/keys.js +0 -78
- package/dist/access/predicates.d.ts +0 -48
- package/dist/access/predicates.js +0 -95
- package/dist/access/snapshot.d.ts +0 -39
- package/dist/access/snapshot.js +0 -92
- package/dist/cookies.d.ts +0 -11
- package/dist/cookies.js +0 -42
- package/dist/device-cookie.d.ts +0 -18
- package/dist/device-cookie.js +0 -22
- package/dist/forwarding.d.ts +0 -2
- package/dist/forwarding.js +0 -32
- package/dist/handoff-errors.d.ts +0 -13
- package/dist/handoff-errors.js +0 -65
- package/dist/handoff.d.ts +0 -55
- package/dist/handoff.js +0 -195
- package/dist/internal.d.ts +0 -16
- package/dist/internal.js +0 -27
- package/dist/return-path.d.ts +0 -6
- package/dist/return-path.js +0 -33
- package/dist/session-gate.d.ts +0 -26
- package/dist/session-gate.js +0 -33
- package/dist/session-refresh.d.ts +0 -24
- package/dist/session-refresh.js +0 -62
- package/dist/token.d.ts +0 -51
- package/dist/token.js +0 -104
package/README.md
CHANGED
|
@@ -56,6 +56,33 @@ const source = createHandoffSource({ apiUrl, clientType, targets: { docs: { orig
|
|
|
56
56
|
getAccessToken, loginPath: "/login" });
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
## `./next` — a Next.js surface's session, fetch and errors
|
|
60
|
+
|
|
61
|
+
Server-only. Everything under `.` plus the three things every first-party app
|
|
62
|
+
used to hand-roll:
|
|
63
|
+
|
|
64
|
+
| Export | Purpose |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `createSessionStore({ audience, cookies, maxAgeSeconds?, secure?, cookieDomain?, defaultRememberMe?, refresh?, requestBoundary?, onError? })` | The session/refresh/remember cookies over `next/headers`: `createSession`, `writeSessionCookies(response, …)`, `getSession`, `getAccessToken`, `getRefreshToken`, `getSessionWithRefresh`, `updateAccessToken`, `isRememberMe`, `destroySession`, `clearSessionCookies(response)`, `requireSession`, `isAuthenticated`. rememberMe=false → no Max-Age; the remember choice survives refresh; deletes carry the same attributes as sets (a `__Host-` cookie ignores a Secure-less delete); a token for another audience is never stored or read. Pass `requestBoundary: connection` (from `next/server`) so Cache Components treat session state as request-time. |
|
|
67
|
+
| `createPlatformFetch({ apiUrl, getAccessToken, forwardHeaders?, timeoutMs?, fetch? })` | Bearer + forwarded client headers + `no-store` + per-method timeout (10 s reads, 30 s writes): `fetchAuthed`, `fetchAuthedJson` (null on 204/404), `fetchAuthedJsonStrict`, `fetchJsonWithAccessToken`, `fetchPublicJson`, `fetchAuthedField`, `fetchAuthedItems`. A missing session **throws** (401), never returns null. |
|
|
68
|
+
| `buildForwardedRequestHeaders` | The incoming request's client identity as headers, for a call you make yourself. |
|
|
69
|
+
| `SessionExpiredError`, `RecentAuthenticationRequiredError`, `SSOReauthenticationRequiredError`, `PlatformServerFetchError{path,status?,code?,requestId?}` + `is*Error` predicates | One error vocabulary; 401 bodies map by `code`. |
|
|
70
|
+
| `mapControlError`, `controlRequest(run)` | Translate a `@c1m-public/control-client` failure into that vocabulary, keeping the backend's code and request id. |
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// lib/auth/session.ts
|
|
74
|
+
export const sessions = createSessionStore({
|
|
75
|
+
audience: "platform_admin",
|
|
76
|
+
cookies: { session: "__Host-platform_session", refreshToken: "__Host-platform_refresh_token", rememberMe: "platform_remember" },
|
|
77
|
+
requestBoundary: connection,
|
|
78
|
+
refresh: { apiUrl: getApiUrl, refreshPath: "/api/v1/platform/auth/refresh", clientType: "nextjs-platform" },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// lib/platform/fetch.ts
|
|
82
|
+
export const platform = createPlatformFetch({ apiUrl: getApiUrl, getAccessToken: sessions.getAccessToken });
|
|
83
|
+
const items = await platform.fetchAuthedItems<Workspace>("/api/v1/platform/workspaces");
|
|
84
|
+
```
|
|
85
|
+
|
|
59
86
|
## `./access` — authorization
|
|
60
87
|
|
|
61
88
|
The resolved snapshot from `GET /api/v1/platform/access`.
|
package/dist/access/index.d.ts
CHANGED
|
@@ -1,5 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { R as Resolvable } from '../internal-rnEuAE21.js';
|
|
2
|
+
|
|
3
|
+
/** Platform-operator permission keys. */
|
|
4
|
+
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"];
|
|
5
|
+
type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
|
|
6
|
+
/** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
|
|
7
|
+
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"];
|
|
8
|
+
type DashPermissionKey = (typeof DASH_PERMISSIONS)[number];
|
|
9
|
+
/** Membership-mirrored dash role bundles (grant keys at dash scope). */
|
|
10
|
+
declare const DASH_ROLES: readonly ["dash.admin", "dash.owner"];
|
|
11
|
+
type DashRole = (typeof DASH_ROLES)[number];
|
|
12
|
+
/** Lead-broker workload permission keys. */
|
|
13
|
+
declare const LEADBROKER_PERMISSIONS: readonly ["leadbroker.finance.manage", "leadbroker.leads.dispatch", "leadbroker.leads.review", "leadbroker.leads.view", "leadbroker.policy.manage"];
|
|
14
|
+
type LeadBrokerPermissionKey = (typeof LEADBROKER_PERMISSIONS)[number];
|
|
15
|
+
/** Workspace-staff permission keys. */
|
|
16
|
+
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"];
|
|
17
|
+
type WorkspacePermissionKey = (typeof WORKSPACE_PERMISSIONS)[number];
|
|
18
|
+
/** Assignable workspace-staff role bundles (grant keys at workspace scope). */
|
|
19
|
+
declare const WORKSPACE_ROLES: readonly ["workspace.editor", "workspace.frontdesk", "workspace.manager"];
|
|
20
|
+
type WorkspaceRole = (typeof WORKSPACE_ROLES)[number];
|
|
21
|
+
/** Every permission key the backend can grant, across all scopes. */
|
|
22
|
+
type PermissionKey = PlatformPermissionKey | DashPermissionKey | LeadBrokerPermissionKey | WorkspacePermissionKey;
|
|
23
|
+
|
|
24
|
+
type AccessErrorCode = "unauthenticated" | "forbidden" | "unavailable" | "invalid_snapshot";
|
|
25
|
+
/**
|
|
26
|
+
* Typed failure for loading or validating an access snapshot. `unauthenticated`
|
|
27
|
+
* = no/expired session (401); `forbidden` = the backend refused the snapshot
|
|
28
|
+
* (403; e.g. wrong audience); `unavailable` = transport/5xx; `invalid_snapshot`
|
|
29
|
+
* = the payload did not match the contract.
|
|
30
|
+
*/
|
|
31
|
+
declare class AccessSnapshotError extends Error {
|
|
32
|
+
readonly code: AccessErrorCode;
|
|
33
|
+
readonly status: number | undefined;
|
|
34
|
+
constructor(code: AccessErrorCode, message: string, status?: number);
|
|
35
|
+
}
|
|
36
|
+
declare function isAccessSnapshotError(error: unknown, code?: AccessErrorCode): error is AccessSnapshotError;
|
|
37
|
+
|
|
38
|
+
/** Scope a permission entry applies to. Mirrors c1m-server authz.Scope*. */
|
|
39
|
+
type AccessScopeType = "platform" | "saas_app" | "workspace";
|
|
40
|
+
interface AccessEntry {
|
|
41
|
+
/** Permission key, e.g. "platform.users.manage_access". */
|
|
42
|
+
key: string;
|
|
43
|
+
scope_type: AccessScopeType;
|
|
44
|
+
/** "" for platform scope; saas_app slug or "*" for saas_app scope; workspace id for workspace scope. */
|
|
45
|
+
scope_id: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The resolved authorization snapshot for one subject, as returned by
|
|
49
|
+
* `GET /api/v1/platform/access`. This is the *output* of the backend resolver
|
|
50
|
+
* (base-role grant ∪ scoped grants), never the grant rows themselves.
|
|
51
|
+
*/
|
|
52
|
+
interface AccessSnapshot {
|
|
53
|
+
subject_id: string;
|
|
54
|
+
/** Monotonic; bumps on any grant/role change. Use for optimistic concurrency and cache keys. */
|
|
55
|
+
access_version: number;
|
|
56
|
+
is_owner: boolean;
|
|
57
|
+
active: boolean;
|
|
58
|
+
/** Platform operator base role (derived; display only — never authorize on it). */
|
|
59
|
+
platform_role: string;
|
|
60
|
+
permissions: AccessEntry[];
|
|
61
|
+
/**
|
|
62
|
+
* Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
|
|
63
|
+
* the subject's dash membership. This is the admission list for the dash
|
|
64
|
+
* area: the dashes this subject runs. What they may *do* on one is a normal
|
|
65
|
+
* `hasPermission(snapshot, key, { saasApp })` check — platform managers hold
|
|
66
|
+
* their authority platform-wide and so appear here not at all.
|
|
67
|
+
*/
|
|
68
|
+
dash_roles: Record<string, string>;
|
|
69
|
+
/** Effective workspace-staff permission keys per workspace id. */
|
|
70
|
+
workspace_permissions: Record<string, string[]>;
|
|
71
|
+
/** Workspace-staff role bundle per workspace id (display only). */
|
|
72
|
+
workspace_roles: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
declare function emptySnapshot(subjectId?: string): AccessSnapshot;
|
|
75
|
+
/** Runtime validator for the wire payload. Throws AccessSnapshotError("invalid_snapshot"). */
|
|
76
|
+
declare function parseAccessSnapshot(input: unknown): AccessSnapshot;
|
|
77
|
+
|
|
78
|
+
interface PermissionCheckOptions {
|
|
79
|
+
/**
|
|
80
|
+
* SaaS app (dash) slug the check is about. Omit for "anywhere": platform
|
|
81
|
+
* entries and any saas_app entry satisfy the check.
|
|
82
|
+
*/
|
|
83
|
+
saasApp?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Mirrors c1m-server authz.entryCoversApp: platform scope covers every app;
|
|
87
|
+
* saas_app scope covers "*" or a case-insensitive slug match; workspace-scoped
|
|
88
|
+
* entries never satisfy an app-level check.
|
|
89
|
+
*/
|
|
90
|
+
declare function entryCoversApp(entry: AccessEntry, saasApp: string | undefined): boolean;
|
|
91
|
+
/** True when the subject is active and holds `key` for the given app (or anywhere). */
|
|
92
|
+
declare function hasPermission(snapshot: AccessSnapshot, key: string, opts?: PermissionCheckOptions): boolean;
|
|
93
|
+
declare function hasAnyPermission(snapshot: AccessSnapshot, keys: readonly string[], opts?: PermissionCheckOptions): boolean;
|
|
94
|
+
declare function hasAllPermissions(snapshot: AccessSnapshot, keys: readonly string[], opts?: PermissionCheckOptions): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Workspace-staff check. Platform owners always pass (they own every
|
|
97
|
+
* workspace); otherwise the workspace's effective permission list decides.
|
|
98
|
+
* Platform/saas_app entries never confer workspace-staff permissions.
|
|
99
|
+
*/
|
|
100
|
+
declare function hasWorkspacePermission(snapshot: AccessSnapshot, workspaceId: string, key: string): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The dashes this subject runs, from the mirrored dash bundle grants — the
|
|
103
|
+
* admission list for the Admin Center's dash area. Platform managers are not
|
|
104
|
+
* listed: their reach is platform-wide, so admit them with
|
|
105
|
+
* `hasPermission(snapshot, "platform.tenants.manage")` instead.
|
|
106
|
+
*/
|
|
107
|
+
declare function managedDashes(snapshot: AccessSnapshot): Array<{
|
|
108
|
+
saasApp: string;
|
|
109
|
+
role: string;
|
|
110
|
+
}>;
|
|
111
|
+
/** The dash bundle held on one dash, or "" when the subject holds none there. */
|
|
112
|
+
declare function dashRole(snapshot: AccessSnapshot, saasApp: string): string;
|
|
113
|
+
declare function isPlatformOwner(snapshot: AccessSnapshot): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* The saas_app scopes under which `key` is held. Returns `{ all: true }` when a
|
|
116
|
+
* platform-scoped or "*" entry grants it everywhere; otherwise the explicit
|
|
117
|
+
* app slugs (empty = nowhere). Mirrors authz.AppPermissionScope.
|
|
118
|
+
*/
|
|
119
|
+
declare function appScopes(snapshot: AccessSnapshot, key: string): {
|
|
120
|
+
all: boolean;
|
|
121
|
+
apps: string[];
|
|
122
|
+
};
|
|
123
|
+
/** Distinct permission keys held for `saasApp` (or anywhere). Handy for nav/badges. */
|
|
124
|
+
declare function permissionsFor(snapshot: AccessSnapshot, opts?: PermissionCheckOptions): string[];
|
|
125
|
+
|
|
126
|
+
declare const DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
|
|
127
|
+
interface FetchAccessSnapshotOptions {
|
|
128
|
+
/** Backend origin, e.g. https://api.example.com (no trailing slash needed). */
|
|
129
|
+
apiUrl: Resolvable;
|
|
130
|
+
/** Bearer access token for the platform surface. */
|
|
131
|
+
accessToken: string;
|
|
132
|
+
/** Defaults to /api/v1/platform/access. */
|
|
133
|
+
endpointPath?: string;
|
|
134
|
+
fetchTimeoutMs?: number;
|
|
135
|
+
fetchImpl?: typeof fetch;
|
|
136
|
+
/** Extra headers. Authorization and Accept are always set here. */
|
|
137
|
+
headers?: HeadersInit;
|
|
138
|
+
/** Incoming BFF request; its client identity (IP, geo, UA) is forwarded to the backend. */
|
|
139
|
+
request?: Request;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Framework-neutral fetch of the resolved access snapshot. Throws
|
|
143
|
+
* AccessSnapshotError: 401 → unauthenticated, 403 → forbidden, other non-2xx or
|
|
144
|
+
* network failure → unavailable, malformed body → invalid_snapshot.
|
|
145
|
+
*/
|
|
146
|
+
declare function fetchAccessSnapshot(opts: FetchAccessSnapshotOptions): Promise<AccessSnapshot>;
|
|
147
|
+
|
|
148
|
+
export { type AccessEntry, type AccessErrorCode, type AccessScopeType, type AccessSnapshot, AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, type DashPermissionKey, type DashRole, type FetchAccessSnapshotOptions, LEADBROKER_PERMISSIONS, type LeadBrokerPermissionKey, PLATFORM_PERMISSIONS, type PermissionCheckOptions, type PermissionKey, type PlatformPermissionKey, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, type WorkspacePermissionKey, type WorkspaceRole, appScopes, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor };
|
package/dist/access/index.js
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
export * from "./snapshot.js";
|
|
4
|
-
export * from "./predicates.js";
|
|
5
|
-
export * from "./fetch.js";
|
|
1
|
+
export { AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, LEADBROKER_PERMISSIONS, PLATFORM_PERMISSIONS, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, appScopes, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor } from '../chunk-RR5DP3QT.js';
|
|
2
|
+
import '../chunk-RVKIDFXM.js';
|
package/dist/access/next.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export * from "./index.js";
|
|
1
|
+
import { R as Resolvable } from '../internal-rnEuAE21.js';
|
|
2
|
+
import { AccessSnapshot, PermissionCheckOptions } from './index.js';
|
|
3
|
+
export { AccessEntry, AccessErrorCode, AccessScopeType, AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, DashPermissionKey, DashRole, FetchAccessSnapshotOptions, LEADBROKER_PERMISSIONS, LeadBrokerPermissionKey, PLATFORM_PERMISSIONS, PermissionKey, PlatformPermissionKey, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, WorkspacePermissionKey, WorkspaceRole, appScopes, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor } from './index.js';
|
|
4
|
+
|
|
6
5
|
/** Default cache tag for one subject's snapshot: `access:<subjectId>`. */
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
declare function accessCacheTag(subjectId: string): string;
|
|
7
|
+
interface CreateAccessLoaderOptions {
|
|
9
8
|
apiUrl: Resolvable;
|
|
10
9
|
endpointPath?: string;
|
|
11
10
|
/** Resolve the current request's bearer token (usually from the httpOnly session cookie). null = no session. */
|
|
@@ -32,11 +31,11 @@ export interface CreateAccessLoaderOptions {
|
|
|
32
31
|
}) => Promise<AccessSnapshot>;
|
|
33
32
|
cacheTag?: (subjectId: string) => string;
|
|
34
33
|
}
|
|
35
|
-
|
|
34
|
+
interface RequireAccessOptions extends PermissionCheckOptions {
|
|
36
35
|
/** "all" (default) requires every key; "any" requires at least one. */
|
|
37
36
|
mode?: "all" | "any";
|
|
38
37
|
}
|
|
39
|
-
|
|
38
|
+
interface AccessLoader {
|
|
40
39
|
/** Snapshot for the current request. Throws AccessSnapshotError (never interrupts). */
|
|
41
40
|
getAccess(): Promise<AccessSnapshot>;
|
|
42
41
|
/** Snapshot or null when unauthenticated. Other failures still throw. */
|
|
@@ -48,4 +47,6 @@ export interface AccessLoader {
|
|
|
48
47
|
invalidateAccess(subjectId: string): void;
|
|
49
48
|
cacheTag(subjectId: string): string;
|
|
50
49
|
}
|
|
51
|
-
|
|
50
|
+
declare function createAccessLoader(options: CreateAccessLoaderOptions): AccessLoader;
|
|
51
|
+
|
|
52
|
+
export { type AccessLoader, AccessSnapshot, type CreateAccessLoaderOptions, PermissionCheckOptions, type RequireAccessOptions, accessCacheTag, createAccessLoader };
|
package/dist/access/next.js
CHANGED
|
@@ -1,86 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import { hasAllPermissions, hasAnyPermission, hasWorkspacePermission } from "./predicates.js";
|
|
13
|
-
export * from "./index.js";
|
|
14
|
-
/** Default cache tag for one subject's snapshot: `access:<subjectId>`. */
|
|
15
|
-
export function accessCacheTag(subjectId) {
|
|
16
|
-
return `access:${subjectId}`;
|
|
1
|
+
import { decodeTokenPayload } from '../chunk-FBC2NUH7.js';
|
|
2
|
+
import { AccessSnapshotError, fetchAccessSnapshot, isAccessSnapshotError, hasAnyPermission, hasAllPermissions, hasWorkspacePermission } from '../chunk-RR5DP3QT.js';
|
|
3
|
+
export { AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, LEADBROKER_PERMISSIONS, PLATFORM_PERMISSIONS, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, appScopes, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor } from '../chunk-RR5DP3QT.js';
|
|
4
|
+
import { resolveValue } from '../chunk-RVKIDFXM.js';
|
|
5
|
+
import 'server-only';
|
|
6
|
+
import { cache } from 'react';
|
|
7
|
+
import { forbidden, unauthorized } from 'next/navigation';
|
|
8
|
+
import { updateTag } from 'next/cache';
|
|
9
|
+
|
|
10
|
+
function accessCacheTag(subjectId) {
|
|
11
|
+
return `access:${subjectId}`;
|
|
17
12
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return options.fetchSnapshot({ accessToken, subjectId });
|
|
32
|
-
}
|
|
33
|
-
const headers = typeof options.headers === "function" ? await options.headers() : options.headers;
|
|
34
|
-
return fetchAccessSnapshot({
|
|
35
|
-
apiUrl: resolveValue(options.apiUrl),
|
|
36
|
-
accessToken,
|
|
37
|
-
endpointPath: options.endpointPath,
|
|
38
|
-
fetchTimeoutMs: options.fetchTimeoutMs,
|
|
39
|
-
fetchImpl: options.fetchImpl,
|
|
40
|
-
headers,
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
async function getAccess() {
|
|
44
|
-
return load();
|
|
45
|
-
}
|
|
46
|
-
async function getAccessOrNull() {
|
|
47
|
-
try {
|
|
48
|
-
return await load();
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
if (isAccessSnapshotError(error, "unauthenticated"))
|
|
52
|
-
return null;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function loadOrInterrupt() {
|
|
57
|
-
try {
|
|
58
|
-
return await load();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
if (isAccessSnapshotError(error, "unauthenticated"))
|
|
62
|
-
unauthorized();
|
|
63
|
-
if (isAccessSnapshotError(error, "forbidden"))
|
|
64
|
-
forbidden();
|
|
65
|
-
throw error;
|
|
66
|
-
}
|
|
13
|
+
function createAccessLoader(options) {
|
|
14
|
+
const tag = options.cacheTag ?? accessCacheTag;
|
|
15
|
+
const subjectIdOf = (accessToken) => {
|
|
16
|
+
const claims = decodeTokenPayload(accessToken);
|
|
17
|
+
const subject = claims?.actor_id ?? claims?.user_id ?? claims?.customer_id ?? claims?.sub;
|
|
18
|
+
return typeof subject === "string" && subject.length > 0 ? subject : null;
|
|
19
|
+
};
|
|
20
|
+
const load = cache(async () => {
|
|
21
|
+
const accessToken = await options.getAccessToken();
|
|
22
|
+
if (!accessToken) throw new AccessSnapshotError("unauthenticated", "no session", 401);
|
|
23
|
+
if (options.fetchSnapshot) {
|
|
24
|
+
const subjectId = options.getSubjectId ? await options.getSubjectId() : subjectIdOf(accessToken);
|
|
25
|
+
return options.fetchSnapshot({ accessToken, subjectId });
|
|
67
26
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
27
|
+
const headers = typeof options.headers === "function" ? await options.headers() : options.headers;
|
|
28
|
+
return fetchAccessSnapshot({
|
|
29
|
+
apiUrl: resolveValue(options.apiUrl),
|
|
30
|
+
accessToken,
|
|
31
|
+
endpointPath: options.endpointPath,
|
|
32
|
+
fetchTimeoutMs: options.fetchTimeoutMs,
|
|
33
|
+
fetchImpl: options.fetchImpl,
|
|
34
|
+
headers
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
async function getAccess() {
|
|
38
|
+
return load();
|
|
39
|
+
}
|
|
40
|
+
async function getAccessOrNull() {
|
|
41
|
+
try {
|
|
42
|
+
return await load();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (isAccessSnapshotError(error, "unauthenticated")) return null;
|
|
45
|
+
throw error;
|
|
81
46
|
}
|
|
82
|
-
|
|
83
|
-
|
|
47
|
+
}
|
|
48
|
+
async function loadOrInterrupt() {
|
|
49
|
+
try {
|
|
50
|
+
return await load();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isAccessSnapshotError(error, "unauthenticated")) unauthorized();
|
|
53
|
+
if (isAccessSnapshotError(error, "forbidden")) forbidden();
|
|
54
|
+
throw error;
|
|
84
55
|
}
|
|
85
|
-
|
|
56
|
+
}
|
|
57
|
+
async function requireAccess(key, opts = {}) {
|
|
58
|
+
const snapshot = await loadOrInterrupt();
|
|
59
|
+
const keys = typeof key === "string" ? [key] : key;
|
|
60
|
+
const ok = opts.mode === "any" ? hasAnyPermission(snapshot, keys, opts) : hasAllPermissions(snapshot, keys, opts);
|
|
61
|
+
if (!ok) forbidden();
|
|
62
|
+
return snapshot;
|
|
63
|
+
}
|
|
64
|
+
async function requireWorkspaceAccess(workspaceId, key) {
|
|
65
|
+
const snapshot = await loadOrInterrupt();
|
|
66
|
+
if (!hasWorkspacePermission(snapshot, workspaceId, key)) forbidden();
|
|
67
|
+
return snapshot;
|
|
68
|
+
}
|
|
69
|
+
function invalidateAccess(subjectId) {
|
|
70
|
+
updateTag(tag(subjectId));
|
|
71
|
+
}
|
|
72
|
+
return { getAccess, getAccessOrNull, requireAccess, requireWorkspaceAccess, invalidateAccess, cacheTag: tag };
|
|
86
73
|
}
|
|
74
|
+
|
|
75
|
+
export { accessCacheTag, createAccessLoader };
|
package/dist/access/react.d.ts
CHANGED
|
@@ -1,19 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/** Scope a permission entry applies to. Mirrors c1m-server authz.Scope*. */
|
|
5
|
+
type AccessScopeType = "platform" | "saas_app" | "workspace";
|
|
6
|
+
interface AccessEntry {
|
|
7
|
+
/** Permission key, e.g. "platform.users.manage_access". */
|
|
8
|
+
key: string;
|
|
9
|
+
scope_type: AccessScopeType;
|
|
10
|
+
/** "" for platform scope; saas_app slug or "*" for saas_app scope; workspace id for workspace scope. */
|
|
11
|
+
scope_id: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The resolved authorization snapshot for one subject, as returned by
|
|
15
|
+
* `GET /api/v1/platform/access`. This is the *output* of the backend resolver
|
|
16
|
+
* (base-role grant ∪ scoped grants), never the grant rows themselves.
|
|
17
|
+
*/
|
|
18
|
+
interface AccessSnapshot {
|
|
19
|
+
subject_id: string;
|
|
20
|
+
/** Monotonic; bumps on any grant/role change. Use for optimistic concurrency and cache keys. */
|
|
21
|
+
access_version: number;
|
|
22
|
+
is_owner: boolean;
|
|
23
|
+
active: boolean;
|
|
24
|
+
/** Platform operator base role (derived; display only — never authorize on it). */
|
|
25
|
+
platform_role: string;
|
|
26
|
+
permissions: AccessEntry[];
|
|
27
|
+
/**
|
|
28
|
+
* Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
|
|
29
|
+
* the subject's dash membership. This is the admission list for the dash
|
|
30
|
+
* area: the dashes this subject runs. What they may *do* on one is a normal
|
|
31
|
+
* `hasPermission(snapshot, key, { saasApp })` check — platform managers hold
|
|
32
|
+
* their authority platform-wide and so appear here not at all.
|
|
33
|
+
*/
|
|
34
|
+
dash_roles: Record<string, string>;
|
|
35
|
+
/** Effective workspace-staff permission keys per workspace id. */
|
|
36
|
+
workspace_permissions: Record<string, string[]>;
|
|
37
|
+
/** Workspace-staff role bundle per workspace id (display only). */
|
|
38
|
+
workspace_roles: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface PermissionCheckOptions {
|
|
42
|
+
/**
|
|
43
|
+
* SaaS app (dash) slug the check is about. Omit for "anywhere": platform
|
|
44
|
+
* entries and any saas_app entry satisfy the check.
|
|
45
|
+
*/
|
|
46
|
+
saasApp?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface AccessProviderProps {
|
|
5
50
|
snapshot: AccessSnapshot;
|
|
6
51
|
children: ReactNode;
|
|
7
52
|
}
|
|
8
53
|
/** Feed the server-loaded snapshot to client components once, at the app/area layout. */
|
|
9
|
-
|
|
10
|
-
|
|
54
|
+
declare function AccessProvider({ snapshot, children }: AccessProviderProps): react.JSX.Element;
|
|
55
|
+
declare function useAccess(): AccessSnapshot;
|
|
11
56
|
/** Optional-provider variant: returns an inactive empty snapshot outside a provider. */
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
57
|
+
declare function useAccessOrEmpty(): AccessSnapshot;
|
|
58
|
+
declare function useCan(): (key: string, opts?: PermissionCheckOptions) => boolean;
|
|
59
|
+
declare function useCanWorkspace(workspaceId: string): (key: string) => boolean;
|
|
60
|
+
declare function useIsPlatformOwner(): boolean;
|
|
61
|
+
interface CanProps extends PermissionCheckOptions {
|
|
17
62
|
/** One key, or several — all must hold unless `mode="any"`. */
|
|
18
63
|
permission: string | readonly string[];
|
|
19
64
|
mode?: "all" | "any";
|
|
@@ -25,4 +70,6 @@ export interface CanProps extends PermissionCheckOptions {
|
|
|
25
70
|
children: ReactNode;
|
|
26
71
|
}
|
|
27
72
|
/** Render-gate. Presentation only — the backend remains the security boundary. */
|
|
28
|
-
|
|
73
|
+
declare function Can({ permission, mode, workspaceId, saasApp, not, fallback, children }: CanProps): react.JSX.Element;
|
|
74
|
+
|
|
75
|
+
export { AccessProvider, type AccessProviderProps, Can, type CanProps, useAccess, useAccessOrEmpty, useCan, useCanWorkspace, useIsPlatformOwner };
|
package/dist/access/react.js
CHANGED
|
@@ -1,40 +1,87 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
// src/access/react.tsx
|
|
3
4
|
import { createContext, useContext, useMemo } from "react";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return snapshot;
|
|
17
|
-
}
|
|
18
|
-
/** Optional-provider variant: returns an inactive empty snapshot outside a provider. */
|
|
19
|
-
export function useAccessOrEmpty() {
|
|
20
|
-
return useContext(AccessContext) ?? emptySnapshot();
|
|
21
|
-
}
|
|
22
|
-
export function useCan() {
|
|
23
|
-
const snapshot = useAccess();
|
|
24
|
-
return useMemo(() => (key, opts) => hasPermission(snapshot, key, opts), [snapshot]);
|
|
25
|
-
}
|
|
26
|
-
export function useCanWorkspace(workspaceId) {
|
|
27
|
-
const snapshot = useAccess();
|
|
28
|
-
return useMemo(() => (key) => hasWorkspacePermission(snapshot, workspaceId, key), [snapshot, workspaceId]);
|
|
29
|
-
}
|
|
30
|
-
export function useIsPlatformOwner() {
|
|
31
|
-
return isPlatformOwner(useAccess());
|
|
32
|
-
}
|
|
33
|
-
/** Render-gate. Presentation only — the backend remains the security boundary. */
|
|
34
|
-
export function Can({ permission, mode = "all", workspaceId, saasApp, not = false, fallback = null, children }) {
|
|
35
|
-
const snapshot = useAccess();
|
|
36
|
-
const keys = typeof permission === "string" ? [permission] : permission;
|
|
37
|
-
const check = (key) => workspaceId !== undefined ? hasWorkspacePermission(snapshot, workspaceId, key) : hasPermission(snapshot, key, { saasApp });
|
|
38
|
-
const allowed = mode === "any" ? keys.some(check) : keys.every(check);
|
|
39
|
-
return _jsx(_Fragment, { children: allowed !== not ? children : fallback });
|
|
5
|
+
|
|
6
|
+
// src/access/predicates.ts
|
|
7
|
+
function entryCoversApp(entry, saasApp) {
|
|
8
|
+
switch (entry.scope_type) {
|
|
9
|
+
case "platform":
|
|
10
|
+
return true;
|
|
11
|
+
case "saas_app":
|
|
12
|
+
if (saasApp === void 0) return true;
|
|
13
|
+
return entry.scope_id === "*" || entry.scope_id.toLowerCase() === saasApp.toLowerCase();
|
|
14
|
+
default:
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
40
17
|
}
|
|
18
|
+
function hasPermission(snapshot, key, opts = {}) {
|
|
19
|
+
if (!snapshot.active) return false;
|
|
20
|
+
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.saasApp));
|
|
21
|
+
}
|
|
22
|
+
function hasWorkspacePermission(snapshot, workspaceId, key) {
|
|
23
|
+
if (!snapshot.active) return false;
|
|
24
|
+
if (snapshot.is_owner) return true;
|
|
25
|
+
return (snapshot.workspace_permissions[workspaceId] ?? []).includes(key);
|
|
26
|
+
}
|
|
27
|
+
function isPlatformOwner(snapshot) {
|
|
28
|
+
return snapshot.active && snapshot.is_owner;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/access/snapshot.ts
|
|
32
|
+
function emptySnapshot(subjectId = "") {
|
|
33
|
+
return {
|
|
34
|
+
subject_id: subjectId,
|
|
35
|
+
access_version: 0,
|
|
36
|
+
is_owner: false,
|
|
37
|
+
active: false,
|
|
38
|
+
platform_role: "",
|
|
39
|
+
permissions: [],
|
|
40
|
+
dash_roles: {},
|
|
41
|
+
workspace_permissions: {},
|
|
42
|
+
workspace_roles: {}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/access/react.tsx
|
|
47
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
48
|
+
var AccessContext = createContext(null);
|
|
49
|
+
function AccessProvider({ snapshot, children }) {
|
|
50
|
+
const value = useMemo(() => snapshot, [snapshot]);
|
|
51
|
+
return /* @__PURE__ */ jsx(AccessContext.Provider, { value, children });
|
|
52
|
+
}
|
|
53
|
+
function useAccess() {
|
|
54
|
+
const snapshot = useContext(AccessContext);
|
|
55
|
+
if (!snapshot) throw new Error("useAccess must be used inside <AccessProvider>");
|
|
56
|
+
return snapshot;
|
|
57
|
+
}
|
|
58
|
+
function useAccessOrEmpty() {
|
|
59
|
+
return useContext(AccessContext) ?? emptySnapshot();
|
|
60
|
+
}
|
|
61
|
+
function useCan() {
|
|
62
|
+
const snapshot = useAccess();
|
|
63
|
+
return useMemo(() => (key, opts) => hasPermission(snapshot, key, opts), [snapshot]);
|
|
64
|
+
}
|
|
65
|
+
function useCanWorkspace(workspaceId) {
|
|
66
|
+
const snapshot = useAccess();
|
|
67
|
+
return useMemo(() => (key) => hasWorkspacePermission(snapshot, workspaceId, key), [snapshot, workspaceId]);
|
|
68
|
+
}
|
|
69
|
+
function useIsPlatformOwner() {
|
|
70
|
+
return isPlatformOwner(useAccess());
|
|
71
|
+
}
|
|
72
|
+
function Can({ permission, mode = "all", workspaceId, saasApp, not = false, fallback = null, children }) {
|
|
73
|
+
const snapshot = useAccess();
|
|
74
|
+
const keys = typeof permission === "string" ? [permission] : permission;
|
|
75
|
+
const check = (key) => workspaceId !== void 0 ? hasWorkspacePermission(snapshot, workspaceId, key) : hasPermission(snapshot, key, { saasApp });
|
|
76
|
+
const allowed = mode === "any" ? keys.some(check) : keys.every(check);
|
|
77
|
+
return /* @__PURE__ */ jsx(Fragment, { children: allowed !== not ? children : fallback });
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
AccessProvider,
|
|
81
|
+
Can,
|
|
82
|
+
useAccess,
|
|
83
|
+
useAccessOrEmpty,
|
|
84
|
+
useCan,
|
|
85
|
+
useCanWorkspace,
|
|
86
|
+
useIsPlatformOwner
|
|
87
|
+
};
|