@c1m-public/platform-auth 0.4.0 → 0.5.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 +2 -1
- package/dist/access/generated/keys.d.ts +7 -1
- package/dist/access/generated/keys.js +20 -0
- package/dist/access/predicates.d.ts +12 -0
- package/dist/access/predicates.js +19 -0
- package/dist/access/snapshot.d.ts +8 -0
- package/dist/access/snapshot.js +13 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ sessions never needs them.
|
|
|
32
32
|
| `createSessionRefresher` | Coalesced refresh-token exchange: one backend call per token per window. |
|
|
33
33
|
| `evaluateSessionGate`, `hasSessionShape` | Pure proxy decision — `pass` / `redirect_home` / `anonymous_public` / `unauthorized_api` / `redirect_login`. The proxy owns response construction. |
|
|
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
|
-
| `HANDOFF_ERROR_MESSAGES`, `handoffErrorMessage`, `isHandoffErrorCode` | One error vocabulary
|
|
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
36
|
|
|
37
37
|
```ts
|
|
38
38
|
// proxy.ts
|
|
@@ -65,6 +65,7 @@ The resolved snapshot from `GET /api/v1/platform/access`.
|
|
|
65
65
|
| `PLATFORM_PERMISSIONS`, `LEADBROKER_PERMISSIONS`, `WORKSPACE_PERMISSIONS`, `WORKSPACE_ROLES` + key types | Generated from the backend catalog — see the drift guard below. |
|
|
66
66
|
| `parseAccessSnapshot`, `emptySnapshot`, `AccessSnapshot`, `AccessEntry` | Validate and shape the snapshot. |
|
|
67
67
|
| `hasPermission`, `hasAnyPermission`, `hasAllPermissions`, `hasWorkspacePermission`, `isPlatformOwner`, `appScopes`, `permissionsFor` | Scope-aware predicates. |
|
|
68
|
+
| `managedDashes`, `dashRole` | The dashes a subject runs, from the mirrored `dash.owner` / `dash.admin` bundle grants (`snapshot.dash_roles`). Admission only — gate an action with `hasPermission(snapshot, "dash.publish", { saasApp })`. Platform managers are not listed; admit them on `platform.tenants.manage`. |
|
|
68
69
|
| `fetchAccessSnapshot` | Framework-neutral fetch. Pass `request` and it forwards client identity for you. |
|
|
69
70
|
| `AccessSnapshotError`, `isAccessSnapshotError` | `unauthenticated` / `forbidden` / `unavailable` / `invalid_snapshot`. |
|
|
70
71
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/** Platform-operator permission keys. */
|
|
2
2
|
export declare const PLATFORM_PERMISSIONS: readonly ["platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.channels.manage", "platform.channels.view", "platform.config.manage", "platform.docs.manage", "platform.docs.view", "platform.observability.manage", "platform.observability.view", "platform.ops.manage", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
|
|
3
3
|
export type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
|
|
4
|
+
/** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
|
|
5
|
+
export declare const DASH_PERMISSIONS: readonly ["dash.apps.manage", "dash.billing.manage", "dash.channels.manage", "dash.customers.manage", "dash.domains.manage", "dash.members.manage", "dash.ownership.transfer", "dash.plans.manage", "dash.publish", "dash.settings.manage", "dash.workspaces.admin", "dash.workspaces.manage"];
|
|
6
|
+
export type DashPermissionKey = (typeof DASH_PERMISSIONS)[number];
|
|
7
|
+
/** Membership-mirrored dash role bundles (grant keys at dash scope). */
|
|
8
|
+
export declare const DASH_ROLES: readonly ["dash.admin", "dash.owner"];
|
|
9
|
+
export type DashRole = (typeof DASH_ROLES)[number];
|
|
4
10
|
/** Lead-broker workload permission keys. */
|
|
5
11
|
export declare const LEADBROKER_PERMISSIONS: readonly ["leadbroker.finance.manage", "leadbroker.leads.dispatch", "leadbroker.leads.review", "leadbroker.leads.view", "leadbroker.policy.manage"];
|
|
6
12
|
export type LeadBrokerPermissionKey = (typeof LEADBROKER_PERMISSIONS)[number];
|
|
@@ -11,4 +17,4 @@ export type WorkspacePermissionKey = (typeof WORKSPACE_PERMISSIONS)[number];
|
|
|
11
17
|
export declare const WORKSPACE_ROLES: readonly ["workspace.editor", "workspace.frontdesk", "workspace.manager"];
|
|
12
18
|
export type WorkspaceRole = (typeof WORKSPACE_ROLES)[number];
|
|
13
19
|
/** Every permission key the backend can grant, across all scopes. */
|
|
14
|
-
export type PermissionKey = PlatformPermissionKey | LeadBrokerPermissionKey | WorkspacePermissionKey;
|
|
20
|
+
export type PermissionKey = PlatformPermissionKey | DashPermissionKey | LeadBrokerPermissionKey | WorkspacePermissionKey;
|
|
@@ -26,6 +26,26 @@ export const PLATFORM_PERMISSIONS = [
|
|
|
26
26
|
"platform.users.reactivate",
|
|
27
27
|
"platform.users.view",
|
|
28
28
|
];
|
|
29
|
+
/** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
|
|
30
|
+
export const DASH_PERMISSIONS = [
|
|
31
|
+
"dash.apps.manage",
|
|
32
|
+
"dash.billing.manage",
|
|
33
|
+
"dash.channels.manage",
|
|
34
|
+
"dash.customers.manage",
|
|
35
|
+
"dash.domains.manage",
|
|
36
|
+
"dash.members.manage",
|
|
37
|
+
"dash.ownership.transfer",
|
|
38
|
+
"dash.plans.manage",
|
|
39
|
+
"dash.publish",
|
|
40
|
+
"dash.settings.manage",
|
|
41
|
+
"dash.workspaces.admin",
|
|
42
|
+
"dash.workspaces.manage",
|
|
43
|
+
];
|
|
44
|
+
/** Membership-mirrored dash role bundles (grant keys at dash scope). */
|
|
45
|
+
export const DASH_ROLES = [
|
|
46
|
+
"dash.admin",
|
|
47
|
+
"dash.owner",
|
|
48
|
+
];
|
|
29
49
|
/** Lead-broker workload permission keys. */
|
|
30
50
|
export const LEADBROKER_PERMISSIONS = [
|
|
31
51
|
"leadbroker.finance.manage",
|
|
@@ -22,6 +22,18 @@ export declare function hasAllPermissions(snapshot: AccessSnapshot, keys: readon
|
|
|
22
22
|
* Platform/saas_app entries never confer workspace-staff permissions.
|
|
23
23
|
*/
|
|
24
24
|
export declare function hasWorkspacePermission(snapshot: AccessSnapshot, workspaceId: string, key: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The dashes this subject runs, from the mirrored dash bundle grants — the
|
|
27
|
+
* admission list for the Admin Center's dash area. Platform managers are not
|
|
28
|
+
* listed: their reach is platform-wide, so admit them with
|
|
29
|
+
* `hasPermission(snapshot, "platform.tenants.manage")` instead.
|
|
30
|
+
*/
|
|
31
|
+
export declare function managedDashes(snapshot: AccessSnapshot): Array<{
|
|
32
|
+
saasApp: string;
|
|
33
|
+
role: string;
|
|
34
|
+
}>;
|
|
35
|
+
/** The dash bundle held on one dash, or "" when the subject holds none there. */
|
|
36
|
+
export declare function dashRole(snapshot: AccessSnapshot, saasApp: string): string;
|
|
25
37
|
export declare function isPlatformOwner(snapshot: AccessSnapshot): boolean;
|
|
26
38
|
/**
|
|
27
39
|
* The saas_app scopes under which `key` is held. Returns `{ all: true }` when a
|
|
@@ -39,6 +39,25 @@ export function hasWorkspacePermission(snapshot, workspaceId, key) {
|
|
|
39
39
|
return true;
|
|
40
40
|
return (snapshot.workspace_permissions[workspaceId] ?? []).includes(key);
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The dashes this subject runs, from the mirrored dash bundle grants — the
|
|
44
|
+
* admission list for the Admin Center's dash area. Platform managers are not
|
|
45
|
+
* listed: their reach is platform-wide, so admit them with
|
|
46
|
+
* `hasPermission(snapshot, "platform.tenants.manage")` instead.
|
|
47
|
+
*/
|
|
48
|
+
export function managedDashes(snapshot) {
|
|
49
|
+
if (!snapshot.active)
|
|
50
|
+
return [];
|
|
51
|
+
return Object.entries(snapshot.dash_roles)
|
|
52
|
+
.map(([saasApp, role]) => ({ saasApp, role }))
|
|
53
|
+
.sort((a, b) => a.saasApp.localeCompare(b.saasApp));
|
|
54
|
+
}
|
|
55
|
+
/** The dash bundle held on one dash, or "" when the subject holds none there. */
|
|
56
|
+
export function dashRole(snapshot, saasApp) {
|
|
57
|
+
if (!snapshot.active)
|
|
58
|
+
return "";
|
|
59
|
+
return snapshot.dash_roles[saasApp] ?? "";
|
|
60
|
+
}
|
|
42
61
|
export function isPlatformOwner(snapshot) {
|
|
43
62
|
return snapshot.active && snapshot.is_owner;
|
|
44
63
|
}
|
|
@@ -21,6 +21,14 @@ export interface AccessSnapshot {
|
|
|
21
21
|
/** Platform operator base role (derived; display only — never authorize on it). */
|
|
22
22
|
platform_role: string;
|
|
23
23
|
permissions: AccessEntry[];
|
|
24
|
+
/**
|
|
25
|
+
* Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
|
|
26
|
+
* the subject's dash membership. This is the admission list for the dash
|
|
27
|
+
* area: the dashes this subject runs. What they may *do* on one is a normal
|
|
28
|
+
* `hasPermission(snapshot, key, { saasApp })` check — platform managers hold
|
|
29
|
+
* their authority platform-wide and so appear here not at all.
|
|
30
|
+
*/
|
|
31
|
+
dash_roles: Record<string, string>;
|
|
24
32
|
/** Effective workspace-staff permission keys per workspace id. */
|
|
25
33
|
workspace_permissions: Record<string, string[]>;
|
|
26
34
|
/** Workspace-staff role bundle per workspace id (display only). */
|
package/dist/access/snapshot.js
CHANGED
|
@@ -8,6 +8,7 @@ export function emptySnapshot(subjectId = "") {
|
|
|
8
8
|
active: false,
|
|
9
9
|
platform_role: "",
|
|
10
10
|
permissions: [],
|
|
11
|
+
dash_roles: {},
|
|
11
12
|
workspace_permissions: {},
|
|
12
13
|
workspace_roles: {},
|
|
13
14
|
};
|
|
@@ -22,7 +23,7 @@ function fail(field, why) {
|
|
|
22
23
|
export function parseAccessSnapshot(input) {
|
|
23
24
|
if (!isRecord(input))
|
|
24
25
|
fail("payload", "is not an object");
|
|
25
|
-
const { subject_id, access_version, is_owner, active, platform_role, permissions, workspace_permissions, workspace_roles } = input;
|
|
26
|
+
const { subject_id, access_version, is_owner, active, platform_role, permissions, dash_roles, workspace_permissions, workspace_roles } = input;
|
|
26
27
|
if (typeof subject_id !== "string" || subject_id === "")
|
|
27
28
|
fail("subject_id", "must be a non-empty string");
|
|
28
29
|
if (typeof access_version !== "number" || !Number.isInteger(access_version) || access_version < 0)
|
|
@@ -47,6 +48,16 @@ export function parseAccessSnapshot(input) {
|
|
|
47
48
|
fail(`permissions[${i}].scope_id`, "must be a string");
|
|
48
49
|
return { key, scope_type: scope_type, scope_id: typeof scope_id === "string" ? scope_id : "" };
|
|
49
50
|
});
|
|
51
|
+
const dashRoles = {};
|
|
52
|
+
if (dash_roles !== undefined && dash_roles !== null) {
|
|
53
|
+
if (!isRecord(dash_roles))
|
|
54
|
+
fail("dash_roles", "must be an object");
|
|
55
|
+
for (const [dash, role] of Object.entries(dash_roles)) {
|
|
56
|
+
if (typeof role !== "string")
|
|
57
|
+
fail(`dash_roles[${dash}]`, "must be a string");
|
|
58
|
+
dashRoles[dash] = role;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
50
61
|
const wsPerms = {};
|
|
51
62
|
if (workspace_permissions !== undefined && workspace_permissions !== null) {
|
|
52
63
|
if (!isRecord(workspace_permissions))
|
|
@@ -74,6 +85,7 @@ export function parseAccessSnapshot(input) {
|
|
|
74
85
|
active,
|
|
75
86
|
platform_role: typeof platform_role === "string" ? platform_role : "",
|
|
76
87
|
permissions: entries,
|
|
88
|
+
dash_roles: dashRoles,
|
|
77
89
|
workspace_permissions: wsPerms,
|
|
78
90
|
workspace_roles: wsRoles,
|
|
79
91
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c1m-public/platform-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Server-side session and authorization helpers for C1M platform surfaces: token/session reads, the first-party handoff (PKCE target + source), proxy session gate and coalesced refresh, plus the resolved access snapshot, typed permission keys, predicates, a Next loader and React bindings under ./access",
|
|
@@ -43,14 +43,6 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
|
-
"scripts": {
|
|
47
|
-
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
48
|
-
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
49
|
-
"lint": "tsc -p tsconfig.json --noEmit",
|
|
50
|
-
"test": "vitest",
|
|
51
|
-
"test:run": "vitest run",
|
|
52
|
-
"type-check": "tsc -p tsconfig.json --noEmit"
|
|
53
|
-
},
|
|
54
46
|
"dependencies": {
|
|
55
47
|
"server-only": "0.0.1"
|
|
56
48
|
},
|
|
@@ -74,5 +66,13 @@
|
|
|
74
66
|
"react": {
|
|
75
67
|
"optional": true
|
|
76
68
|
}
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
72
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
73
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
74
|
+
"test": "vitest",
|
|
75
|
+
"test:run": "vitest run",
|
|
76
|
+
"type-check": "tsc -p tsconfig.json --noEmit"
|
|
77
77
|
}
|
|
78
|
-
}
|
|
78
|
+
}
|