@canopy-io/node 0.1.0 → 0.2.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/dist/index.d.cts CHANGED
@@ -22,6 +22,20 @@ interface CursorPagination {
22
22
  type Pagination = OffsetPagination | CursorPagination;
23
23
  declare function isCursorPagination(pagination: Pagination): pagination is CursorPagination;
24
24
  /** A collection response, paginated or not. */
25
+ /**
26
+ * The answer to a conditional read.
27
+ *
28
+ * A discriminated union rather than an optional body, so a caller cannot
29
+ * mistake "nothing changed" for "changed to nothing" — the two are opposite
30
+ * instructions to whatever is holding the cached copy.
31
+ */
32
+ type ConditionalResult<T> = {
33
+ modified: true;
34
+ data: T;
35
+ etag: string | null;
36
+ } | {
37
+ modified: false;
38
+ };
25
39
  interface Collection<T> {
26
40
  items: T[];
27
41
  pagination?: Pagination;
@@ -59,6 +73,14 @@ interface CanopyClientOptions {
59
73
  timeoutMs?: number;
60
74
  /** Retries after the first attempt. Defaults to 2. */
61
75
  maxRetries?: number;
76
+ /**
77
+ * Ceiling on a single wait between attempts. Defaults to 30s.
78
+ *
79
+ * The server's `Retry-After` is honoured up to this, then clamped. Without a
80
+ * cap the wait is whatever a response header asks for, which sits outside
81
+ * `timeoutMs` and can hold a caller far longer than its own budget allows.
82
+ */
83
+ maxBackoffMs?: number;
62
84
  /** Extra headers on every request. */
63
85
  headers?: Record<string, string>;
64
86
  /** Injectable for tests and for runtimes with a non-global fetch. */
@@ -68,17 +90,67 @@ interface RequestOptions {
68
90
  query?: Record<string, string | number | boolean | undefined | null>;
69
91
  body?: unknown;
70
92
  signal?: AbortSignal;
93
+ /**
94
+ * Per-attempt deadline for this call only, overriding the client-wide
95
+ * `timeoutMs`. 0 disables it.
96
+ *
97
+ * The client-wide default suits administrative CRUD. A latency-critical call
98
+ * on a request path — an authorization check on every inbound request — wants
99
+ * a much tighter one, because the total time at risk is this deadline times
100
+ * the attempts, and it is spent holding an inbound request open.
101
+ */
102
+ timeoutMs?: number;
103
+ /**
104
+ * Retries after the first attempt for this call only, overriding the
105
+ * client-wide `maxRetries`. 0 disables retrying.
106
+ *
107
+ * Set this together with `timeoutMs` when a call needs a bounded worst case:
108
+ * the two multiply. A deadline alone still permits `maxRetries + 1` of them
109
+ * back to back, which is the difference between a slow call and a request
110
+ * held open long past the point the answer was useful.
111
+ */
112
+ maxRetries?: number;
113
+ /**
114
+ * Ceiling on a single wait between attempts, for this call only.
115
+ *
116
+ * Set it alongside `timeoutMs` and `maxRetries` when a call needs a real
117
+ * worst case: the deadline bounds an attempt, `maxRetries` bounds how many,
118
+ * and this bounds the waiting in between — which a server's `Retry-After`
119
+ * would otherwise control.
120
+ */
121
+ maxBackoffMs?: number;
122
+ /**
123
+ * Headers for this call only, overriding the client-wide `headers`.
124
+ *
125
+ * This is how the per-request protocol headers are sent: `If-Match`, carrying
126
+ * a resource's current `version` for optimistic concurrency (a stale value
127
+ * answers 409), and `Idempotency-Key`, which makes a replayed bulk create
128
+ * return the original result instead of creating rows twice.
129
+ *
130
+ * The credential is not overridable this way — auth is settled by the client.
131
+ */
132
+ headers?: Record<string, string>;
71
133
  /**
72
134
  * Declares this call safe to repeat, which allows retrying a 5xx.
73
135
  *
74
- * Needed because the OpenAPI document only marks one operation
75
- * `x-canopy-idempotent`, so the spec cannot drive this. GET, HEAD, PUT and
76
- * DELETE are treated as idempotent by HTTP definition; POST is not, and a
77
- * blind retry there can create a second role assignment or a second
78
- * invitation. Set this only when the endpoint genuinely tolerates a repeat.
136
+ * Needed because the generated types carry no idempotency marker, so the
137
+ * spec cannot drive this. GET, HEAD, PUT and DELETE are treated as
138
+ * idempotent by HTTP definition; POST is not, and a blind retry there can
139
+ * create a second role assignment or a second invitation.
140
+ *
141
+ * Set it where a POST is a read in disguise — the permission evaluations
142
+ * take a body and so must be POSTs, but they compute an answer and write
143
+ * nothing. Do not set it on anything that creates.
79
144
  */
80
145
  idempotent?: boolean;
81
146
  }
147
+ /**
148
+ * The per-call knobs a resource method accepts.
149
+ *
150
+ * Deliberately excludes `body` and `query`, which the method itself owns, and
151
+ * `idempotent`, which is a property of the endpoint rather than the call site.
152
+ */
153
+ type CallOptions = Pick<RequestOptions, "signal" | "timeoutMs" | "maxRetries" | "maxBackoffMs" | "headers">;
82
154
  /**
83
155
  * The transport every resource is built on: one place that knows how to
84
156
  * authenticate, unwrap Canopy's response envelope, turn a failure into a typed
@@ -91,6 +163,7 @@ declare class CanopyClient {
91
163
  private readonly baseUrl;
92
164
  private readonly timeoutMs;
93
165
  private readonly maxRetries;
166
+ private readonly maxBackoffMs;
94
167
  private readonly authHeaders;
95
168
  private readonly extraHeaders;
96
169
  private readonly fetchImpl;
@@ -107,6 +180,29 @@ declare class CanopyClient {
107
180
  * `CanopyConnectionError`.
108
181
  */
109
182
  request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
183
+ /**
184
+ * A conditional read: send the validator you already hold, and find out
185
+ * whether anything changed.
186
+ *
187
+ * The sibling of `If-Match`, which this client already sends for optimistic
188
+ * concurrency. `304 Not Modified` is a *success* — it means the copy you
189
+ * have is current — but it is not a 2xx, so `request` would raise it as an
190
+ * error. Hence a separate entry point with a return type that says which
191
+ * happened rather than one that has to be inspected.
192
+ *
193
+ * Use it to hold something expensive and revalidate cheaply — the hierarchy
194
+ * behind local authorization is the case this exists for.
195
+ */
196
+ requestConditional<T>(method: string, path: string, etag: string | undefined, options?: RequestOptions): Promise<ConditionalResult<T>>;
197
+ /**
198
+ * Everything up to the response: retries, backoff, cancellation and the
199
+ * status check, without deciding what the body means.
200
+ *
201
+ * Split out so a conditional read can accept `304` where an ordinary one
202
+ * must not, rather than either duplicating the retry policy or teaching
203
+ * `unwrap` about statuses that carry no body.
204
+ */
205
+ private perform;
110
206
  private shouldRetry;
111
207
  private send;
112
208
  private buildUrl;
@@ -231,6 +327,26 @@ interface paths {
231
327
  patch: operations["ApiPermissionsController_updatePermission"];
232
328
  trace?: never;
233
329
  };
330
+ "/api/v1/permissions/{id}/usage": {
331
+ parameters: {
332
+ query?: never;
333
+ header?: never;
334
+ path?: never;
335
+ cookie?: never;
336
+ };
337
+ /**
338
+ * Get where a permission is used
339
+ * @description Returns the roles that grant a permission, each with the number of distinct identities holding that role, plus `role_count` and the distinct `identity_count` the permission reaches overall. Because a permission is only ever held through a role, this is the full blast radius of deleting it — the listed roles are exactly the ones a delete would strip it from. Deactivated roles are included; `identity_count` is not the sum of the per-role counts, since one identity may hold several granting roles. Returns `404` when no permission with that id exists in the Environment. Requires the `rbac.view_roles` permission.
340
+ */
341
+ get: operations["ApiPermissionsController_getPermissionUsage"];
342
+ put?: never;
343
+ post?: never;
344
+ delete?: never;
345
+ options?: never;
346
+ head?: never;
347
+ patch?: never;
348
+ trace?: never;
349
+ };
234
350
  "/api/v1/permissions/evaluate": {
235
351
  parameters: {
236
352
  query?: never;
@@ -643,6 +759,26 @@ interface paths {
643
759
  patch?: never;
644
760
  trace?: never;
645
761
  };
762
+ "/api/v1/identities/{id}/grants": {
763
+ parameters: {
764
+ query?: never;
765
+ header?: never;
766
+ path?: never;
767
+ cookie?: never;
768
+ };
769
+ /**
770
+ * Get where an identity holds each permission
771
+ * @description Returns, for the current Environment, every permission the identity holds mapped to the hierarchy nodes its grants were made at — the nodes themselves, **not** expanded through their descendants. A grant already means "this node and everything beneath it", so a caller answers a node-scoped question by walking up from the node in question and looking for one of these roots, using a copy of the tree fetched once from `GET /api/v1/nodes` and shared across identities. Answers both scopes without a further call: the permission appearing at all is the Application-wide answer, and the walk is the node-scoped one. Scheduled assignments outside their effective window are excluded. Returns `404` when the identity has no membership in this Environment.
772
+ */
773
+ get: operations["ApiIdentitiesController_getIdentityGrants"];
774
+ put?: never;
775
+ post?: never;
776
+ delete?: never;
777
+ options?: never;
778
+ head?: never;
779
+ patch?: never;
780
+ trace?: never;
781
+ };
646
782
  "/api/v1/identity-invites": {
647
783
  parameters: {
648
784
  query?: never;
@@ -771,6 +907,26 @@ interface paths {
771
907
  patch?: never;
772
908
  trace?: never;
773
909
  };
910
+ "/api/v1/nodes/parents": {
911
+ parameters: {
912
+ query?: never;
913
+ header?: never;
914
+ path?: never;
915
+ cookie?: never;
916
+ };
917
+ /**
918
+ * List the hierarchy as parent edges
919
+ * @description Returns the same hierarchy `GET /api/v1/nodes` describes, scoped identically, but as a flat list of `{ id, parent_node_id }` and nothing else. Intended for a client that evaluates authorization locally: it walks upward from a node and reads none of the tree's names, statuses, access flags or counts. At fifty thousand nodes the tree is roughly 18.6 MB against 4 MB of edges, on a read that runs at every consuming process's startup. Supports the same `If-None-Match` revalidation, with a validator distinct from the tree's — a tag from one representation never satisfies a request for the other.
920
+ */
921
+ get: operations["ApiNodesController_listNodeParents"];
922
+ put?: never;
923
+ post?: never;
924
+ delete?: never;
925
+ options?: never;
926
+ head?: never;
927
+ patch?: never;
928
+ trace?: never;
929
+ };
774
930
  "/api/v1/nodes/{id}": {
775
931
  parameters: {
776
932
  query?: never;
@@ -1462,6 +1618,24 @@ interface components {
1462
1618
  /** @description One or more permissions to register */
1463
1619
  permissions: components["schemas"]["PermissionItemDto"][];
1464
1620
  };
1621
+ PermissionUsageRoleDto: {
1622
+ id: string;
1623
+ name: string;
1624
+ description?: string | null;
1625
+ is_system_role: boolean;
1626
+ /** @description Deactivated roles still carry the grant, so they are listed too — deleting the permission strips it from them as well. */
1627
+ is_active: boolean;
1628
+ /** @description Number of distinct identities assigned this role, across all nodes. */
1629
+ member_count: number;
1630
+ };
1631
+ PermissionUsageDto: {
1632
+ permission_id: string;
1633
+ /** @description Number of roles that grant this permission. */
1634
+ role_count: number;
1635
+ /** @description Distinct identities that hold this permission through any granting role. Lower than the sum of `member_count` when an identity holds more than one granting role. */
1636
+ identity_count: number;
1637
+ roles: components["schemas"]["PermissionUsageRoleDto"][];
1638
+ };
1465
1639
  UpdatePermissionDto: {
1466
1640
  /** @description Updated name */
1467
1641
  name?: string;
@@ -1470,18 +1644,6 @@ interface components {
1470
1644
  /** @description Updated category */
1471
1645
  category?: string;
1472
1646
  };
1473
- EvaluatePermissionDto: {
1474
- /** @description Identity ID (from the `identities` table — the end user being evaluated, not an admin). */
1475
- identity_id: string;
1476
- permission: string;
1477
- /**
1478
- * @description Required. `node` asks 'does this identity have the permission *at* `node_id`?' (lineage walk). `app_wide` asks the coarse-grained 'does this identity have the permission *anywhere* in the org?' question — useful for UI gating, **never** for resource-scoped enforcement. `node` requires `node_id`; `app_wide` forbids it.
1479
- * @enum {string}
1480
- */
1481
- scope: "node" | "app_wide";
1482
- /** @description Required when `scope` is `node`; must be omitted when `scope` is `app_wide`. */
1483
- node_id?: string;
1484
- };
1485
1647
  EvaluateResponseDto: {
1486
1648
  allowed: boolean;
1487
1649
  permission: string;
@@ -1495,6 +1657,18 @@ interface components {
1495
1657
  granting_roles: string[];
1496
1658
  denial_reason?: string | null;
1497
1659
  };
1660
+ EvaluatePermissionDto: {
1661
+ /** @description Identity ID (from the `identities` table — the end user being evaluated, not an admin). */
1662
+ identity_id: string;
1663
+ permission: string;
1664
+ /**
1665
+ * @description Required. `node` asks 'does this identity have the permission *at* `node_id`?' (lineage walk). `app_wide` asks the coarse-grained 'does this identity have the permission *anywhere* in the org?' question — useful for UI gating, **never** for resource-scoped enforcement. `node` requires `node_id`; `app_wide` forbids it.
1666
+ * @enum {string}
1667
+ */
1668
+ scope: "node" | "app_wide";
1669
+ /** @description Required when `scope` is `node`; must be omitted when `scope` is `app_wide`. */
1670
+ node_id?: string;
1671
+ };
1498
1672
  EvaluateCheckDto: {
1499
1673
  /** @description Identity ID (from the `identities` table — the end user being evaluated, not an admin). */
1500
1674
  identity_id: string;
@@ -1510,13 +1684,6 @@ interface components {
1510
1684
  BulkEvaluatePermissionDto: {
1511
1685
  checks: components["schemas"]["EvaluateCheckDto"][];
1512
1686
  };
1513
- ExplainPermissionDto: {
1514
- /** @description Identity ID (from the `identities` table — the end user being explained, not an admin). */
1515
- identity_id: string;
1516
- permission: string;
1517
- /** @description The hierarchy node to explain the decision at. The lineage from the root to this node is walked and returned in the trace. */
1518
- node_id: string;
1519
- };
1520
1687
  PermissionTraceAssignmentDto: {
1521
1688
  assignment_id: string;
1522
1689
  role_id: string;
@@ -1552,6 +1719,13 @@ interface components {
1552
1719
  /** @description Root-first lineage of the target node, each with the identity's assignments observed at that node. */
1553
1720
  lineage: components["schemas"]["PermissionTraceNodeDto"][];
1554
1721
  };
1722
+ ExplainPermissionDto: {
1723
+ /** @description Identity ID (from the `identities` table — the end user being explained, not an admin). */
1724
+ identity_id: string;
1725
+ permission: string;
1726
+ /** @description The hierarchy node to explain the decision at. The lineage from the root to this node is walked and returned in the trace. */
1727
+ node_id: string;
1728
+ };
1555
1729
  PageMetaDto: {
1556
1730
  /** @description Current page number (1-based) */
1557
1731
  page: number;
@@ -1584,6 +1758,10 @@ interface components {
1584
1758
  name: string;
1585
1759
  node_id: string;
1586
1760
  node_name: string;
1761
+ /** @description When this assignment starts; null means it is already active */
1762
+ effective_from: string | null;
1763
+ /** @description When this assignment expires; null means it never does */
1764
+ effective_to: string | null;
1587
1765
  };
1588
1766
  IdentityRowDto: {
1589
1767
  id: string;
@@ -1714,6 +1892,12 @@ interface components {
1714
1892
  /** Format: date-time */
1715
1893
  updated_at: string;
1716
1894
  };
1895
+ IdentityGrantResponseDto: {
1896
+ /** @example reports.view */
1897
+ permission: string;
1898
+ /** @description Hierarchy node ids the permission was granted at. Not expanded through descendants. */
1899
+ nodes: string[];
1900
+ };
1717
1901
  IdentityInviteResponseDto: {
1718
1902
  id: string;
1719
1903
  email: string;
@@ -1749,29 +1933,6 @@ interface components {
1749
1933
  expired_count: number;
1750
1934
  revoked_count: number;
1751
1935
  };
1752
- CreateIdentityInviteDto: {
1753
- /** @description OAuth client ID — determines which app the invite links to. If omitted, uses Canopy hosted fallback. */
1754
- client_id?: string;
1755
- /**
1756
- * @description Optional. `activate` (default) creates a net-new identity OR — if an identity with this email already exists in the Account but has no active membership in this App — auto-derives an `add_to_app` invite that adds them to this App without touching their existing password. `password_reset` is the explicit admin-driven credential-rotation flow for an existing identity; it cannot carry a role/node assignment. The legacy `onboard` value is accepted and treated as `activate`.
1757
- * @enum {string}
1758
- */
1759
- intent?: "activate" | "password_reset" | "onboard";
1760
- email: string;
1761
- /** @description Required for `activate`. Ignored for `add_to_app` (the existing identity's name wins) and for `password_reset`. */
1762
- first_name?: string;
1763
- /** @description Required for `activate`. Ignored for `add_to_app` and `password_reset`. */
1764
- last_name?: string;
1765
- /** @description Role ID — required if node_id is provided */
1766
- role_id?: string;
1767
- /** @description Node ID — required if role_id is provided */
1768
- node_id?: string;
1769
- /**
1770
- * @description Whether Canopy should send the invite email. Set false to suppress delivery and handle it yourself — the API response includes accept_url with the tokenized link. Defaults to true.
1771
- * @default true
1772
- */
1773
- send_email: boolean;
1774
- };
1775
1936
  ApiIdentityInviteResponseDto: {
1776
1937
  id: string;
1777
1938
  email: string;
@@ -1800,6 +1961,29 @@ interface components {
1800
1961
  /** @description Tokenized URL the invitee would land on. Returned so callers that pass send_email=false can deliver it themselves. */
1801
1962
  accept_url: string;
1802
1963
  };
1964
+ CreateIdentityInviteDto: {
1965
+ /** @description OAuth client ID — determines which app the invite links to. If omitted, uses Canopy hosted fallback. */
1966
+ client_id?: string;
1967
+ /**
1968
+ * @description Optional. `activate` (default) creates a net-new identity OR — if an identity with this email already exists in the Account but has no active membership in this App — auto-derives an `add_to_app` invite that adds them to this App without touching their existing password. `password_reset` is the explicit admin-driven credential-rotation flow for an existing identity; it cannot carry a role/node assignment. The legacy `onboard` value is accepted and treated as `activate`.
1969
+ * @enum {string}
1970
+ */
1971
+ intent?: "activate" | "password_reset" | "onboard";
1972
+ email: string;
1973
+ /** @description Required for `activate`. Ignored for `add_to_app` (the existing identity's name wins) and for `password_reset`. */
1974
+ first_name?: string;
1975
+ /** @description Required for `activate`. Ignored for `add_to_app` and `password_reset`. */
1976
+ last_name?: string;
1977
+ /** @description Role ID — required if node_id is provided */
1978
+ role_id?: string;
1979
+ /** @description Node ID — required if role_id is provided */
1980
+ node_id?: string;
1981
+ /**
1982
+ * @description Whether Canopy should send the invite email. Set false to suppress delivery and handle it yourself — the API response includes accept_url with the tokenized link. Defaults to true.
1983
+ * @default true
1984
+ */
1985
+ send_email: boolean;
1986
+ };
1803
1987
  BulkCreateIdentityInvitesDto: {
1804
1988
  invites: components["schemas"]["CreateIdentityInviteDto"][];
1805
1989
  };
@@ -1808,20 +1992,6 @@ interface components {
1808
1992
  /** @description Tokenized URL for the regenerated invite. Resend rotates the token, which invalidates any accept_url returned from the original create call. Self-delivery callers must replace the stored URL with this one. */
1809
1993
  accept_url: string;
1810
1994
  };
1811
- CreateNodeDto: {
1812
- /** @description Parent node ID (null for root) */
1813
- parent_node_id?: string;
1814
- /** @description Node type (org-defined, e.g. 'department', 'team') */
1815
- node_type: string;
1816
- /** @description Display name for the node */
1817
- name: string;
1818
- /** @description Optional free-text description shown under the node name */
1819
- description?: string;
1820
- /** @description URL-friendly slug (auto-generated if omitted) */
1821
- slug?: string;
1822
- /** @description Arbitrary metadata */
1823
- metadata?: Record<string, unknown>;
1824
- };
1825
1995
  NodeResponseDto: {
1826
1996
  id: string;
1827
1997
  application_id: string;
@@ -1840,6 +2010,20 @@ interface components {
1840
2010
  /** @description Optimistic-lock version. Send back as the `If-Match` header when updating, moving, or deleting to detect concurrent edits. */
1841
2011
  version: number;
1842
2012
  };
2013
+ CreateNodeDto: {
2014
+ /** @description Parent node ID (null for root) */
2015
+ parent_node_id?: string;
2016
+ /** @description Node type (org-defined, e.g. 'department', 'team') */
2017
+ node_type: string;
2018
+ /** @description Display name for the node */
2019
+ name: string;
2020
+ /** @description Optional free-text description shown under the node name */
2021
+ description?: string;
2022
+ /** @description URL-friendly slug (auto-generated if omitted) */
2023
+ slug?: string;
2024
+ /** @description Arbitrary metadata */
2025
+ metadata?: Record<string, unknown>;
2026
+ };
1843
2027
  HierarchyTreeNodeDto: {
1844
2028
  id: string;
1845
2029
  name: string;
@@ -1871,6 +2055,11 @@ interface components {
1871
2055
  tree: components["schemas"]["HierarchyTreeNodeDto"][];
1872
2056
  scope: components["schemas"]["HierarchyScopeDto"];
1873
2057
  };
2058
+ NodeParentEdgeResponseDto: {
2059
+ id: string;
2060
+ /** @description Null for the root node, which has no parent. */
2061
+ parent_node_id?: string | null;
2062
+ };
1874
2063
  NodeAccessResponseDto: {
1875
2064
  id: string;
1876
2065
  application_id: string;
@@ -2074,6 +2263,44 @@ interface components {
2074
2263
  /** @description Whether this row should appear on a future end-user `My security activity` surface. Not consumed by current reads. */
2075
2264
  identity_visible: boolean;
2076
2265
  };
2266
+ ExportJobDto: {
2267
+ /** Format: uuid */
2268
+ id: string;
2269
+ /**
2270
+ * @description `pending` (enqueued), `processing` (worker rendering), `completed` (file ready), or `failed` (see `error`).
2271
+ * @enum {string}
2272
+ */
2273
+ status: "pending" | "processing" | "completed" | "failed";
2274
+ /**
2275
+ * @description The audit surface the export was taken from.
2276
+ * @enum {string}
2277
+ */
2278
+ surface: "admin" | "identities";
2279
+ /**
2280
+ * @description Wire format of the rendered file.
2281
+ * @enum {string}
2282
+ */
2283
+ format: "csv" | "ndjson";
2284
+ /** Format: uuid */
2285
+ application_id?: string | null;
2286
+ /** Format: uuid */
2287
+ environment_id?: string | null;
2288
+ /** @description Rows written, set once the job completes. */
2289
+ row_count?: number | null;
2290
+ /** @description Failure detail, present only when `status` is `failed`. */
2291
+ error?: string | null;
2292
+ /** Format: date-time */
2293
+ created_at: string;
2294
+ /** Format: date-time */
2295
+ completed_at?: string | null;
2296
+ /**
2297
+ * Format: date-time
2298
+ * @description When the stored file + this record become eligible for sweep.
2299
+ */
2300
+ expires_at?: string | null;
2301
+ /** @description Short-lived signed download URL. Present only on the single-job read of a completed job; `null` otherwise. */
2302
+ download_url?: string | null;
2303
+ };
2077
2304
  AuditExportQueryDto: {
2078
2305
  /**
2079
2306
  * Format: date-time
@@ -2116,49 +2343,11 @@ interface components {
2116
2343
  /** @description Full-text query against actor_label, resource_label (trigram), and metadata (GIN). */
2117
2344
  q?: string;
2118
2345
  /**
2119
- * @description Export wire format. `csv` for spreadsheet review (metadata folded into one JSON column); `ndjson` for SIEM ingestion (one JSON object per line, metadata nested). Defaults to `csv`.
2120
- * @default csv
2121
- * @enum {string}
2122
- */
2123
- format: "csv" | "ndjson";
2124
- };
2125
- ExportJobDto: {
2126
- /** Format: uuid */
2127
- id: string;
2128
- /**
2129
- * @description `pending` (enqueued), `processing` (worker rendering), `completed` (file ready), or `failed` (see `error`).
2130
- * @enum {string}
2131
- */
2132
- status: "pending" | "processing" | "completed" | "failed";
2133
- /**
2134
- * @description The audit surface the export was taken from.
2135
- * @enum {string}
2136
- */
2137
- surface: "admin" | "identities";
2138
- /**
2139
- * @description Wire format of the rendered file.
2346
+ * @description Export wire format. `csv` for spreadsheet review (metadata folded into one JSON column); `ndjson` for SIEM ingestion (one JSON object per line, metadata nested). Defaults to `csv`.
2347
+ * @default csv
2140
2348
  * @enum {string}
2141
2349
  */
2142
2350
  format: "csv" | "ndjson";
2143
- /** Format: uuid */
2144
- application_id?: string | null;
2145
- /** Format: uuid */
2146
- environment_id?: string | null;
2147
- /** @description Rows written, set once the job completes. */
2148
- row_count?: number | null;
2149
- /** @description Failure detail, present only when `status` is `failed`. */
2150
- error?: string | null;
2151
- /** Format: date-time */
2152
- created_at: string;
2153
- /** Format: date-time */
2154
- completed_at?: string | null;
2155
- /**
2156
- * Format: date-time
2157
- * @description When the stored file + this record become eligible for sweep.
2158
- */
2159
- expires_at?: string | null;
2160
- /** @description Short-lived signed download URL. Present only on the single-job read of a completed job; `null` otherwise. */
2161
- download_url?: string | null;
2162
2351
  };
2163
2352
  AuditLogDetailResponseDto: {
2164
2353
  id: string;
@@ -2252,11 +2441,6 @@ interface components {
2252
2441
  /** @description Node type used when the root node is auto-created. Must be one of `node_types`. */
2253
2442
  root_node_type: string;
2254
2443
  };
2255
- CreateWebhookDto: {
2256
- url: string;
2257
- event_types: string[];
2258
- description?: string;
2259
- };
2260
2444
  WebhookCreatedResponseDto: {
2261
2445
  id: string;
2262
2446
  /** @enum {string} */
@@ -2270,6 +2454,11 @@ interface components {
2270
2454
  /** @description HMAC secret — only shown once on creation */
2271
2455
  secret: string;
2272
2456
  };
2457
+ CreateWebhookDto: {
2458
+ url: string;
2459
+ event_types: string[];
2460
+ description?: string;
2461
+ };
2273
2462
  WebhookResponseDto: {
2274
2463
  id: string;
2275
2464
  /** @enum {string} */
@@ -2305,21 +2494,6 @@ interface components {
2305
2494
  description?: string | null;
2306
2495
  is_active?: boolean;
2307
2496
  };
2308
- CreateApiKeyDto: {
2309
- /** @description API key name */
2310
- name: string;
2311
- /** @description API key description */
2312
- description?: string;
2313
- /**
2314
- * @description Required. `scoped` enforces the `scopes` array on every authorization check (deny if the requested permission isn't listed). `full_access` bypasses RBAC entirely within the key's Application — every permission is granted. Pick `scoped` whenever possible; `full_access` should be a deliberate choice (use cases: bootstrap automation, trusted backend services that legitimately need App-wide access). `scoped` requires a non-empty `scopes` array; `full_access` forbids `scopes`.
2315
- * @enum {string}
2316
- */
2317
- access_mode: "scoped" | "full_access";
2318
- /** @description Permission scopes this key is authorized for. Required and must be non-empty when `access_mode` is `scoped`. Must be omitted when `access_mode` is `full_access`. */
2319
- scopes?: string[];
2320
- /** @description Expiration date (ISO 8601). Omit for no expiration. */
2321
- expires_at?: string;
2322
- };
2323
2497
  ApiKeyCreatedResponseDto: {
2324
2498
  id: string;
2325
2499
  name: string;
@@ -2338,6 +2512,21 @@ interface components {
2338
2512
  /** Format: date-time */
2339
2513
  created_at: string;
2340
2514
  };
2515
+ CreateApiKeyDto: {
2516
+ /** @description API key name */
2517
+ name: string;
2518
+ /** @description API key description */
2519
+ description?: string;
2520
+ /**
2521
+ * @description Required. `scoped` enforces the `scopes` array on every authorization check (deny if the requested permission isn't listed). `full_access` bypasses RBAC entirely within the key's Application — every permission is granted. Pick `scoped` whenever possible; `full_access` should be a deliberate choice (use cases: bootstrap automation, trusted backend services that legitimately need App-wide access). `scoped` requires a non-empty `scopes` array; `full_access` forbids `scopes`.
2522
+ * @enum {string}
2523
+ */
2524
+ access_mode: "scoped" | "full_access";
2525
+ /** @description Permission scopes this key is authorized for. Required and must be non-empty when `access_mode` is `scoped`. Must be omitted when `access_mode` is `full_access`. */
2526
+ scopes?: string[];
2527
+ /** @description Expiration date (ISO 8601). Omit for no expiration. */
2528
+ expires_at?: string;
2529
+ };
2341
2530
  ApiKeyResponseDto: {
2342
2531
  id: string;
2343
2532
  client_id: string;
@@ -2498,7 +2687,9 @@ interface operations {
2498
2687
  [name: string]: unknown;
2499
2688
  };
2500
2689
  content: {
2501
- "application/json": components["schemas"]["PermissionResponseDto"];
2690
+ "application/json": {
2691
+ data: components["schemas"]["PermissionResponseDto"];
2692
+ };
2502
2693
  };
2503
2694
  };
2504
2695
  /** @description Invalid or expired token */
@@ -2562,7 +2753,9 @@ interface operations {
2562
2753
  [name: string]: unknown;
2563
2754
  };
2564
2755
  content: {
2565
- "application/json": components["schemas"]["PermissionResponseDto"];
2756
+ "application/json": {
2757
+ data: components["schemas"]["PermissionResponseDto"];
2758
+ };
2566
2759
  };
2567
2760
  };
2568
2761
  /** @description Invalid or expired token */
@@ -2721,7 +2914,9 @@ interface operations {
2721
2914
  [name: string]: unknown;
2722
2915
  };
2723
2916
  content: {
2724
- "application/json": components["schemas"]["PermissionResponseDto"];
2917
+ "application/json": {
2918
+ data: components["schemas"]["PermissionResponseDto"];
2919
+ };
2725
2920
  };
2726
2921
  };
2727
2922
  /** @description Invalid or expired token */
@@ -2789,6 +2984,93 @@ interface operations {
2789
2984
  };
2790
2985
  };
2791
2986
  };
2987
+ ApiPermissionsController_getPermissionUsage: {
2988
+ parameters: {
2989
+ query?: never;
2990
+ header?: never;
2991
+ path: {
2992
+ id: string;
2993
+ };
2994
+ cookie?: never;
2995
+ };
2996
+ requestBody?: never;
2997
+ responses: {
2998
+ /** @description Granting roles and identity reach returned */
2999
+ 200: {
3000
+ headers: {
3001
+ [name: string]: unknown;
3002
+ };
3003
+ content: {
3004
+ "application/json": {
3005
+ data: components["schemas"]["PermissionUsageDto"];
3006
+ };
3007
+ };
3008
+ };
3009
+ /** @description Invalid or expired token */
3010
+ 401: {
3011
+ headers: {
3012
+ [name: string]: unknown;
3013
+ };
3014
+ content: {
3015
+ /**
3016
+ * @example {
3017
+ * "error": {
3018
+ * "statusCode": 401,
3019
+ * "code": null,
3020
+ * "message": "Invalid or expired token",
3021
+ * "timestamp": "2026-04-20T12:00:00.000Z",
3022
+ * "path": "/api/v1/permissions",
3023
+ * "method": "GET"
3024
+ * }
3025
+ * }
3026
+ */
3027
+ "application/json": components["schemas"]["ErrorResponseDto"];
3028
+ };
3029
+ };
3030
+ /** @description This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa) */
3031
+ 403: {
3032
+ headers: {
3033
+ [name: string]: unknown;
3034
+ };
3035
+ content: {
3036
+ /**
3037
+ * @example {
3038
+ * "error": {
3039
+ * "statusCode": 403,
3040
+ * "code": null,
3041
+ * "message": "This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)",
3042
+ * "timestamp": "2026-04-20T12:00:00.000Z",
3043
+ * "path": "/api/v1/permissions",
3044
+ * "method": "GET"
3045
+ * }
3046
+ * }
3047
+ */
3048
+ "application/json": components["schemas"]["ErrorResponseDto"];
3049
+ };
3050
+ };
3051
+ /** @description No permission with that id exists in this Environment */
3052
+ 404: {
3053
+ headers: {
3054
+ [name: string]: unknown;
3055
+ };
3056
+ content: {
3057
+ /**
3058
+ * @example {
3059
+ * "error": {
3060
+ * "statusCode": 404,
3061
+ * "code": null,
3062
+ * "message": "No permission with that id exists in this Environment",
3063
+ * "timestamp": "2026-04-20T12:00:00.000Z",
3064
+ * "path": "/api/v1/permissions/{id}/usage",
3065
+ * "method": "GET"
3066
+ * }
3067
+ * }
3068
+ */
3069
+ "application/json": components["schemas"]["ErrorResponseDto"];
3070
+ };
3071
+ };
3072
+ };
3073
+ };
2792
3074
  ApiPermissionsController_evaluate: {
2793
3075
  parameters: {
2794
3076
  query?: never;
@@ -2808,7 +3090,9 @@ interface operations {
2808
3090
  [name: string]: unknown;
2809
3091
  };
2810
3092
  content: {
2811
- "application/json": components["schemas"]["EvaluateResponseDto"];
3093
+ "application/json": {
3094
+ data: components["schemas"]["EvaluateResponseDto"];
3095
+ };
2812
3096
  };
2813
3097
  };
2814
3098
  /** @description Invalid or expired token */
@@ -2942,7 +3226,9 @@ interface operations {
2942
3226
  [name: string]: unknown;
2943
3227
  };
2944
3228
  content: {
2945
- "application/json": components["schemas"]["PermissionTraceDto"];
3229
+ "application/json": {
3230
+ data: components["schemas"]["PermissionTraceDto"];
3231
+ };
2946
3232
  };
2947
3233
  };
2948
3234
  /** @description Invalid or expired token */
@@ -3105,7 +3391,9 @@ interface operations {
3105
3391
  [name: string]: unknown;
3106
3392
  };
3107
3393
  content: {
3108
- "application/json": components["schemas"]["IdentityResponseDto"];
3394
+ "application/json": {
3395
+ data: components["schemas"]["IdentityResponseDto"];
3396
+ };
3109
3397
  };
3110
3398
  };
3111
3399
  /** @description Password rejected — appeared in a known data breach (HaveIBeenPwned check) */
@@ -3289,7 +3577,9 @@ interface operations {
3289
3577
  [name: string]: unknown;
3290
3578
  };
3291
3579
  content: {
3292
- "application/json": components["schemas"]["IdentitiesSummaryDto"];
3580
+ "application/json": {
3581
+ data: components["schemas"]["IdentitiesSummaryDto"];
3582
+ };
3293
3583
  };
3294
3584
  };
3295
3585
  /** @description Invalid or expired token */
@@ -3477,7 +3767,9 @@ interface operations {
3477
3767
  [name: string]: unknown;
3478
3768
  };
3479
3769
  content: {
3480
- "application/json": components["schemas"]["IdentityResponseDto"];
3770
+ "application/json": {
3771
+ data: components["schemas"]["IdentityResponseDto"];
3772
+ };
3481
3773
  };
3482
3774
  };
3483
3775
  /** @description Invalid or expired token */
@@ -3649,7 +3941,9 @@ interface operations {
3649
3941
  [name: string]: unknown;
3650
3942
  };
3651
3943
  content: {
3652
- "application/json": components["schemas"]["IdentityResponseDto"];
3944
+ "application/json": {
3945
+ data: components["schemas"]["IdentityResponseDto"];
3946
+ };
3653
3947
  };
3654
3948
  };
3655
3949
  /** @description Invalid or expired token */
@@ -3734,7 +4028,9 @@ interface operations {
3734
4028
  [name: string]: unknown;
3735
4029
  };
3736
4030
  content: {
3737
- "application/json": components["schemas"]["IdentityDetailResponseDto"];
4031
+ "application/json": {
4032
+ data: components["schemas"]["IdentityDetailResponseDto"];
4033
+ };
3738
4034
  };
3739
4035
  };
3740
4036
  /** @description Invalid or expired token */
@@ -3819,7 +4115,9 @@ interface operations {
3819
4115
  [name: string]: unknown;
3820
4116
  };
3821
4117
  content: {
3822
- "application/json": components["schemas"]["MessageResponseDto"];
4118
+ "application/json": {
4119
+ data: components["schemas"]["MessageResponseDto"];
4120
+ };
3823
4121
  };
3824
4122
  };
3825
4123
  /** @description Invalid or expired token */
@@ -3904,7 +4202,9 @@ interface operations {
3904
4202
  [name: string]: unknown;
3905
4203
  };
3906
4204
  content: {
3907
- "application/json": components["schemas"]["MessageResponseDto"];
4205
+ "application/json": {
4206
+ data: components["schemas"]["MessageResponseDto"];
4207
+ };
3908
4208
  };
3909
4209
  };
3910
4210
  /** @description Invalid or expired token */
@@ -4263,7 +4563,9 @@ interface operations {
4263
4563
  [name: string]: unknown;
4264
4564
  };
4265
4565
  content: {
4266
- "application/json": components["schemas"]["AccountIdentityMfaResponseDto"];
4566
+ "application/json": {
4567
+ data: components["schemas"]["AccountIdentityMfaResponseDto"];
4568
+ };
4267
4569
  };
4268
4570
  };
4269
4571
  /** @description Invalid or expired token */
@@ -4431,7 +4733,9 @@ interface operations {
4431
4733
  [name: string]: unknown;
4432
4734
  };
4433
4735
  content: {
4434
- "application/json": components["schemas"]["IdentityAuthStateResponseDto"];
4736
+ "application/json": {
4737
+ data: components["schemas"]["IdentityAuthStateResponseDto"];
4738
+ };
4435
4739
  };
4436
4740
  };
4437
4741
  /** @description Invalid or expired token */
@@ -4671,14 +4975,101 @@ interface operations {
4671
4975
  };
4672
4976
  requestBody?: never;
4673
4977
  responses: {
4674
- /** @description Effective App-wide permissions returned */
4978
+ /** @description Effective App-wide permissions returned */
4979
+ 200: {
4980
+ headers: {
4981
+ [name: string]: unknown;
4982
+ };
4983
+ content: {
4984
+ "application/json": {
4985
+ items: string[];
4986
+ };
4987
+ };
4988
+ };
4989
+ /** @description Invalid or expired token */
4990
+ 401: {
4991
+ headers: {
4992
+ [name: string]: unknown;
4993
+ };
4994
+ content: {
4995
+ /**
4996
+ * @example {
4997
+ * "error": {
4998
+ * "statusCode": 401,
4999
+ * "code": null,
5000
+ * "message": "Invalid or expired token",
5001
+ * "timestamp": "2026-04-20T12:00:00.000Z",
5002
+ * "path": "/api/v1/identities",
5003
+ * "method": "GET"
5004
+ * }
5005
+ * }
5006
+ */
5007
+ "application/json": components["schemas"]["ErrorResponseDto"];
5008
+ };
5009
+ };
5010
+ /** @description This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa) */
5011
+ 403: {
5012
+ headers: {
5013
+ [name: string]: unknown;
5014
+ };
5015
+ content: {
5016
+ /**
5017
+ * @example {
5018
+ * "error": {
5019
+ * "statusCode": 403,
5020
+ * "code": null,
5021
+ * "message": "This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)",
5022
+ * "timestamp": "2026-04-20T12:00:00.000Z",
5023
+ * "path": "/api/v1/identities",
5024
+ * "method": "GET"
5025
+ * }
5026
+ * }
5027
+ */
5028
+ "application/json": components["schemas"]["ErrorResponseDto"];
5029
+ };
5030
+ };
5031
+ /** @description Identity not found */
5032
+ 404: {
5033
+ headers: {
5034
+ [name: string]: unknown;
5035
+ };
5036
+ content: {
5037
+ /**
5038
+ * @example {
5039
+ * "error": {
5040
+ * "statusCode": 404,
5041
+ * "code": null,
5042
+ * "message": "Identity not found",
5043
+ * "timestamp": "2026-04-20T12:00:00.000Z",
5044
+ * "path": "/api/v1/identities/{id}/permissions",
5045
+ * "method": "GET"
5046
+ * }
5047
+ * }
5048
+ */
5049
+ "application/json": components["schemas"]["ErrorResponseDto"];
5050
+ };
5051
+ };
5052
+ };
5053
+ };
5054
+ ApiIdentitiesController_getIdentityGrants: {
5055
+ parameters: {
5056
+ query?: never;
5057
+ header?: never;
5058
+ path: {
5059
+ id: string;
5060
+ };
5061
+ cookie?: never;
5062
+ };
5063
+ requestBody?: never;
5064
+ responses: {
5065
+ /** @description Grant map returned */
4675
5066
  200: {
4676
5067
  headers: {
4677
5068
  [name: string]: unknown;
4678
5069
  };
4679
5070
  content: {
4680
5071
  "application/json": {
4681
- items: string[];
5072
+ items: components["schemas"]["IdentityGrantResponseDto"][];
4682
5073
  };
4683
5074
  };
4684
5075
  };
@@ -4724,6 +5115,27 @@ interface operations {
4724
5115
  "application/json": components["schemas"]["ErrorResponseDto"];
4725
5116
  };
4726
5117
  };
5118
+ /** @description Identity not found */
5119
+ 404: {
5120
+ headers: {
5121
+ [name: string]: unknown;
5122
+ };
5123
+ content: {
5124
+ /**
5125
+ * @example {
5126
+ * "error": {
5127
+ * "statusCode": 404,
5128
+ * "code": null,
5129
+ * "message": "Identity not found",
5130
+ * "timestamp": "2026-04-20T12:00:00.000Z",
5131
+ * "path": "/api/v1/identities/{id}/grants",
5132
+ * "method": "GET"
5133
+ * }
5134
+ * }
5135
+ */
5136
+ "application/json": components["schemas"]["ErrorResponseDto"];
5137
+ };
5138
+ };
4727
5139
  };
4728
5140
  };
4729
5141
  ApiIdentityInvitesController_listInvites: {
@@ -4825,7 +5237,9 @@ interface operations {
4825
5237
  [name: string]: unknown;
4826
5238
  };
4827
5239
  content: {
4828
- "application/json": components["schemas"]["ApiIdentityInviteResponseDto"];
5240
+ "application/json": {
5241
+ data: components["schemas"]["ApiIdentityInviteResponseDto"];
5242
+ };
4829
5243
  };
4830
5244
  };
4831
5245
  /** @description Invalid or expired token */
@@ -4887,7 +5301,9 @@ interface operations {
4887
5301
  [name: string]: unknown;
4888
5302
  };
4889
5303
  content: {
4890
- "application/json": components["schemas"]["IdentityInvitesSummaryDto"];
5304
+ "application/json": {
5305
+ data: components["schemas"]["IdentityInvitesSummaryDto"];
5306
+ };
4891
5307
  };
4892
5308
  };
4893
5309
  /** @description Invalid or expired token */
@@ -5075,7 +5491,9 @@ interface operations {
5075
5491
  [name: string]: unknown;
5076
5492
  };
5077
5493
  content: {
5078
- "application/json": components["schemas"]["ApiIdentityInviteResendResponseDto"];
5494
+ "application/json": {
5495
+ data: components["schemas"]["ApiIdentityInviteResendResponseDto"];
5496
+ };
5079
5497
  };
5080
5498
  };
5081
5499
  /** @description Invite is no longer pending */
@@ -5271,7 +5689,10 @@ interface operations {
5271
5689
  ApiNodesController_listNodes: {
5272
5690
  parameters: {
5273
5691
  query?: never;
5274
- header?: never;
5692
+ header?: {
5693
+ /** @description An `ETag` from a previous response. Answers `304 Not Modified` when the hierarchy has not changed since, so a client holding a cached tree can revalidate on a short interval without re-reading it. */
5694
+ "If-None-Match"?: string;
5695
+ };
5275
5696
  path?: never;
5276
5697
  cookie?: never;
5277
5698
  };
@@ -5283,8 +5704,17 @@ interface operations {
5283
5704
  [name: string]: unknown;
5284
5705
  };
5285
5706
  content: {
5286
- "application/json": components["schemas"]["ScopedHierarchyTreeResponseDto"];
5707
+ "application/json": {
5708
+ data: components["schemas"]["ScopedHierarchyTreeResponseDto"];
5709
+ };
5710
+ };
5711
+ };
5712
+ /** @description The hierarchy is unchanged since the supplied `ETag`. No body; keep using the cached tree. */
5713
+ 304: {
5714
+ headers: {
5715
+ [name: string]: unknown;
5287
5716
  };
5717
+ content?: never;
5288
5718
  };
5289
5719
  /** @description Invalid or expired token */
5290
5720
  401: {
@@ -5349,7 +5779,9 @@ interface operations {
5349
5779
  [name: string]: unknown;
5350
5780
  };
5351
5781
  content: {
5352
- "application/json": components["schemas"]["NodeResponseDto"];
5782
+ "application/json": {
5783
+ data: components["schemas"]["NodeResponseDto"];
5784
+ };
5353
5785
  };
5354
5786
  };
5355
5787
  /** @description Invalid parent-child relationship */
@@ -5438,6 +5870,80 @@ interface operations {
5438
5870
  };
5439
5871
  };
5440
5872
  };
5873
+ ApiNodesController_listNodeParents: {
5874
+ parameters: {
5875
+ query?: never;
5876
+ header?: {
5877
+ /** @description An `ETag` from a previous response. Answers `304 Not Modified` when the hierarchy has not changed since, so a client holding a cached tree can revalidate on a short interval without re-reading it. */
5878
+ "If-None-Match"?: string;
5879
+ };
5880
+ path?: never;
5881
+ cookie?: never;
5882
+ };
5883
+ requestBody?: never;
5884
+ responses: {
5885
+ /** @description Parent edges returned */
5886
+ 200: {
5887
+ headers: {
5888
+ [name: string]: unknown;
5889
+ };
5890
+ content: {
5891
+ "application/json": {
5892
+ items: components["schemas"]["NodeParentEdgeResponseDto"][];
5893
+ };
5894
+ };
5895
+ };
5896
+ /** @description The hierarchy is unchanged since the supplied `ETag`. No body; keep using the cached tree. */
5897
+ 304: {
5898
+ headers: {
5899
+ [name: string]: unknown;
5900
+ };
5901
+ content?: never;
5902
+ };
5903
+ /** @description Invalid or expired token */
5904
+ 401: {
5905
+ headers: {
5906
+ [name: string]: unknown;
5907
+ };
5908
+ content: {
5909
+ /**
5910
+ * @example {
5911
+ * "error": {
5912
+ * "statusCode": 401,
5913
+ * "code": null,
5914
+ * "message": "Invalid or expired token",
5915
+ * "timestamp": "2026-04-20T12:00:00.000Z",
5916
+ * "path": "/api/v1/nodes",
5917
+ * "method": "GET"
5918
+ * }
5919
+ * }
5920
+ */
5921
+ "application/json": components["schemas"]["ErrorResponseDto"];
5922
+ };
5923
+ };
5924
+ /** @description This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa) */
5925
+ 403: {
5926
+ headers: {
5927
+ [name: string]: unknown;
5928
+ };
5929
+ content: {
5930
+ /**
5931
+ * @example {
5932
+ * "error": {
5933
+ * "statusCode": 403,
5934
+ * "code": null,
5935
+ * "message": "This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)",
5936
+ * "timestamp": "2026-04-20T12:00:00.000Z",
5937
+ * "path": "/api/v1/nodes",
5938
+ * "method": "GET"
5939
+ * }
5940
+ * }
5941
+ */
5942
+ "application/json": components["schemas"]["ErrorResponseDto"];
5943
+ };
5944
+ };
5945
+ };
5946
+ };
5441
5947
  ApiNodesController_getNode: {
5442
5948
  parameters: {
5443
5949
  query?: never;
@@ -5455,7 +5961,9 @@ interface operations {
5455
5961
  [name: string]: unknown;
5456
5962
  };
5457
5963
  content: {
5458
- "application/json": components["schemas"]["NodeAccessResponseDto"];
5964
+ "application/json": {
5965
+ data: components["schemas"]["NodeAccessResponseDto"];
5966
+ };
5459
5967
  };
5460
5968
  };
5461
5969
  /** @description Invalid or expired token */
@@ -5656,7 +6164,9 @@ interface operations {
5656
6164
  [name: string]: unknown;
5657
6165
  };
5658
6166
  content: {
5659
- "application/json": components["schemas"]["NodeResponseDto"];
6167
+ "application/json": {
6168
+ data: components["schemas"]["NodeResponseDto"];
6169
+ };
5660
6170
  };
5661
6171
  };
5662
6172
  /** @description Invalid parent-child relationship */
@@ -5783,7 +6293,9 @@ interface operations {
5783
6293
  [name: string]: unknown;
5784
6294
  };
5785
6295
  content: {
5786
- "application/json": components["schemas"]["HierarchyTreeNodeDto"];
6296
+ "application/json": {
6297
+ data: components["schemas"]["HierarchyTreeNodeDto"];
6298
+ };
5787
6299
  };
5788
6300
  };
5789
6301
  /** @description Invalid or expired token */
@@ -5987,7 +6499,9 @@ interface operations {
5987
6499
  [name: string]: unknown;
5988
6500
  };
5989
6501
  content: {
5990
- "application/json": components["schemas"]["NodeResponseDto"];
6502
+ "application/json": {
6503
+ data: components["schemas"]["NodeResponseDto"];
6504
+ };
5991
6505
  };
5992
6506
  };
5993
6507
  /** @description Invalid or expired token */
@@ -6200,7 +6714,9 @@ interface operations {
6200
6714
  [name: string]: unknown;
6201
6715
  };
6202
6716
  content: {
6203
- "application/json": components["schemas"]["NodeIdentitiesSummaryDto"];
6717
+ "application/json": {
6718
+ data: components["schemas"]["NodeIdentitiesSummaryDto"];
6719
+ };
6204
6720
  };
6205
6721
  };
6206
6722
  /** @description Invalid or expired token */
@@ -6354,7 +6870,9 @@ interface operations {
6354
6870
  [name: string]: unknown;
6355
6871
  };
6356
6872
  content: {
6357
- "application/json": components["schemas"]["RoleResponseDto"];
6873
+ "application/json": {
6874
+ data: components["schemas"]["RoleResponseDto"];
6875
+ };
6358
6876
  };
6359
6877
  };
6360
6878
  /** @description Invalid or expired token */
@@ -6439,7 +6957,9 @@ interface operations {
6439
6957
  [name: string]: unknown;
6440
6958
  };
6441
6959
  content: {
6442
- "application/json": components["schemas"]["RoleResponseDto"];
6960
+ "application/json": {
6961
+ data: components["schemas"]["RoleResponseDto"];
6962
+ };
6443
6963
  };
6444
6964
  };
6445
6965
  /** @description Invalid or expired token */
@@ -6640,7 +7160,9 @@ interface operations {
6640
7160
  [name: string]: unknown;
6641
7161
  };
6642
7162
  content: {
6643
- "application/json": components["schemas"]["RoleResponseDto"];
7163
+ "application/json": {
7164
+ data: components["schemas"]["RoleResponseDto"];
7165
+ };
6644
7166
  };
6645
7167
  };
6646
7168
  /** @description Invalid or expired token */
@@ -6837,7 +7359,9 @@ interface operations {
6837
7359
  [name: string]: unknown;
6838
7360
  };
6839
7361
  content: {
6840
- "application/json": components["schemas"]["MessageResponseDto"];
7362
+ "application/json": {
7363
+ data: components["schemas"]["MessageResponseDto"];
7364
+ };
6841
7365
  };
6842
7366
  };
6843
7367
  /** @description Invalid or expired token */
@@ -7002,7 +7526,9 @@ interface operations {
7002
7526
  [name: string]: unknown;
7003
7527
  };
7004
7528
  content: {
7005
- "application/json": components["schemas"]["AssignmentsSummaryDto"];
7529
+ "application/json": {
7530
+ data: components["schemas"]["AssignmentsSummaryDto"];
7531
+ };
7006
7532
  };
7007
7533
  };
7008
7534
  /** @description Invalid or expired token */
@@ -7068,7 +7594,9 @@ interface operations {
7068
7594
  [name: string]: unknown;
7069
7595
  };
7070
7596
  content: {
7071
- "application/json": components["schemas"]["AssignmentResponseDto"];
7597
+ "application/json": {
7598
+ data: components["schemas"]["AssignmentResponseDto"];
7599
+ };
7072
7600
  };
7073
7601
  };
7074
7602
  /** @description System roles cannot be assigned to identities — they are reserved for platform administration */
@@ -7261,7 +7789,9 @@ interface operations {
7261
7789
  [name: string]: unknown;
7262
7790
  };
7263
7791
  content: {
7264
- "application/json": components["schemas"]["AssignmentResponseDto"];
7792
+ "application/json": {
7793
+ data: components["schemas"]["AssignmentResponseDto"];
7794
+ };
7265
7795
  };
7266
7796
  };
7267
7797
  /** @description System roles cannot be assigned to identities — they are reserved for platform administration */
@@ -7433,7 +7963,9 @@ interface operations {
7433
7963
  [name: string]: unknown;
7434
7964
  };
7435
7965
  content: {
7436
- "application/json": components["schemas"]["MessageResponseDto"];
7966
+ "application/json": {
7967
+ data: components["schemas"]["MessageResponseDto"];
7968
+ };
7437
7969
  };
7438
7970
  };
7439
7971
  /** @description System roles cannot be assigned to identities — they are reserved for platform administration */
@@ -7821,7 +8353,9 @@ interface operations {
7821
8353
  [name: string]: unknown;
7822
8354
  };
7823
8355
  content: {
7824
- "application/json": components["schemas"]["ExportJobDto"];
8356
+ "application/json": {
8357
+ data: components["schemas"]["ExportJobDto"];
8358
+ };
7825
8359
  };
7826
8360
  };
7827
8361
  /** @description Invalid or expired token */
@@ -7885,7 +8419,9 @@ interface operations {
7885
8419
  [name: string]: unknown;
7886
8420
  };
7887
8421
  content: {
7888
- "application/json": components["schemas"]["ExportJobDto"];
8422
+ "application/json": {
8423
+ data: components["schemas"]["ExportJobDto"];
8424
+ };
7889
8425
  };
7890
8426
  };
7891
8427
  /** @description Invalid or expired token */
@@ -8053,7 +8589,9 @@ interface operations {
8053
8589
  [name: string]: unknown;
8054
8590
  };
8055
8591
  content: {
8056
- "application/json": components["schemas"]["AuditLogDetailResponseDto"];
8592
+ "application/json": {
8593
+ data: components["schemas"]["AuditLogDetailResponseDto"];
8594
+ };
8057
8595
  };
8058
8596
  };
8059
8597
  /** @description Invalid or expired token */
@@ -8230,7 +8768,9 @@ interface operations {
8230
8768
  [name: string]: unknown;
8231
8769
  };
8232
8770
  content: {
8233
- "application/json": components["schemas"]["HierarchySchemaResponseDto"];
8771
+ "application/json": {
8772
+ data: components["schemas"]["HierarchySchemaResponseDto"];
8773
+ };
8234
8774
  };
8235
8775
  };
8236
8776
  /** @description Invalid or expired token */
@@ -8300,7 +8840,9 @@ interface operations {
8300
8840
  [name: string]: unknown;
8301
8841
  };
8302
8842
  content: {
8303
- "application/json": components["schemas"]["HierarchySchemaResponseDto"];
8843
+ "application/json": {
8844
+ data: components["schemas"]["HierarchySchemaResponseDto"];
8845
+ };
8304
8846
  };
8305
8847
  };
8306
8848
  /** @description Invalid or expired token */
@@ -8463,7 +9005,9 @@ interface operations {
8463
9005
  [name: string]: unknown;
8464
9006
  };
8465
9007
  content: {
8466
- "application/json": components["schemas"]["WebhookCreatedResponseDto"];
9008
+ "application/json": {
9009
+ data: components["schemas"]["WebhookCreatedResponseDto"];
9010
+ };
8467
9011
  };
8468
9012
  };
8469
9013
  /** @description One or more event types are not supported */
@@ -8711,7 +9255,9 @@ interface operations {
8711
9255
  [name: string]: unknown;
8712
9256
  };
8713
9257
  content: {
8714
- "application/json": components["schemas"]["WebhookResponseDto"];
9258
+ "application/json": {
9259
+ data: components["schemas"]["WebhookResponseDto"];
9260
+ };
8715
9261
  };
8716
9262
  };
8717
9263
  /** @description Invalid or expired token */
@@ -8883,7 +9429,9 @@ interface operations {
8883
9429
  [name: string]: unknown;
8884
9430
  };
8885
9431
  content: {
8886
- "application/json": components["schemas"]["WebhookResponseDto"];
9432
+ "application/json": {
9433
+ data: components["schemas"]["WebhookResponseDto"];
9434
+ };
8887
9435
  };
8888
9436
  };
8889
9437
  /** @description One or more event types are not supported */
@@ -9067,7 +9615,9 @@ interface operations {
9067
9615
  [name: string]: unknown;
9068
9616
  };
9069
9617
  content: {
9070
- "application/json": components["schemas"]["ApiKeyCreatedResponseDto"];
9618
+ "application/json": {
9619
+ data: components["schemas"]["ApiKeyCreatedResponseDto"];
9620
+ };
9071
9621
  };
9072
9622
  };
9073
9623
  /** @description Invalid or expired token */
@@ -9218,7 +9768,9 @@ interface operations {
9218
9768
  [name: string]: unknown;
9219
9769
  };
9220
9770
  content: {
9221
- "application/json": components["schemas"]["ApiKeyResponseDto"];
9771
+ "application/json": {
9772
+ data: components["schemas"]["ApiKeyResponseDto"];
9773
+ };
9222
9774
  };
9223
9775
  };
9224
9776
  /** @description Invalid or expired token */
@@ -9303,7 +9855,9 @@ interface operations {
9303
9855
  [name: string]: unknown;
9304
9856
  };
9305
9857
  content: {
9306
- "application/json": components["schemas"]["ApiKeyCreatedResponseDto"];
9858
+ "application/json": {
9859
+ data: components["schemas"]["ApiKeyCreatedResponseDto"];
9860
+ };
9307
9861
  };
9308
9862
  };
9309
9863
  /** @description Invalid or expired token */
@@ -9436,6 +9990,27 @@ type QueryParams<Id extends OperationId> = operations[Id] extends {
9436
9990
  } ? NonNullable<Query> : Record<string, never>;
9437
9991
  /** Shorthand for a named schema, e.g. `Schema<"EvaluateResponseDto">`. */
9438
9992
  type Schema<Name extends keyof components["schemas"]> = components["schemas"][Name];
9993
+ /**
9994
+ * Optimistic concurrency, on the operations whose spec declares `If-Match`.
9995
+ *
9996
+ * Pass the `version` you read off the resource. If someone else has changed it
9997
+ * since, the API answers 409 rather than letting the write silently clobber
9998
+ * theirs — so this is how a read-modify-write is made safe.
9999
+ */
10000
+ interface ConcurrencyOptions {
10001
+ ifMatch?: string;
10002
+ }
10003
+ /**
10004
+ * The `If-Match` header for a call that supports it, shaped to be spread into
10005
+ * `RequestOptions`.
10006
+ *
10007
+ * Spreadable rather than returning `headers: undefined`, which
10008
+ * `exactOptionalPropertyTypes` rejects — an absent property and one explicitly
10009
+ * set to undefined are not the same thing here.
10010
+ */
10011
+ declare function withConcurrency(options?: ConcurrencyOptions): {
10012
+ headers: Record<string, string>;
10013
+ } | Record<string, never>;
9439
10014
 
9440
10015
  /**
9441
10016
  * Role grants: which identity holds which role, at which node.
@@ -9490,8 +10065,18 @@ declare class Identities {
9490
10065
  */
9491
10066
  deactivate(id: string): Promise<ResponseBody<"ApiIdentitiesController_deactivateIdentity">>;
9492
10067
  activate(id: string): Promise<ResponseBody<"ApiIdentitiesController_activateIdentity">>;
9493
- /** Every role this identity holds, and where. */
9494
- assignments(id: string): Promise<ResponseBody<"ApiIdentitiesController_getIdentityAssignments">>;
10068
+ /**
10069
+ * Every role this identity holds, and where — across all pages.
10070
+ *
10071
+ * Paginated (20 per page by default), so this returns a `Paginator` rather
10072
+ * than one response. Reading a single page here would under-report what an
10073
+ * identity can do, which is the dangerous direction to be wrong in.
10074
+ *
10075
+ * ```ts
10076
+ * for await (const assignment of canopy.identities.assignments(id)) { … }
10077
+ * ```
10078
+ */
10079
+ assignments(id: string, query?: QueryParams<"ApiIdentitiesController_getIdentityAssignments">): Paginator<IdentityAssignmentItem>;
9495
10080
  /**
9496
10081
  * The permissions this identity effectively holds, inheritance resolved.
9497
10082
  *
@@ -9502,6 +10087,8 @@ declare class Identities {
9502
10087
  permissions(id: string): Promise<ResponseBody<"ApiIdentitiesController_getIdentityPermissions">>;
9503
10088
  }
9504
10089
  type IdentityItem = ItemOf$2<ResponseBody<"ApiIdentitiesController_listIdentities">>;
10090
+ /** One role grant from an identity's assignments collection. */
10091
+ type IdentityAssignmentItem = ItemOf$2<ResponseBody<"ApiIdentitiesController_getIdentityAssignments">>;
9505
10092
  type ItemOf$2<T> = T extends {
9506
10093
  items: (infer Item)[];
9507
10094
  } ? Item : never;
@@ -9524,28 +10111,37 @@ declare class Permissions {
9524
10111
  * `effective_node_id: null` — that answer must never be used to guard a
9525
10112
  * resource that belongs to a specific node, which is why the scope is a
9526
10113
  * required field rather than a default.
10114
+ *
10115
+ * This runs on the request path, so it is the call most worth passing
10116
+ * `signal` and a tight `timeoutMs` to: without them a slow answer here holds
10117
+ * an inbound request open for the client-wide deadline on every attempt.
9527
10118
  */
9528
- evaluate(input: RequestBody<"ApiPermissionsController_evaluate">): Promise<ResponseBody<"ApiPermissionsController_evaluate">>;
10119
+ evaluate(input: RequestBody<"ApiPermissionsController_evaluate">, options?: CallOptions): Promise<ResponseBody<"ApiPermissionsController_evaluate">>;
9529
10120
  /**
9530
10121
  * Evaluate many decisions in one round trip.
9531
10122
  *
9532
10123
  * Prefer this to a loop over `evaluate` when rendering a screen: the checks
9533
10124
  * are answered together instead of paying request latency for each.
9534
10125
  */
9535
- evaluateBulk(input: RequestBody<"ApiPermissionsController_evaluateBulk">): Promise<ResponseBody<"ApiPermissionsController_evaluateBulk">>;
10126
+ evaluateBulk(input: RequestBody<"ApiPermissionsController_evaluateBulk">, options?: CallOptions): Promise<ResponseBody<"ApiPermissionsController_evaluateBulk">>;
9536
10127
  /**
9537
10128
  * The same decision with its reasoning — which role granted it, which node
9538
10129
  * it was inherited from. For debugging an unexpected allow or deny, not for
9539
10130
  * the enforcement path.
9540
10131
  */
9541
- explain(input: RequestBody<"ApiPermissionsController_explain">): Promise<ResponseBody<"ApiPermissionsController_explain">>;
10132
+ explain(input: RequestBody<"ApiPermissionsController_explain">, options?: CallOptions): Promise<ResponseBody<"ApiPermissionsController_explain">>;
9542
10133
  /** Every permission in the Environment, page by page. */
9543
10134
  list(query?: QueryParams<"ApiPermissionsController_listPermissions">): Paginator<PermissionItem>;
9544
10135
  get(id: string): Promise<ResponseBody<"ApiPermissionsController_getPermission">>;
9545
10136
  /** Define permissions. The request takes a batch, not a single key. */
9546
10137
  create(input: RequestBody<"ApiPermissionsController_createPermissions">): Promise<ResponseBody<"ApiPermissionsController_createPermissions">>;
9547
- update(id: string, input: RequestBody<"ApiPermissionsController_updatePermission">): Promise<ResponseBody<"ApiPermissionsController_updatePermission">>;
9548
- delete(id: string): Promise<void>;
10138
+ /**
10139
+ * Pass `ifMatch` with the permission's current `version` to make a
10140
+ * read-modify-write safe — a concurrent edit answers 409 instead of being
10141
+ * silently overwritten.
10142
+ */
10143
+ update(id: string, input: RequestBody<"ApiPermissionsController_updatePermission">, options?: ConcurrencyOptions): Promise<ResponseBody<"ApiPermissionsController_updatePermission">>;
10144
+ delete(id: string, options?: ConcurrencyOptions): Promise<void>;
9549
10145
  }
9550
10146
  /**
9551
10147
  * One item from the permissions collection.
@@ -9565,8 +10161,13 @@ declare class Roles {
9565
10161
  list(query?: QueryParams<"ApiRolesController_listRoles">): Paginator<RoleItem>;
9566
10162
  get(id: string): Promise<ResponseBody<"ApiRolesController_getRole">>;
9567
10163
  create(input: RequestBody<"ApiRolesController_createRole">): Promise<ResponseBody<"ApiRolesController_createRole">>;
9568
- update(id: string, input: RequestBody<"ApiRolesController_updateRole">): Promise<ResponseBody<"ApiRolesController_updateRole">>;
9569
- delete(id: string): Promise<void>;
10164
+ /**
10165
+ * Pass `ifMatch` with the role's current `version` to make a
10166
+ * read-modify-write safe — a concurrent edit answers 409 instead of being
10167
+ * silently overwritten.
10168
+ */
10169
+ update(id: string, input: RequestBody<"ApiRolesController_updateRole">, options?: ConcurrencyOptions): Promise<ResponseBody<"ApiRolesController_updateRole">>;
10170
+ delete(id: string, options?: ConcurrencyOptions): Promise<void>;
9570
10171
  permissions(id: string): Promise<ResponseBody<"ApiRolesController_getRolePermissions">>;
9571
10172
  /**
9572
10173
  * Replaces the role's permissions wholesale — this is a PUT, so anything
@@ -9608,13 +10209,129 @@ declare class Canopy {
9608
10209
  constructor(options: CanopyClientOptions);
9609
10210
  }
9610
10211
 
10212
+ interface LocalAuthorizerOptions {
10213
+ /**
10214
+ * How long grant roots and the hierarchy are reused, in milliseconds.
10215
+ * Defaults to 60s.
10216
+ *
10217
+ * This is a staleness budget, not a performance dial. Shortening it makes
10218
+ * revocation take effect sooner and costs more calls — and below the gap
10219
+ * between a user's requests it stops saving anything at all, because every
10220
+ * request finds the cache expired and refetches. Human-paced traffic has
10221
+ * multi-second gaps, so a value of a few seconds can cost full price for no
10222
+ * benefit.
10223
+ *
10224
+ * **`0` disables caching**, reading fresh on every check. That is the escape
10225
+ * for a caller who cannot accept a stale allow at all — it costs a round trip
10226
+ * per request, which is what this class exists to avoid, so reach for it
10227
+ * knowingly rather than as a default.
10228
+ */
10229
+ ttlMs?: number;
10230
+ /**
10231
+ * Per-attempt deadline for the reads this class makes.
10232
+ *
10233
+ * A cache hit costs nothing, but a miss happens on the request path and
10234
+ * holds an inbound request open exactly as a per-request check used to — so
10235
+ * the bound still matters, it just applies far less often.
10236
+ */
10237
+ timeoutMs?: number;
10238
+ /** Retries for those reads. */
10239
+ maxRetries?: number;
10240
+ /** Injectable clock, for tests. */
10241
+ now?: () => number;
10242
+ }
10243
+ interface LocalAuthorizationQuery {
10244
+ identity_id: string;
10245
+ permission: string;
10246
+ /** Defaults to `node`, which is the strict question. */
10247
+ scope?: "node" | "app_wide";
10248
+ node_id?: string;
10249
+ }
10250
+ interface LocalAuthorizerStats {
10251
+ /** Calls made to read an identity's grant roots. */
10252
+ grantFetches: number;
10253
+ /** Calls made to read the hierarchy, including revalidations. */
10254
+ treeRequests: number;
10255
+ /** Revalidations the server answered `304`, so no tree was transferred. */
10256
+ treeNotModified: number;
10257
+ }
10258
+ declare class LocalAuthorizer {
10259
+ private readonly client;
10260
+ private readonly ttlMs;
10261
+ private readonly now;
10262
+ private readonly readOptions;
10263
+ private readonly grants;
10264
+ private tree;
10265
+ /**
10266
+ * In-flight reads, so concurrent requests for the same thing share one call.
10267
+ *
10268
+ * Without this a cold start under load fans out: a hundred simultaneous
10269
+ * requests for one identity would each miss the cache and each fetch, which
10270
+ * is the per-request traffic this class exists to remove, concentrated into
10271
+ * the worst possible moment.
10272
+ */
10273
+ private readonly pendingGrants;
10274
+ private pendingTree;
10275
+ private stats;
10276
+ constructor(client: CanopyClient, options?: LocalAuthorizerOptions);
10277
+ /**
10278
+ * Whether the identity holds the permission.
10279
+ *
10280
+ * Shaped like the API's own evaluate so a caller can swap one for the other.
10281
+ * A `node` check with no node is a denial rather than an error: a request
10282
+ * whose subject cannot be established is exactly the one that must not pass.
10283
+ */
10284
+ evaluate(query: LocalAuthorizationQuery, options?: {
10285
+ signal?: AbortSignal;
10286
+ }): Promise<{
10287
+ allowed: boolean;
10288
+ }>;
10289
+ private decide;
10290
+ /** Counters for observability — how much traffic the cache is actually saving. */
10291
+ snapshot(): LocalAuthorizerStats;
10292
+ /**
10293
+ * Drop what is held so the next evaluate refetches.
10294
+ *
10295
+ * With an `identityId`, only that identity's grants are dropped — the
10296
+ * cached hierarchy and every other identity's entries stay warm. This is
10297
+ * the shape an assignment webhook wants: the event names the identity
10298
+ * whose authority moved, and nothing else needs to pay a refetch for it.
10299
+ *
10300
+ * With no argument, everything goes: grants and the hierarchy tree. Not
10301
+ * needed in normal operation, where entries expire on their own; useful in
10302
+ * tests and after a change whose reach you cannot name (a role's
10303
+ * permissions edited, a node moved).
10304
+ *
10305
+ * Multi-instance honesty: an invalidation reaches THIS process only. A
10306
+ * webhook lands on one instance behind a load balancer; the others serve
10307
+ * their cached grants until their own TTL expires. Unless the app fans the
10308
+ * event out over its own pub/sub, the fleet-wide revocation guarantee is
10309
+ * the TTL, and webhook-driven invalidation is a latency optimization on
10310
+ * top of it — size the TTL to the revocation latency you can promise.
10311
+ */
10312
+ invalidate(identityId?: string): void;
10313
+ /** Climb from `nodeId` and look for a grant root among its ancestors. */
10314
+ private holdsAtNode;
10315
+ private grantRootsFor;
10316
+ private fetchGrantRoots;
10317
+ private parents;
10318
+ /**
10319
+ * Revalidate rather than re-read. The hierarchy is the expensive half and
10320
+ * the one that changes least, so the common case is a `304` and no transfer
10321
+ * at all — the tree stays in memory and only its expiry moves.
10322
+ */
10323
+ private fetchTree;
10324
+ }
10325
+
9611
10326
  /**
9612
10327
  * Everything this client throws.
9613
10328
  *
9614
- * Two classes, because callers act on the distinction: `CanopyError` means the
9615
- * API answered and said no, `CanopyConnectionError` means it never answered.
9616
- * The first is a decision you may need to surface to a user; the second is
9617
- * usually worth retrying or alerting on.
10329
+ * Three classes, because callers act on the distinction: `CanopyError` means
10330
+ * the API answered and said no, `CanopyConnectionError` means it never
10331
+ * answered, and `CanopyTokenError` means a token failed local verification
10332
+ * without anything being asked of the API at all. The first is a decision you
10333
+ * may need to surface to a user; the second is usually worth retrying or
10334
+ * alerting on; the third is a 401 for the caller who presented the token.
9618
10335
  */
9619
10336
  /** The `error` object inside Canopy's error envelope. */
9620
10337
  interface CanopyErrorBody {
@@ -9651,18 +10368,29 @@ declare class CanopyError extends Error {
9651
10368
  method: string;
9652
10369
  path: string;
9653
10370
  };
10371
+ /**
10372
+ * How long to wait before retrying, in milliseconds, when the server said so
10373
+ * via `Retry-After` — populated on a 429, and on any other response that
10374
+ * carries the header. Undefined when the server gave no guidance.
10375
+ */
10376
+ readonly retryAfterMs: number | undefined;
9654
10377
  constructor(body: CanopyErrorBody, request: {
9655
10378
  method: string;
9656
10379
  path: string;
9657
- });
10380
+ }, retryAfterMs?: number);
9658
10381
  /** A 429. `retryAfterMs` is populated when the server said how long to wait. */
9659
10382
  get isRateLimited(): boolean;
9660
10383
  /** 401 or 403 — the credential is wrong or not permitted here. */
9661
10384
  get isAuthFailure(): boolean;
9662
10385
  }
9663
10386
  /**
9664
- * The request never produced a response — DNS failure, connection reset,
9665
- * timeout, or an aborted signal.
10387
+ * The request never produced a response — DNS failure, connection reset, or a
10388
+ * timeout.
10389
+ *
10390
+ * Not this: a caller's own cancellation. Aborting the `signal` passed to a
10391
+ * request rejects with that abort (`AbortError`, or whatever `signal.reason`
10392
+ * holds) and is never retried, because the caller stopping is an intent rather
10393
+ * than a failure to report.
9666
10394
  *
9667
10395
  * Kept separate from `CanopyError` because there is no status code and no
9668
10396
  * server opinion to report: nothing is known about whether the operation
@@ -9681,8 +10409,202 @@ declare class CanopyConnectionError extends Error {
9681
10409
  cause?: unknown;
9682
10410
  });
9683
10411
  }
10412
+ /**
10413
+ * A token failed verification.
10414
+ *
10415
+ * Separate from `CanopyError` because nothing was asked of the API: the token
10416
+ * was checked here, against keys already held. There is no status code to
10417
+ * report and no server opinion to relay — the decision was local, which is the
10418
+ * whole point of verifying a signature rather than calling an endpoint.
10419
+ *
10420
+ * Branch on `code`; it follows the same dot-notation contract as the API's own
10421
+ * codes. Every one of them means the same thing to an HTTP caller — 401 — so
10422
+ * the code is for your logs and your tests, not usually for the response.
10423
+ */
10424
+ declare class CanopyTokenError extends Error {
10425
+ readonly name = "CanopyTokenError";
10426
+ /**
10427
+ * One of:
10428
+ *
10429
+ * - `token.malformed` — not a JWS, or a segment would not decode
10430
+ * - `token.unsupported_algorithm` — not RS256; Canopy issues only RS256
10431
+ * - `token.key_not_found` — no published key matches the token's `kid`
10432
+ * - `token.jwks_unavailable` — the key set could not be fetched or parsed
10433
+ * - `token.signature_invalid` — signature does not match the signing key
10434
+ * - `token.expired` / `token.not_yet_valid` — outside its validity window
10435
+ * - `token.issuer_mismatch` — `iss` is not the configured issuer
10436
+ * - `token.audience_mismatch` — `aud` does not include the configured audience
10437
+ * - `token.audience_unverified` — token has an `aud` but none was configured
10438
+ * - `token.preauth_not_allowed` — a pre-auth token, which grants no access
10439
+ */
10440
+ readonly code: string;
10441
+ constructor(code: string, message: string, options?: {
10442
+ cause?: unknown;
10443
+ });
10444
+ }
10445
+ /**
10446
+ * The local authorizer could not answer, and must not pretend otherwise.
10447
+ *
10448
+ * Distinct from a denial. It is raised when the data a decision needs is not
10449
+ * merely absent but *unreadable* — most often a credential that cannot read the
10450
+ * hierarchy, which comes back as an empty tree rather than an error. Denying
10451
+ * there would be a silent, total false-deny on every node-scoped route while
10452
+ * every response still looked healthy.
10453
+ *
10454
+ * No retry fixes it, so callers should treat it as a misconfiguration rather
10455
+ * than an outage.
10456
+ */
10457
+ declare class CanopyAuthorizerError extends Error {
10458
+ readonly name = "CanopyAuthorizerError";
10459
+ readonly code: string;
10460
+ constructor(code: string, message: string, options?: {
10461
+ cause?: unknown;
10462
+ });
10463
+ }
10464
+ declare function isCanopyAuthorizerError(error: unknown): error is CanopyAuthorizerError;
9684
10465
  /** Narrowing helper that survives bundling and duplicate copies of the package. */
9685
10466
  declare function isCanopyError(error: unknown): error is CanopyError;
9686
10467
  declare function isCanopyConnectionError(error: unknown): error is CanopyConnectionError;
10468
+ declare function isCanopyTokenError(error: unknown): error is CanopyTokenError;
10469
+
10470
+ interface TokenVerifierOptions {
10471
+ /**
10472
+ * The issuer whose tokens are accepted, matched against `iss` exactly.
10473
+ * Defaults to Canopy's hosted issuer; set it for a self-hosted instance.
10474
+ *
10475
+ * Safe to default because it fails closed: pointed at the wrong issuer, a
10476
+ * token is rejected rather than accepted.
10477
+ */
10478
+ issuer?: string;
10479
+ /**
10480
+ * Required audience, matched against `aud`.
10481
+ *
10482
+ * Hosted Login (OAuth) tokens carry `aud` — your client id — and Direct API
10483
+ * identity tokens do not. Verifying an OAuth token without setting this
10484
+ * throws rather than ignoring the claim, because `aud` is what stops a token
10485
+ * minted for one client being replayed at another.
10486
+ */
10487
+ audience?: string;
10488
+ /** Where the signing keys live. Defaults to `${issuer}/.well-known/jwks.json`. */
10489
+ jwksUri?: string;
10490
+ /** How long a fetched key set is reused. Defaults to 10 minutes. */
10491
+ jwksCacheMaxAgeMs?: number;
10492
+ /** Floor between refetches provoked by an unknown `kid`. Defaults to 30s. */
10493
+ jwksMinRefetchIntervalMs?: number;
10494
+ /** Leeway on `exp` and `nbf`, in seconds. Defaults to 60. */
10495
+ clockToleranceSec?: number;
10496
+ /**
10497
+ * Accept pre-auth tokens. Defaults to `false`, and should stay false unless
10498
+ * you are building an account picker.
10499
+ *
10500
+ * A pre-auth token is a genuine, correctly-signed Canopy token issued
10501
+ * partway through a multi-account login: the person proved their password
10502
+ * but has not yet chosen an Account, so the token carries no Account context
10503
+ * and grants nothing. Accepting one as a session is a privilege escalation,
10504
+ * and it is the failure a hand-rolled verifier is most likely to miss —
10505
+ * every signature check passes.
10506
+ */
10507
+ allowPreAuthTokens?: boolean;
10508
+ /**
10509
+ * Deadline for a single JWKS read, in milliseconds. Defaults to 5s.
10510
+ *
10511
+ * Without one, an issuer that accepts a connection and then never answers
10512
+ * holds every inbound request that needs a key — the verification sits on the
10513
+ * request path, so an unbounded read there is an unbounded request.
10514
+ */
10515
+ jwksTimeoutMs?: number;
10516
+ /** Injectable for tests and for runtimes with a non-global fetch. */
10517
+ fetch?: typeof globalThis.fetch;
10518
+ }
10519
+ /**
10520
+ * The claims Canopy puts in an access token.
10521
+ *
10522
+ * Named claims are the ones the API guarantees; the index signature keeps
10523
+ * anything added later reachable without a version bump.
10524
+ */
10525
+ interface CanopyTokenClaims {
10526
+ /** The identity or user this token acts as. */
10527
+ sub: string;
10528
+ /** Which kind of principal `sub` is. */
10529
+ type: "user" | "identity" | "api_key" | "platform";
10530
+ iss: string;
10531
+ exp: number;
10532
+ iat?: number;
10533
+ nbf?: number;
10534
+ aud?: string | string[];
10535
+ account_id?: string;
10536
+ application_id?: string;
10537
+ /** Identity tokens carry this; admin tokens deliberately omit it. */
10538
+ environment_id?: string;
10539
+ account_slug?: string;
10540
+ application_slug?: string;
10541
+ environment_slug?: string;
10542
+ /** Present only on a pre-auth token — see `allowPreAuthTokens`. */
10543
+ token_type?: "preauth";
10544
+ console_access?: "granted" | "none";
10545
+ /** Emitted only when the `permissions` OAuth scope was granted. */
10546
+ permissions?: string[];
10547
+ /** True when `permissions` was truncated; query the API for the full set. */
10548
+ permissions_overflow?: boolean;
10549
+ [claim: string]: unknown;
10550
+ }
10551
+ declare class TokenVerifier {
10552
+ private readonly issuer;
10553
+ private readonly audience;
10554
+ private readonly jwksUri;
10555
+ private readonly jwksCacheMaxAgeMs;
10556
+ private readonly jwksMinRefetchIntervalMs;
10557
+ private readonly jwksTimeoutMs;
10558
+ private readonly clockToleranceSec;
10559
+ private readonly allowPreAuthTokens;
10560
+ private readonly fetchImpl;
10561
+ /** Imported keys by `kid`, so a repeat verification skips the import cost. */
10562
+ private keys;
10563
+ private keysFetchedAt;
10564
+ private lastFetchAttemptAt;
10565
+ /** In-flight fetch, so a burst of requests triggers one call, not N. */
10566
+ private inFlight;
10567
+ constructor(options?: TokenVerifierOptions);
10568
+ /**
10569
+ * Verify a token and return its claims. Throws {@link CanopyTokenError} on
10570
+ * anything short of a full pass — branch on `error.code`.
10571
+ *
10572
+ * Order matters: the signature is checked before any claim is believed, so
10573
+ * nothing downstream ever reads an unverified payload.
10574
+ */
10575
+ verify(token: string): Promise<CanopyTokenClaims>;
10576
+ /** Everything checked after the signature is known good. */
10577
+ private assertClaims;
10578
+ /**
10579
+ * `aud` is checked when either side mentions it.
10580
+ *
10581
+ * The case worth stating: a token carries `aud` but the verifier was not
10582
+ * configured with one. That is not "no audience to check" — it is an OAuth
10583
+ * token being verified by something that never said which client it is, and
10584
+ * ignoring it would accept a token minted for a different client. So it
10585
+ * throws and names the option.
10586
+ */
10587
+ private assertAudience;
10588
+ /**
10589
+ * The signing key for a `kid`, fetching the key set when it is stale or when
10590
+ * the `kid` is unknown — the latter is how key rotation is picked up
10591
+ * mid-process, bounded by `jwksMinRefetchIntervalMs`.
10592
+ */
10593
+ private resolveKey;
10594
+ /**
10595
+ * Whether to await a key-set refresh.
10596
+ *
10597
+ * Two ways to qualify, and the first matters as much as the second. A read
10598
+ * already in flight is joined regardless of the floor: it costs no extra
10599
+ * outbound request, and it is what lets a concurrent burst share one fetch
10600
+ * instead of one caller winning and the rest being turned away.
10601
+ *
10602
+ * Otherwise the floor applies — the same one for every refetch path, so they
10603
+ * cannot drift into having different amplification properties.
10604
+ */
10605
+ private shouldRefresh;
10606
+ private refreshKeys;
10607
+ private fetchKeys;
10608
+ }
9687
10609
 
9688
- export { type AssignmentItem, Assignments, Canopy, CanopyClient, type CanopyClientOptions, CanopyConnectionError, CanopyError, type CanopyErrorBody, type Collection, type CursorPagination, Identities, type IdentityItem, type OffsetPagination, type OperationId, type PageFetcher, type PageParams, type PaginateOptions, type Pagination, Paginator, type PartialSuccess, type PermissionItem, Permissions, type QueryParams, type RequestBody, type RequestOptions, type ResponseBody, type RoleItem, Roles, type Schema, type components, isCanopyConnectionError, isCanopyError, isCursorPagination, type operations, paginate, type paths };
10610
+ export { type AssignmentItem, Assignments, type CallOptions, Canopy, CanopyAuthorizerError, CanopyClient, type CanopyClientOptions, CanopyConnectionError, CanopyError, type CanopyErrorBody, type CanopyTokenClaims, CanopyTokenError, type Collection, type ConcurrencyOptions, type ConditionalResult, type CursorPagination, Identities, type IdentityAssignmentItem, type IdentityItem, type LocalAuthorizationQuery, LocalAuthorizer, type LocalAuthorizerOptions, type LocalAuthorizerStats, type OffsetPagination, type OperationId, type PageFetcher, type PageParams, type PaginateOptions, type Pagination, Paginator, type PartialSuccess, type PermissionItem, Permissions, type QueryParams, type RequestBody, type RequestOptions, type ResponseBody, type RoleItem, Roles, type Schema, TokenVerifier, type TokenVerifierOptions, type components, isCanopyAuthorizerError, isCanopyConnectionError, isCanopyError, isCanopyTokenError, isCursorPagination, type operations, paginate, type paths, withConcurrency };