@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 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 shared by backend and BFF codes. |
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). */
@@ -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.4.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
+ }