@c1m-public/platform-auth 0.11.0 → 0.13.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
@@ -12,6 +12,7 @@ need — the boundary is the subpath.
12
12
  . session: token reads, handoff, proxy gate, refresh (Web Request/Response)
13
13
  ./next session, server-only guard (Next route handlers)
14
14
  ./mcp-tokens self-service MCP PAT lifecycle (framework-neutral)
15
+ ./service-accounts operator-owned service accounts and credentials (framework-neutral)
15
16
  ./access authorization: snapshot, keys, predicates (framework-neutral)
16
17
  ./access/next access loader: require / forbidden / invalidate (Next, server-only)
17
18
  ./access/react <Can>, useCan, AccessProvider (client)
@@ -37,6 +38,29 @@ of URLs, logs, storage, and telemetry. A `recent_authentication_required` API
37
38
  error is a step-up challenge, not an expired session; send the user through the
38
39
  host application's re-authentication path before retrying the mint.
39
40
 
41
+ ## `./service-accounts` — operator service accounts
42
+
43
+ `createServiceAccountClient` manages the operator-owned service accounts under
44
+ `/api/v1/platform/identity/service-accounts`: list, create, update, replace
45
+ grants, and list, mint or revoke credentials. Reads need
46
+ `SERVICE_ACCOUNTS_VIEW_PERMISSION`; every mutation needs
47
+ `SERVICE_ACCOUNTS_MANAGE_PERMISSION` and is never retried.
48
+
49
+ A credential is minted from **reviewed operation ids**, not from arbitrary
50
+ permissions: callers name `operation_ids` and the backend derives the
51
+ permission ceiling, so a caller cannot widen its own grant. The client also
52
+ holds the caller to the wire contract before a request leaves the process — a
53
+ `dash:<id>` audience must carry exactly its route Dash in `dash_ids`, a Dash
54
+ grant must name one Dash rather than `*`, `expires_in_days` is 1–30, and
55
+ `max_output_bytes` is 1–1048576.
56
+
57
+ Every mutation carries `expected_version`; a `409` with
58
+ `service_account_version_conflict` means the record moved under the caller and
59
+ the read has to be repeated. A `recent_authentication_required` API error is a
60
+ step-up challenge, not an expired session. The minted `secret` is one-time and
61
+ is checked to correlate with the credential it belongs to: keep it out of URLs,
62
+ logs, storage, and telemetry — the server never returns it again.
63
+
40
64
  ## `.` — session
41
65
 
42
66
  | Export | Purpose |
@@ -110,7 +134,7 @@ The resolved snapshot from `GET /api/v1/platform/access`.
110
134
  | `PLATFORM_PERMISSIONS`, `LEADBROKER_PERMISSIONS`, `WORKSPACE_PERMISSIONS`, `WORKSPACE_ROLES` + key types | Generated from the backend catalog — see the drift guard below. |
111
135
  | `parseAccessSnapshot`, `emptySnapshot`, `AccessSnapshot`, `AccessEntry` | Validate and shape the snapshot. |
112
136
  | `hasPermission`, `hasAnyPermission`, `hasAllPermissions`, `hasWorkspacePermission`, `isPlatformOwner`, `appScopes`, `permissionsFor` | Scope-aware predicates. |
113
- | `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`. |
137
+ | `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", { dashId })`. Platform managers are not listed; admit them on `platform.tenants.manage`. |
114
138
  | `fetchAccessSnapshot` | Framework-neutral fetch. Pass `request` and it forwards client identity for you. |
115
139
  | `AccessSnapshotError`, `isAccessSnapshotError` | `unauthenticated` / `forbidden` / `unavailable` / `invalid_snapshot`. |
116
140
 
@@ -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-HL6QFIZy.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-CPjLkXA-.js';
@@ -26,6 +26,8 @@ var PLATFORM_PERMISSIONS = [
26
26
  "platform.offboarding.view",
27
27
  "platform.ops.manage",
28
28
  "platform.ops.view",
29
+ "platform.service_accounts.manage",
30
+ "platform.service_accounts.view",
29
31
  "platform.support.impersonate",
30
32
  "platform.support.sessions",
31
33
  "platform.tenants.archive",
@@ -111,7 +113,7 @@ function isAccessSnapshotError(error, code) {
111
113
  }
112
114
 
113
115
  // src/access/snapshot.ts
114
- var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "saas_app", "workspace"]);
116
+ var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "dash_id", "workspace"]);
115
117
  function emptySnapshot(subjectId = "") {
116
118
  return {
117
119
  subject_id: subjectId,
@@ -144,7 +146,7 @@ function parseAccessSnapshot(input) {
144
146
  if (!isRecord(raw)) fail(`permissions[${i}]`, "is not an object");
145
147
  const { key, scope_type, scope_id } = raw;
146
148
  if (typeof key !== "string" || key === "") fail(`permissions[${i}].key`, "must be a non-empty string");
147
- if (typeof scope_type !== "string" || !SCOPE_TYPES.has(scope_type)) fail(`permissions[${i}].scope_type`, "must be platform|saas_app|workspace");
149
+ if (typeof scope_type !== "string" || !SCOPE_TYPES.has(scope_type)) fail(`permissions[${i}].scope_type`, "must be platform|dash_id|workspace");
148
150
  if (scope_id !== void 0 && scope_id !== null && typeof scope_id !== "string") fail(`permissions[${i}].scope_id`, "must be a string");
149
151
  return { key, scope_type, scope_id: typeof scope_id === "string" ? scope_id : "" };
150
152
  });
@@ -190,7 +192,7 @@ function entryCoversApp(entry, dashId) {
190
192
  switch (entry.scope_type) {
191
193
  case "platform":
192
194
  return true;
193
- case "saas_app":
195
+ case "dash_id":
194
196
  if (dashId === void 0) return true;
195
197
  return entry.scope_id === "*" || entry.scope_id.toLowerCase() === dashId.toLowerCase();
196
198
  default:
@@ -229,7 +231,7 @@ function appScopes(snapshot, key) {
229
231
  for (const entry of snapshot.permissions) {
230
232
  if (entry.key !== key) continue;
231
233
  if (entry.scope_type === "platform") return { all: true, apps: [] };
232
- if (entry.scope_type === "saas_app") {
234
+ if (entry.scope_type === "dash_id") {
233
235
  if (entry.scope_id === "*") return { all: true, apps: [] };
234
236
  apps.add(entry.scope_id.toLowerCase());
235
237
  }
@@ -1,5 +1,5 @@
1
- import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-HL6QFIZy.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-HL6QFIZy.js';
1
+ import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-CPjLkXA-.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-CPjLkXA-.js';
3
3
 
4
4
  /** Default cache tag for one subject's snapshot: `access:<subjectId>`. */
5
5
  declare function accessCacheTag(subjectId: string): string;
@@ -93,7 +93,7 @@ function forwardedRequestHeaders2(request, init) {
93
93
  }
94
94
 
95
95
  // src/access/snapshot.ts
96
- var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "saas_app", "workspace"]);
96
+ var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "dash_id", "workspace"]);
97
97
  function emptySnapshot(subjectId = "") {
98
98
  return {
99
99
  subject_id: subjectId,
@@ -126,7 +126,7 @@ function parseAccessSnapshot(input) {
126
126
  if (!isRecord(raw)) fail(`permissions[${i}]`, "is not an object");
127
127
  const { key, scope_type, scope_id } = raw;
128
128
  if (typeof key !== "string" || key === "") fail(`permissions[${i}].key`, "must be a non-empty string");
129
- if (typeof scope_type !== "string" || !SCOPE_TYPES.has(scope_type)) fail(`permissions[${i}].scope_type`, "must be platform|saas_app|workspace");
129
+ if (typeof scope_type !== "string" || !SCOPE_TYPES.has(scope_type)) fail(`permissions[${i}].scope_type`, "must be platform|dash_id|workspace");
130
130
  if (scope_id !== void 0 && scope_id !== null && typeof scope_id !== "string") fail(`permissions[${i}].scope_id`, "must be a string");
131
131
  return { key, scope_type, scope_id: typeof scope_id === "string" ? scope_id : "" };
132
132
  });
@@ -201,7 +201,7 @@ function entryCoversApp(entry, dashId) {
201
201
  switch (entry.scope_type) {
202
202
  case "platform":
203
203
  return true;
204
- case "saas_app":
204
+ case "dash_id":
205
205
  if (dashId === void 0) return true;
206
206
  return entry.scope_id === "*" || entry.scope_id.toLowerCase() === dashId.toLowerCase();
207
207
  default:
@@ -240,7 +240,7 @@ function appScopes(snapshot, key) {
240
240
  for (const entry of snapshot.permissions) {
241
241
  if (entry.key !== key) continue;
242
242
  if (entry.scope_type === "platform") return { all: true, apps: [] };
243
- if (entry.scope_type === "saas_app") {
243
+ if (entry.scope_type === "dash_id") {
244
244
  if (entry.scope_id === "*") return { all: true, apps: [] };
245
245
  apps.add(entry.scope_id.toLowerCase());
246
246
  }
@@ -282,6 +282,8 @@ var PLATFORM_PERMISSIONS = [
282
282
  "platform.offboarding.view",
283
283
  "platform.ops.manage",
284
284
  "platform.ops.view",
285
+ "platform.service_accounts.manage",
286
+ "platform.service_accounts.view",
285
287
  "platform.support.impersonate",
286
288
  "platform.support.sessions",
287
289
  "platform.tenants.archive",
@@ -2,12 +2,12 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
 
4
4
  /** Scope a permission entry applies to. Mirrors c1m-server authz.Scope*. */
5
- type AccessScopeType = "platform" | "saas_app" | "workspace";
5
+ type AccessScopeType = "platform" | "dash_id" | "workspace";
6
6
  interface AccessEntry {
7
7
  /** Permission key, e.g. "platform.users.manage_access". */
8
8
  key: string;
9
9
  scope_type: AccessScopeType;
10
- /** "" for platform scope; saas_app slug or "*" for saas_app scope; workspace id for workspace scope. */
10
+ /** "" for platform scope; dash id or "*" for dash scope; workspace id for workspace scope. */
11
11
  scope_id: string;
12
12
  }
13
13
  /**
@@ -25,7 +25,7 @@ interface AccessSnapshot {
25
25
  platform_role: string;
26
26
  permissions: AccessEntry[];
27
27
  /**
28
- * Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
28
+ * Dash bundle (`dash.owner` / `dash.admin`) per dash id, mirrored from
29
29
  * the subject's dash membership. This is the admission list for the dash
30
30
  * area: the dashes this subject runs. What they may *do* on one is a normal
31
31
  * `hasPermission(snapshot, key, { dashId })` check — platform managers hold
@@ -41,7 +41,7 @@ interface AccessSnapshot {
41
41
  interface PermissionCheckOptions {
42
42
  /**
43
43
  * Dash slug the check is about. Omit for "anywhere": platform
44
- * entries and any saas_app-scoped entry satisfy the check.
44
+ * entries and any dash-scoped entry satisfy the check.
45
45
  */
46
46
  dashId?: string;
47
47
  }
@@ -8,7 +8,7 @@ function entryCoversApp(entry, dashId) {
8
8
  switch (entry.scope_type) {
9
9
  case "platform":
10
10
  return true;
11
- case "saas_app":
11
+ case "dash_id":
12
12
  if (dashId === void 0) return true;
13
13
  return entry.scope_id === "*" || entry.scope_id.toLowerCase() === dashId.toLowerCase();
14
14
  default:
@@ -1,5 +1,5 @@
1
1
  /** Platform-operator permission keys. */
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.config.manage", "platform.docs.manage", "platform.docs.view", "platform.mcp.access", "platform.mcp.tokens.manage", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.ops.view", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.tenants.view", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
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.config.manage", "platform.docs.manage", "platform.docs.view", "platform.mcp.access", "platform.mcp.tokens.manage", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.ops.view", "platform.service_accounts.manage", "platform.service_accounts.view", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.tenants.view", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "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
5
  declare const DASH_PERMISSIONS: readonly ["dash.apps.manage", "dash.billing.manage", "dash.billing.view", "dash.channels.manage", "dash.customers.manage", "dash.domains.manage", "dash.domains.view", "dash.members.manage", "dash.ownership.transfer", "dash.plans.manage", "dash.plans.view", "dash.publish", "dash.settings.manage", "dash.workspaces.admin", "dash.workspaces.manage", "dash.workspaces.view"];
@@ -39,12 +39,12 @@ declare class AccessSnapshotError extends Error {
39
39
  declare function isAccessSnapshotError(error: unknown, code?: AccessErrorCode): error is AccessSnapshotError;
40
40
 
41
41
  /** Scope a permission entry applies to. Mirrors c1m-server authz.Scope*. */
42
- type AccessScopeType = "platform" | "saas_app" | "workspace";
42
+ type AccessScopeType = "platform" | "dash_id" | "workspace";
43
43
  interface AccessEntry {
44
44
  /** Permission key, e.g. "platform.users.manage_access". */
45
45
  key: string;
46
46
  scope_type: AccessScopeType;
47
- /** "" for platform scope; saas_app slug or "*" for saas_app scope; workspace id for workspace scope. */
47
+ /** "" for platform scope; dash id or "*" for dash scope; workspace id for workspace scope. */
48
48
  scope_id: string;
49
49
  }
50
50
  /**
@@ -62,7 +62,7 @@ interface AccessSnapshot {
62
62
  platform_role: string;
63
63
  permissions: AccessEntry[];
64
64
  /**
65
- * Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
65
+ * Dash bundle (`dash.owner` / `dash.admin`) per dash id, mirrored from
66
66
  * the subject's dash membership. This is the admission list for the dash
67
67
  * area: the dashes this subject runs. What they may *do* on one is a normal
68
68
  * `hasPermission(snapshot, key, { dashId })` check — platform managers hold
@@ -81,13 +81,13 @@ declare function parseAccessSnapshot(input: unknown): AccessSnapshot;
81
81
  interface PermissionCheckOptions {
82
82
  /**
83
83
  * Dash slug the check is about. Omit for "anywhere": platform
84
- * entries and any saas_app-scoped entry satisfy the check.
84
+ * entries and any dash-scoped entry satisfy the check.
85
85
  */
86
86
  dashId?: string;
87
87
  }
88
88
  /**
89
89
  * Mirrors c1m-server authz.entryCoversApp: platform scope covers every app;
90
- * saas_app scope covers "*" or a case-insensitive slug match; workspace-scoped
90
+ * Dash scope covers "*" or a case-insensitive dash-id match; workspace-scoped
91
91
  * entries never satisfy an app-level check.
92
92
  */
93
93
  declare function entryCoversApp(entry: AccessEntry, dashId: string | undefined): boolean;
@@ -98,7 +98,7 @@ declare function hasAllPermissions(snapshot: AccessSnapshot, keys: readonly stri
98
98
  /**
99
99
  * Workspace-staff check. Platform owners always pass (they own every
100
100
  * workspace); otherwise the workspace's effective permission list decides.
101
- * Platform/saas_app entries never confer workspace-staff permissions.
101
+ * Platform/dash entries never confer workspace-staff permissions.
102
102
  */
103
103
  declare function hasWorkspacePermission(snapshot: AccessSnapshot, workspaceId: string, key: string): boolean;
104
104
  /**
@@ -0,0 +1,146 @@
1
+ declare const SERVICE_ACCOUNTS_VIEW_PERMISSION: "platform.service_accounts.view";
2
+ declare const SERVICE_ACCOUNTS_MANAGE_PERMISSION: "platform.service_accounts.manage";
3
+ type ServiceAccountAudience = "platform" | `dash:${string}`;
4
+ type ServiceAccountGrantScope = "platform" | "dash_id";
5
+ type ServiceAccountOperation = "platform.identity.service_accounts.list" | "platform.identity.service_accounts.create" | "platform.identity.service_accounts.get" | "platform.identity.service_accounts.update" | "platform.identity.service_accounts.grants.list" | "platform.identity.service_accounts.grants.replace" | "platform.identity.service_accounts.credentials.list" | "platform.identity.service_accounts.credentials.mint" | "platform.identity.service_accounts.credentials.revoke";
6
+ type ServiceAccountRequestOutcome = "success" | "api_error" | "network_error" | "timeout" | "invalid_response" | "cancelled";
7
+ interface ServiceAccountView {
8
+ id: string;
9
+ name: string;
10
+ purpose: string;
11
+ owner_user_id: string;
12
+ enabled: boolean;
13
+ version: number;
14
+ created_by: string;
15
+ updated_by: string;
16
+ created_at: string;
17
+ updated_at: string;
18
+ }
19
+ interface ServiceAccountEnvelope {
20
+ service_account: ServiceAccountView;
21
+ }
22
+ interface ServiceAccountPage {
23
+ items: ServiceAccountView[];
24
+ next_cursor?: string;
25
+ has_more: boolean;
26
+ }
27
+ interface ServiceAccountGrant {
28
+ grant_key: string;
29
+ scope_type: string;
30
+ scope_id: string;
31
+ id?: string;
32
+ subject_id?: string;
33
+ subject_type?: string;
34
+ granted_by?: string;
35
+ created_at?: string;
36
+ }
37
+ interface ServiceAccountGrantInput {
38
+ grant_key: string;
39
+ scope_type: ServiceAccountGrantScope;
40
+ scope_id: string;
41
+ }
42
+ interface ServiceAccountGrantPage {
43
+ items: ServiceAccountGrant[];
44
+ }
45
+ interface ServiceAccountCredential {
46
+ id: string;
47
+ service_account_id: string;
48
+ name: string;
49
+ audience: ServiceAccountAudience;
50
+ permissions: string[];
51
+ operation_ids: string[];
52
+ dash_ids: string[];
53
+ max_output_bytes: number;
54
+ created_by: string;
55
+ created_at: string;
56
+ expires_at: string;
57
+ last_used_at?: string;
58
+ revoked_at?: string;
59
+ }
60
+ interface ServiceAccountCredentialPage {
61
+ items: ServiceAccountCredential[];
62
+ next_cursor?: string;
63
+ has_more: boolean;
64
+ }
65
+ interface CreateServiceAccountInput {
66
+ name: string;
67
+ purpose: string;
68
+ owner_user_id: string;
69
+ reason: string;
70
+ }
71
+ interface UpdateServiceAccountInput extends CreateServiceAccountInput {
72
+ enabled: boolean;
73
+ expected_version: number;
74
+ }
75
+ interface ReplaceServiceAccountGrantsInput {
76
+ grants: ServiceAccountGrantInput[];
77
+ expected_version: number;
78
+ reason: string;
79
+ }
80
+ interface MintServiceAccountCredentialInput {
81
+ name: string;
82
+ audience: ServiceAccountAudience;
83
+ operation_ids: string[];
84
+ dash_ids: string[];
85
+ expires_in_days: number;
86
+ expected_version: number;
87
+ reason: string;
88
+ max_output_bytes?: number;
89
+ }
90
+ interface MintServiceAccountCredentialResponse {
91
+ credential: ServiceAccountCredential;
92
+ /** One-time presented secret. The server never returns it again. */
93
+ secret: string;
94
+ }
95
+ interface RevokeServiceAccountCredentialInput {
96
+ expected_version: number;
97
+ reason: string;
98
+ }
99
+ interface ServiceAccountListParams {
100
+ cursor?: string;
101
+ limit?: number;
102
+ }
103
+ interface ServiceAccountRequestObservation {
104
+ operation: ServiceAccountOperation;
105
+ method: string;
106
+ outcome: ServiceAccountRequestOutcome;
107
+ status: number;
108
+ requestId: string;
109
+ durationMs: number;
110
+ attempts: number;
111
+ }
112
+ interface CreateServiceAccountClientOptions {
113
+ baseUrl: string;
114
+ getAccessToken: () => Promise<string | null | undefined> | string | null | undefined;
115
+ getHeaders?: () => Promise<HeadersInit | undefined> | HeadersInit | undefined;
116
+ fetch?: typeof globalThis.fetch;
117
+ /** Per attempt (default 10000). */
118
+ timeoutMs?: number;
119
+ /** Additional attempts for GET only (default 1). Mutations are never retried. */
120
+ retryAttempts?: number;
121
+ /** Cap on one retry delay (default 2000). */
122
+ maxRetryDelayMs?: number;
123
+ observe?: (event: ServiceAccountRequestObservation) => void;
124
+ signal?: AbortSignal;
125
+ }
126
+ declare class ServiceAccountAPIError extends Error {
127
+ readonly status: number;
128
+ readonly code: string;
129
+ readonly requestId: string;
130
+ constructor(message: string, status: number, code: string, requestId: string);
131
+ }
132
+ declare function isServiceAccountAPIError(error: unknown): error is ServiceAccountAPIError;
133
+ declare function createServiceAccountClient(options: CreateServiceAccountClientOptions): {
134
+ readonly list: (params?: ServiceAccountListParams) => Promise<ServiceAccountPage>;
135
+ readonly get: (serviceAccountID: string) => Promise<ServiceAccountEnvelope>;
136
+ readonly create: (input: CreateServiceAccountInput) => Promise<ServiceAccountEnvelope>;
137
+ readonly update: (serviceAccountID: string, input: UpdateServiceAccountInput) => Promise<ServiceAccountEnvelope>;
138
+ readonly listGrants: (serviceAccountID: string) => Promise<ServiceAccountGrantPage>;
139
+ readonly replaceGrants: (serviceAccountID: string, input: ReplaceServiceAccountGrantsInput) => Promise<ServiceAccountEnvelope>;
140
+ readonly listCredentials: (serviceAccountID: string, params?: ServiceAccountListParams) => Promise<ServiceAccountCredentialPage>;
141
+ readonly mintCredential: (serviceAccountID: string, input: MintServiceAccountCredentialInput) => Promise<MintServiceAccountCredentialResponse>;
142
+ readonly revokeCredential: (serviceAccountID: string, credentialID: string, input: RevokeServiceAccountCredentialInput) => Promise<ServiceAccountEnvelope>;
143
+ };
144
+ type ServiceAccountClient = ReturnType<typeof createServiceAccountClient>;
145
+
146
+ export { type CreateServiceAccountClientOptions, type CreateServiceAccountInput, type MintServiceAccountCredentialInput, type MintServiceAccountCredentialResponse, type ReplaceServiceAccountGrantsInput, type RevokeServiceAccountCredentialInput, SERVICE_ACCOUNTS_MANAGE_PERMISSION, SERVICE_ACCOUNTS_VIEW_PERMISSION, ServiceAccountAPIError, type ServiceAccountAudience, type ServiceAccountClient, type ServiceAccountCredential, type ServiceAccountCredentialPage, type ServiceAccountEnvelope, type ServiceAccountGrant, type ServiceAccountGrantInput, type ServiceAccountGrantPage, type ServiceAccountGrantScope, type ServiceAccountListParams, type ServiceAccountOperation, type ServiceAccountPage, type ServiceAccountRequestObservation, type ServiceAccountRequestOutcome, type ServiceAccountView, type UpdateServiceAccountInput, createServiceAccountClient, isServiceAccountAPIError };
@@ -0,0 +1,481 @@
1
+ // ../transport/src/index.ts
2
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
3
+ var TransportError = class extends Error {
4
+ constructor(failure) {
5
+ super(failure.message || failure.outcome);
6
+ this.failure = failure;
7
+ this.name = "TransportError";
8
+ }
9
+ failure;
10
+ };
11
+ function isTransportError(error) {
12
+ return error instanceof TransportError || typeof error === "object" && error !== null && error.name === "TransportError";
13
+ }
14
+ function positiveNumber(value, label, fail) {
15
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw fail(`${label} must be a positive finite number`);
16
+ return value;
17
+ }
18
+ function nonNegativeInteger(value, label, fail) {
19
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw fail(`${label} must be a non-negative safe integer`);
20
+ return value;
21
+ }
22
+ function requiredString(value, label, fail) {
23
+ const normalized = typeof value === "string" ? value.trim() : "";
24
+ if (!normalized) throw fail(`${label} is required`);
25
+ return normalized;
26
+ }
27
+ function backoffDelay(attempt, maxDelayMs) {
28
+ const base = Math.min(250 * 2 ** Math.max(attempt, 0), maxDelayMs);
29
+ return Math.floor(Math.random() * base);
30
+ }
31
+ function retryDelay(response, attempt, maxDelayMs) {
32
+ const value = response.headers.get("Retry-After");
33
+ if (!value) return backoffDelay(attempt, maxDelayMs);
34
+ const seconds = Number(value);
35
+ if (Number.isFinite(seconds)) return Math.min(Math.max(seconds * 1e3, 0), maxDelayMs);
36
+ const date = Date.parse(value);
37
+ return Number.isNaN(date) ? backoffDelay(attempt, maxDelayMs) : Math.min(Math.max(date - Date.now(), 0), maxDelayMs);
38
+ }
39
+ function wait(ms) {
40
+ return ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms));
41
+ }
42
+ function combineSignals(...signals) {
43
+ const active = signals.filter((signal) => Boolean(signal));
44
+ if (active.length === 0) return void 0;
45
+ if (active.length === 1) return active[0];
46
+ return AbortSignal.any(active);
47
+ }
48
+ function isRetryableRequest(method, headers, retry) {
49
+ if (method === "GET") return true;
50
+ return retry.idempotentWrites === true && headers.has("Idempotency-Key");
51
+ }
52
+ async function performRequest(transport, url, init, observe, decode) {
53
+ const method = (init.method ?? "GET").toUpperCase();
54
+ const headers = new Headers(init.headers);
55
+ const startedAt = Date.now();
56
+ const retryable = isRetryableRequest(method, headers, transport.retry);
57
+ const maxAttempts = retryable ? transport.retry.attempts : 0;
58
+ let attempts = 0;
59
+ let status = 0;
60
+ let requestId = "";
61
+ let outcome = "network_error";
62
+ const fail = (failure) => {
63
+ outcome = failure.outcome;
64
+ throw new TransportError({ requestId, ...failure });
65
+ };
66
+ try {
67
+ let response;
68
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
69
+ attempts = attempt + 1;
70
+ const controller = new AbortController();
71
+ const timeout = setTimeout(() => controller.abort(), transport.timeoutMs);
72
+ try {
73
+ response = await transport.fetch(url, {
74
+ ...init,
75
+ method,
76
+ headers,
77
+ signal: combineSignals(transport.signal, init.signal, controller.signal)
78
+ });
79
+ } catch {
80
+ if (controller.signal.aborted) fail({ outcome: "timeout", status: 0, code: "", message: "" });
81
+ if (transport.signal?.aborted || init.signal?.aborted) fail({ outcome: "cancelled", status: 0, code: "", message: "" });
82
+ if (attempt < maxAttempts) {
83
+ await wait(backoffDelay(attempt, transport.retry.maxDelayMs));
84
+ continue;
85
+ }
86
+ fail({ outcome: "network_error", status: 0, code: "", message: "" });
87
+ } finally {
88
+ clearTimeout(timeout);
89
+ }
90
+ if (!RETRYABLE_STATUSES.has(response.status) || attempt === maxAttempts) break;
91
+ await wait(retryDelay(response, attempt, transport.retry.maxDelayMs));
92
+ }
93
+ if (!response) return fail({ outcome: "network_error", status: 0, code: "", message: "" });
94
+ status = response.status;
95
+ requestId = response.headers.get("X-Request-ID") ?? "";
96
+ if (!response.ok) {
97
+ const envelope = await response.json().catch(() => ({}));
98
+ if (typeof envelope.request_id === "string") requestId = envelope.request_id;
99
+ return fail({
100
+ outcome: "api_error",
101
+ status,
102
+ code: typeof envelope.code === "string" ? envelope.code : "",
103
+ message: typeof envelope.message === "string" ? envelope.message : "",
104
+ requestId
105
+ });
106
+ }
107
+ let payload;
108
+ try {
109
+ if (status === 204) {
110
+ payload = void 0;
111
+ } else {
112
+ const text = await response.text();
113
+ const parsed = text ? JSON.parse(text) : void 0;
114
+ payload = decode ? decode(parsed) : parsed;
115
+ }
116
+ } catch {
117
+ return fail({ outcome: "invalid_response", status, code: "", message: "", requestId });
118
+ }
119
+ outcome = "success";
120
+ return { payload, status, requestId };
121
+ } finally {
122
+ if (observe) {
123
+ try {
124
+ observe({ method, outcome, status, requestId, durationMs: Math.max(0, Date.now() - startedAt), attempts });
125
+ } catch {
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ // src/service-accounts.ts
132
+ var SERVICE_ACCOUNTS_VIEW_PERMISSION = "platform.service_accounts.view";
133
+ var SERVICE_ACCOUNTS_MANAGE_PERMISSION = "platform.service_accounts.manage";
134
+ var ServiceAccountAPIError = class extends Error {
135
+ constructor(message, status, code, requestId) {
136
+ super(message);
137
+ this.status = status;
138
+ this.code = code;
139
+ this.requestId = requestId;
140
+ this.name = "ServiceAccountAPIError";
141
+ }
142
+ status;
143
+ code;
144
+ requestId;
145
+ };
146
+ function isServiceAccountAPIError(error) {
147
+ return error instanceof ServiceAccountAPIError || typeof error === "object" && error !== null && error.name === "ServiceAccountAPIError" && typeof error.status === "number" && typeof error.code === "string" && typeof error.requestId === "string";
148
+ }
149
+ var basePath = "/api/v1/platform/identity/service-accounts";
150
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
151
+ var credentialIDPattern = /^[0-9a-f]{32}$/;
152
+ var operationIDPattern = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/;
153
+ var timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
154
+ var secretPattern = /^mml_sa_([0-9a-f]{32})_([0-9a-f]{64})$/;
155
+ var configurationError = (message) => new Error(message);
156
+ function invalidResponse() {
157
+ throw new Error("invalid Service Account response");
158
+ }
159
+ function objectValue(value) {
160
+ if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse();
161
+ return value;
162
+ }
163
+ function responseString(value, options = {}) {
164
+ if (value === void 0 && options.optional) return void 0;
165
+ if (typeof value !== "string" || value.length === 0) invalidResponse();
166
+ if (options.pattern && !options.pattern.test(value)) invalidResponse();
167
+ if (options.timestamp && (!timestampPattern.test(value) || !Number.isFinite(Date.parse(value)))) invalidResponse();
168
+ return value;
169
+ }
170
+ function responseStrings(value) {
171
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) {
172
+ invalidResponse();
173
+ }
174
+ return [...value];
175
+ }
176
+ function positiveIntegerResponse(value) {
177
+ if (!Number.isSafeInteger(value) || Number(value) < 1) invalidResponse();
178
+ return Number(value);
179
+ }
180
+ function parseServiceAccount(value) {
181
+ const account = objectValue(value);
182
+ if (typeof account.enabled !== "boolean") invalidResponse();
183
+ return {
184
+ id: responseString(account.id, { pattern: uuidPattern }),
185
+ name: responseString(account.name),
186
+ purpose: responseString(account.purpose),
187
+ owner_user_id: responseString(account.owner_user_id),
188
+ enabled: account.enabled,
189
+ version: positiveIntegerResponse(account.version),
190
+ created_by: responseString(account.created_by),
191
+ updated_by: responseString(account.updated_by),
192
+ created_at: responseString(account.created_at, { timestamp: true }),
193
+ updated_at: responseString(account.updated_at, { timestamp: true })
194
+ };
195
+ }
196
+ function parseServiceAccountEnvelope(value) {
197
+ return { service_account: parseServiceAccount(objectValue(value).service_account) };
198
+ }
199
+ function parsePage(value, parseItem) {
200
+ const page = objectValue(value);
201
+ if (!Array.isArray(page.items) || typeof page.has_more !== "boolean") invalidResponse();
202
+ const nextCursor = responseString(page.next_cursor, { optional: true });
203
+ if (Boolean(nextCursor) !== page.has_more) invalidResponse();
204
+ return { items: page.items.map(parseItem), next_cursor: nextCursor, has_more: page.has_more };
205
+ }
206
+ function parseGrant(value) {
207
+ const grant = objectValue(value);
208
+ return {
209
+ grant_key: responseString(grant.grant_key),
210
+ scope_type: responseString(grant.scope_type),
211
+ scope_id: responseString(grant.scope_id),
212
+ id: responseString(grant.id, { optional: true }),
213
+ subject_id: responseString(grant.subject_id, { optional: true }),
214
+ subject_type: responseString(grant.subject_type, { optional: true }),
215
+ granted_by: responseString(grant.granted_by, { optional: true }),
216
+ created_at: responseString(grant.created_at, { optional: true, timestamp: true })
217
+ };
218
+ }
219
+ function parseCredential(value) {
220
+ const credential = objectValue(value);
221
+ const audience = responseString(credential.audience);
222
+ if (audience !== "platform" && !audience.startsWith("dash:")) invalidResponse();
223
+ return {
224
+ id: responseString(credential.id, { pattern: credentialIDPattern }),
225
+ service_account_id: responseString(credential.service_account_id, { pattern: uuidPattern }),
226
+ name: responseString(credential.name),
227
+ audience,
228
+ permissions: responseStrings(credential.permissions),
229
+ operation_ids: responseStrings(credential.operation_ids),
230
+ dash_ids: responseStrings(credential.dash_ids),
231
+ max_output_bytes: positiveIntegerResponse(credential.max_output_bytes),
232
+ created_by: responseString(credential.created_by),
233
+ created_at: responseString(credential.created_at, { timestamp: true }),
234
+ expires_at: responseString(credential.expires_at, { timestamp: true }),
235
+ last_used_at: responseString(credential.last_used_at, { optional: true, timestamp: true }),
236
+ revoked_at: responseString(credential.revoked_at, { optional: true, timestamp: true })
237
+ };
238
+ }
239
+ function parseMintedCredential(value) {
240
+ const minted = objectValue(value);
241
+ const credential = parseCredential(minted.credential);
242
+ const secret = responseString(minted.secret);
243
+ const match = secret.match(secretPattern);
244
+ if (!match || match[1] !== credential.id) invalidResponse();
245
+ return { credential, secret };
246
+ }
247
+ function canonicalID(value, label, pattern) {
248
+ const id = requiredString(value, label, configurationError);
249
+ if (!pattern.test(id)) throw configurationError(`${label} is invalid`);
250
+ return id;
251
+ }
252
+ function version(value) {
253
+ if (!Number.isSafeInteger(value) || value < 1) throw configurationError("expected_version must be a positive integer");
254
+ return value;
255
+ }
256
+ function reason(value) {
257
+ const normalized = requiredString(value, "reason", configurationError);
258
+ if (normalized.length > 500) throw configurationError("reason must be 500 characters or fewer");
259
+ return normalized;
260
+ }
261
+ function accountFields(input) {
262
+ const name = requiredString(input.name, "name", configurationError);
263
+ const purpose = requiredString(input.purpose, "purpose", configurationError);
264
+ const ownerUserID = requiredString(input.owner_user_id, "owner_user_id", configurationError);
265
+ if (name.length > 64) throw configurationError("name must be 64 characters or fewer");
266
+ if (purpose.length > 500) throw configurationError("purpose must be 500 characters or fewer");
267
+ return { name, purpose, owner_user_id: ownerUserID, reason: reason(input.reason) };
268
+ }
269
+ function listQuery(params) {
270
+ const query = new URLSearchParams();
271
+ const cursor = params.cursor?.trim();
272
+ if (cursor) query.set("cursor", cursor);
273
+ if (params.limit !== void 0) {
274
+ const limit = positiveNumber(params.limit, "limit", configurationError);
275
+ if (!Number.isSafeInteger(limit) || limit > 100) throw configurationError("limit must be an integer from 1 to 100");
276
+ query.set("limit", String(limit));
277
+ }
278
+ return query.size ? `?${query}` : "";
279
+ }
280
+ function grantInput(value) {
281
+ const grantKey = requiredString(value.grant_key, "grant_key", configurationError);
282
+ if (value.scope_type !== "platform" && value.scope_type !== "dash_id") {
283
+ throw configurationError("scope_type must be platform or dash_id");
284
+ }
285
+ const scopeID = requiredString(value.scope_id, "scope_id", configurationError);
286
+ if (value.scope_type === "platform" && scopeID !== "*") {
287
+ throw configurationError('platform grants must use scope_id "*"');
288
+ }
289
+ if (value.scope_type === "dash_id" && scopeID === "*") {
290
+ throw configurationError("service-account Dash grants must name one Dash");
291
+ }
292
+ return { grant_key: grantKey, scope_type: value.scope_type, scope_id: scopeID };
293
+ }
294
+ function mintInput(value) {
295
+ const name = requiredString(value.name, "name", configurationError);
296
+ if (name.length > 64) throw configurationError("name must be 64 characters or fewer");
297
+ const audience = requiredString(value.audience, "audience", configurationError);
298
+ if (audience !== "platform" && !audience.startsWith("dash:")) {
299
+ throw configurationError("audience must be platform or dash:<dash_id>");
300
+ }
301
+ const operationIDs = [...new Set(value.operation_ids.map((item) => requiredString(item, "operation_id", configurationError)))];
302
+ if (operationIDs.length < 1 || operationIDs.length > 64 || operationIDs.some((item) => !operationIDPattern.test(item))) {
303
+ throw configurationError("operation_ids must contain 1 to 64 stable operation ids");
304
+ }
305
+ const dashIDs = [...new Set(value.dash_ids.map((item) => requiredString(item, "dash_id", configurationError)))];
306
+ if (dashIDs.length > 32 || dashIDs.includes("*")) throw configurationError("dash_ids must name at most 32 explicit Dashes");
307
+ if (audience.startsWith("dash:") && (dashIDs.length !== 1 || dashIDs[0] !== audience.slice(5))) {
308
+ throw configurationError("a Dash audience requires exactly its route Dash in dash_ids");
309
+ }
310
+ if (!Number.isSafeInteger(value.expires_in_days) || value.expires_in_days < 1 || value.expires_in_days > 30) {
311
+ throw configurationError("expires_in_days must be an integer from 1 to 30");
312
+ }
313
+ if (value.max_output_bytes !== void 0 && (!Number.isSafeInteger(value.max_output_bytes) || value.max_output_bytes < 1 || value.max_output_bytes > 1048576)) {
314
+ throw configurationError("max_output_bytes must be an integer from 1 to 1048576");
315
+ }
316
+ return {
317
+ name,
318
+ audience,
319
+ operation_ids: operationIDs,
320
+ dash_ids: dashIDs,
321
+ expires_in_days: value.expires_in_days,
322
+ expected_version: version(value.expected_version),
323
+ reason: reason(value.reason),
324
+ ...value.max_output_bytes === void 0 ? {} : { max_output_bytes: value.max_output_bytes }
325
+ };
326
+ }
327
+ function createServiceAccountClient(options) {
328
+ const baseUrl = requiredString(options.baseUrl, "baseUrl", configurationError).replace(/\/+$/, "");
329
+ const transport = {
330
+ fetch: (input, init) => (options.fetch ?? globalThis.fetch)(input, init),
331
+ timeoutMs: positiveNumber(options.timeoutMs ?? 1e4, "timeoutMs", configurationError),
332
+ retry: {
333
+ attempts: nonNegativeInteger(options.retryAttempts ?? 1, "retryAttempts", configurationError),
334
+ maxDelayMs: positiveNumber(options.maxRetryDelayMs ?? 2e3, "maxRetryDelayMs", configurationError),
335
+ idempotentWrites: false
336
+ },
337
+ signal: options.signal
338
+ };
339
+ async function request(operation, path, decode, init = {}) {
340
+ const method = (init.method ?? "GET").toUpperCase();
341
+ const report = options.observe && ((event) => {
342
+ options.observe?.({ operation, ...event });
343
+ });
344
+ let headers;
345
+ try {
346
+ const accessToken = requiredString(await options.getAccessToken() ?? "", "access token", configurationError);
347
+ headers = new Headers(await options.getHeaders?.());
348
+ new Headers(init.headers).forEach((value, key) => headers.set(key, value));
349
+ headers.set("Authorization", `Bearer ${accessToken}`);
350
+ headers.set("Accept", "application/json");
351
+ if (init.body !== void 0) headers.set("Content-Type", "application/json");
352
+ } catch (error) {
353
+ try {
354
+ report?.({ method, outcome: "network_error", status: 0, requestId: "", durationMs: 0, attempts: 0 });
355
+ } catch {
356
+ }
357
+ throw error;
358
+ }
359
+ try {
360
+ const result = await performRequest(
361
+ transport,
362
+ `${baseUrl}${path}`,
363
+ { ...init, method, headers, cache: "no-store" },
364
+ report,
365
+ decode
366
+ );
367
+ return result.payload;
368
+ } catch (error) {
369
+ if (!isTransportError(error)) throw error;
370
+ const { outcome, status, code, message, requestId } = error.failure;
371
+ switch (outcome) {
372
+ case "timeout":
373
+ throw new ServiceAccountAPIError("Service Account request timed out", 0, "service_account_request_timeout", "");
374
+ case "cancelled":
375
+ throw new ServiceAccountAPIError("Service Account request was cancelled", 0, "service_account_request_cancelled", "");
376
+ case "network_error":
377
+ throw new ServiceAccountAPIError("Service Account service is unavailable", 0, "service_account_service_unavailable", "");
378
+ case "invalid_response":
379
+ throw new ServiceAccountAPIError("Service Account service returned an invalid response", status, "service_account_invalid_response", requestId);
380
+ case "api_error":
381
+ throw new ServiceAccountAPIError(message || code || "Service Account request failed", status, code, requestId);
382
+ }
383
+ }
384
+ }
385
+ function accountPath(serviceAccountID) {
386
+ return `${basePath}/${canonicalID(serviceAccountID, "serviceAccountId", uuidPattern)}`;
387
+ }
388
+ return {
389
+ list(params = {}) {
390
+ return request(
391
+ "platform.identity.service_accounts.list",
392
+ `${basePath}${listQuery(params)}`,
393
+ (payload) => parsePage(payload, parseServiceAccount)
394
+ );
395
+ },
396
+ get(serviceAccountID) {
397
+ return request(
398
+ "platform.identity.service_accounts.get",
399
+ accountPath(serviceAccountID),
400
+ parseServiceAccountEnvelope
401
+ );
402
+ },
403
+ create(input) {
404
+ return request(
405
+ "platform.identity.service_accounts.create",
406
+ basePath,
407
+ parseServiceAccountEnvelope,
408
+ { method: "POST", body: JSON.stringify(accountFields(input)) }
409
+ );
410
+ },
411
+ update(serviceAccountID, input) {
412
+ const fields = accountFields(input);
413
+ return request(
414
+ "platform.identity.service_accounts.update",
415
+ accountPath(serviceAccountID),
416
+ parseServiceAccountEnvelope,
417
+ {
418
+ method: "PUT",
419
+ body: JSON.stringify({ ...fields, enabled: input.enabled, expected_version: version(input.expected_version) })
420
+ }
421
+ );
422
+ },
423
+ listGrants(serviceAccountID) {
424
+ return request(
425
+ "platform.identity.service_accounts.grants.list",
426
+ `${accountPath(serviceAccountID)}/grants`,
427
+ (payload) => {
428
+ const page = objectValue(payload);
429
+ if (!Array.isArray(page.items)) invalidResponse();
430
+ return { items: page.items.map(parseGrant) };
431
+ }
432
+ );
433
+ },
434
+ replaceGrants(serviceAccountID, input) {
435
+ if (!Array.isArray(input.grants) || input.grants.length > 32) {
436
+ throw configurationError("grants must contain at most 32 entries");
437
+ }
438
+ return request(
439
+ "platform.identity.service_accounts.grants.replace",
440
+ `${accountPath(serviceAccountID)}/grants`,
441
+ parseServiceAccountEnvelope,
442
+ {
443
+ method: "PUT",
444
+ body: JSON.stringify({
445
+ grants: input.grants.map(grantInput),
446
+ expected_version: version(input.expected_version),
447
+ reason: reason(input.reason)
448
+ })
449
+ }
450
+ );
451
+ },
452
+ listCredentials(serviceAccountID, params = {}) {
453
+ return request(
454
+ "platform.identity.service_accounts.credentials.list",
455
+ `${accountPath(serviceAccountID)}/credentials${listQuery(params)}`,
456
+ (payload) => parsePage(payload, parseCredential)
457
+ );
458
+ },
459
+ mintCredential(serviceAccountID, input) {
460
+ return request(
461
+ "platform.identity.service_accounts.credentials.mint",
462
+ `${accountPath(serviceAccountID)}/credentials`,
463
+ parseMintedCredential,
464
+ { method: "POST", body: JSON.stringify(mintInput(input)) }
465
+ );
466
+ },
467
+ revokeCredential(serviceAccountID, credentialID, input) {
468
+ return request(
469
+ "platform.identity.service_accounts.credentials.revoke",
470
+ `${accountPath(serviceAccountID)}/credentials/${canonicalID(credentialID, "credentialId", credentialIDPattern)}`,
471
+ parseServiceAccountEnvelope,
472
+ {
473
+ method: "DELETE",
474
+ body: JSON.stringify({ expected_version: version(input.expected_version), reason: reason(input.reason) })
475
+ }
476
+ );
477
+ }
478
+ };
479
+ }
480
+
481
+ export { SERVICE_ACCOUNTS_MANAGE_PERMISSION, SERVICE_ACCOUNTS_VIEW_PERMISSION, ServiceAccountAPIError, createServiceAccountClient, isServiceAccountAPIError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c1m-public/platform-auth",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -24,6 +24,10 @@
24
24
  "types": "./dist/mcp-tokens.d.ts",
25
25
  "default": "./dist/mcp-tokens.js"
26
26
  },
27
+ "./service-accounts": {
28
+ "types": "./dist/service-accounts.d.ts",
29
+ "default": "./dist/service-accounts.js"
30
+ },
27
31
  "./access": {
28
32
  "types": "./dist/access/index.d.ts",
29
33
  "default": "./dist/access/index.js"