@canopy-io/node 0.1.0 → 0.1.1
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 +102 -36
- package/dist/index.cjs +747 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +714 -23
- package/dist/index.d.ts +714 -23
- package/dist/index.js +741 -49
- package/dist/index.js.map +1 -1
- package/package.json +7 -27
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
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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;
|
|
@@ -643,6 +739,26 @@ interface paths {
|
|
|
643
739
|
patch?: never;
|
|
644
740
|
trace?: never;
|
|
645
741
|
};
|
|
742
|
+
"/api/v1/identities/{id}/grants": {
|
|
743
|
+
parameters: {
|
|
744
|
+
query?: never;
|
|
745
|
+
header?: never;
|
|
746
|
+
path?: never;
|
|
747
|
+
cookie?: never;
|
|
748
|
+
};
|
|
749
|
+
/**
|
|
750
|
+
* Get where an identity holds each permission
|
|
751
|
+
* @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.
|
|
752
|
+
*/
|
|
753
|
+
get: operations["ApiIdentitiesController_getIdentityGrants"];
|
|
754
|
+
put?: never;
|
|
755
|
+
post?: never;
|
|
756
|
+
delete?: never;
|
|
757
|
+
options?: never;
|
|
758
|
+
head?: never;
|
|
759
|
+
patch?: never;
|
|
760
|
+
trace?: never;
|
|
761
|
+
};
|
|
646
762
|
"/api/v1/identity-invites": {
|
|
647
763
|
parameters: {
|
|
648
764
|
query?: never;
|
|
@@ -771,6 +887,26 @@ interface paths {
|
|
|
771
887
|
patch?: never;
|
|
772
888
|
trace?: never;
|
|
773
889
|
};
|
|
890
|
+
"/api/v1/nodes/parents": {
|
|
891
|
+
parameters: {
|
|
892
|
+
query?: never;
|
|
893
|
+
header?: never;
|
|
894
|
+
path?: never;
|
|
895
|
+
cookie?: never;
|
|
896
|
+
};
|
|
897
|
+
/**
|
|
898
|
+
* List the hierarchy as parent edges
|
|
899
|
+
* @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.
|
|
900
|
+
*/
|
|
901
|
+
get: operations["ApiNodesController_listNodeParents"];
|
|
902
|
+
put?: never;
|
|
903
|
+
post?: never;
|
|
904
|
+
delete?: never;
|
|
905
|
+
options?: never;
|
|
906
|
+
head?: never;
|
|
907
|
+
patch?: never;
|
|
908
|
+
trace?: never;
|
|
909
|
+
};
|
|
774
910
|
"/api/v1/nodes/{id}": {
|
|
775
911
|
parameters: {
|
|
776
912
|
query?: never;
|
|
@@ -1714,6 +1850,12 @@ interface components {
|
|
|
1714
1850
|
/** Format: date-time */
|
|
1715
1851
|
updated_at: string;
|
|
1716
1852
|
};
|
|
1853
|
+
IdentityGrantResponseDto: {
|
|
1854
|
+
/** @example reports.view */
|
|
1855
|
+
permission: string;
|
|
1856
|
+
/** @description Hierarchy node ids the permission was granted at. Not expanded through descendants. */
|
|
1857
|
+
nodes: string[];
|
|
1858
|
+
};
|
|
1717
1859
|
IdentityInviteResponseDto: {
|
|
1718
1860
|
id: string;
|
|
1719
1861
|
email: string;
|
|
@@ -1871,6 +2013,11 @@ interface components {
|
|
|
1871
2013
|
tree: components["schemas"]["HierarchyTreeNodeDto"][];
|
|
1872
2014
|
scope: components["schemas"]["HierarchyScopeDto"];
|
|
1873
2015
|
};
|
|
2016
|
+
NodeParentEdgeResponseDto: {
|
|
2017
|
+
id: string;
|
|
2018
|
+
/** @description Null for the root node, which has no parent. */
|
|
2019
|
+
parent_node_id?: string | null;
|
|
2020
|
+
};
|
|
1874
2021
|
NodeAccessResponseDto: {
|
|
1875
2022
|
id: string;
|
|
1876
2023
|
application_id: string;
|
|
@@ -4724,6 +4871,114 @@ interface operations {
|
|
|
4724
4871
|
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
4725
4872
|
};
|
|
4726
4873
|
};
|
|
4874
|
+
/** @description Identity not found */
|
|
4875
|
+
404: {
|
|
4876
|
+
headers: {
|
|
4877
|
+
[name: string]: unknown;
|
|
4878
|
+
};
|
|
4879
|
+
content: {
|
|
4880
|
+
/**
|
|
4881
|
+
* @example {
|
|
4882
|
+
* "error": {
|
|
4883
|
+
* "statusCode": 404,
|
|
4884
|
+
* "code": null,
|
|
4885
|
+
* "message": "Identity not found",
|
|
4886
|
+
* "timestamp": "2026-04-20T12:00:00.000Z",
|
|
4887
|
+
* "path": "/api/v1/identities/{id}/permissions",
|
|
4888
|
+
* "method": "GET"
|
|
4889
|
+
* }
|
|
4890
|
+
* }
|
|
4891
|
+
*/
|
|
4892
|
+
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
4893
|
+
};
|
|
4894
|
+
};
|
|
4895
|
+
};
|
|
4896
|
+
};
|
|
4897
|
+
ApiIdentitiesController_getIdentityGrants: {
|
|
4898
|
+
parameters: {
|
|
4899
|
+
query?: never;
|
|
4900
|
+
header?: never;
|
|
4901
|
+
path: {
|
|
4902
|
+
id: string;
|
|
4903
|
+
};
|
|
4904
|
+
cookie?: never;
|
|
4905
|
+
};
|
|
4906
|
+
requestBody?: never;
|
|
4907
|
+
responses: {
|
|
4908
|
+
/** @description Grant map returned */
|
|
4909
|
+
200: {
|
|
4910
|
+
headers: {
|
|
4911
|
+
[name: string]: unknown;
|
|
4912
|
+
};
|
|
4913
|
+
content: {
|
|
4914
|
+
"application/json": {
|
|
4915
|
+
items: components["schemas"]["IdentityGrantResponseDto"][];
|
|
4916
|
+
};
|
|
4917
|
+
};
|
|
4918
|
+
};
|
|
4919
|
+
/** @description Invalid or expired token */
|
|
4920
|
+
401: {
|
|
4921
|
+
headers: {
|
|
4922
|
+
[name: string]: unknown;
|
|
4923
|
+
};
|
|
4924
|
+
content: {
|
|
4925
|
+
/**
|
|
4926
|
+
* @example {
|
|
4927
|
+
* "error": {
|
|
4928
|
+
* "statusCode": 401,
|
|
4929
|
+
* "code": null,
|
|
4930
|
+
* "message": "Invalid or expired token",
|
|
4931
|
+
* "timestamp": "2026-04-20T12:00:00.000Z",
|
|
4932
|
+
* "path": "/api/v1/identities",
|
|
4933
|
+
* "method": "GET"
|
|
4934
|
+
* }
|
|
4935
|
+
* }
|
|
4936
|
+
*/
|
|
4937
|
+
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
4938
|
+
};
|
|
4939
|
+
};
|
|
4940
|
+
/** @description This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa) */
|
|
4941
|
+
403: {
|
|
4942
|
+
headers: {
|
|
4943
|
+
[name: string]: unknown;
|
|
4944
|
+
};
|
|
4945
|
+
content: {
|
|
4946
|
+
/**
|
|
4947
|
+
* @example {
|
|
4948
|
+
* "error": {
|
|
4949
|
+
* "statusCode": 403,
|
|
4950
|
+
* "code": null,
|
|
4951
|
+
* "message": "This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)",
|
|
4952
|
+
* "timestamp": "2026-04-20T12:00:00.000Z",
|
|
4953
|
+
* "path": "/api/v1/identities",
|
|
4954
|
+
* "method": "GET"
|
|
4955
|
+
* }
|
|
4956
|
+
* }
|
|
4957
|
+
*/
|
|
4958
|
+
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
4959
|
+
};
|
|
4960
|
+
};
|
|
4961
|
+
/** @description Identity not found */
|
|
4962
|
+
404: {
|
|
4963
|
+
headers: {
|
|
4964
|
+
[name: string]: unknown;
|
|
4965
|
+
};
|
|
4966
|
+
content: {
|
|
4967
|
+
/**
|
|
4968
|
+
* @example {
|
|
4969
|
+
* "error": {
|
|
4970
|
+
* "statusCode": 404,
|
|
4971
|
+
* "code": null,
|
|
4972
|
+
* "message": "Identity not found",
|
|
4973
|
+
* "timestamp": "2026-04-20T12:00:00.000Z",
|
|
4974
|
+
* "path": "/api/v1/identities/{id}/grants",
|
|
4975
|
+
* "method": "GET"
|
|
4976
|
+
* }
|
|
4977
|
+
* }
|
|
4978
|
+
*/
|
|
4979
|
+
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
4980
|
+
};
|
|
4981
|
+
};
|
|
4727
4982
|
};
|
|
4728
4983
|
};
|
|
4729
4984
|
ApiIdentityInvitesController_listInvites: {
|
|
@@ -5271,7 +5526,10 @@ interface operations {
|
|
|
5271
5526
|
ApiNodesController_listNodes: {
|
|
5272
5527
|
parameters: {
|
|
5273
5528
|
query?: never;
|
|
5274
|
-
header?:
|
|
5529
|
+
header?: {
|
|
5530
|
+
/** @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. */
|
|
5531
|
+
"If-None-Match"?: string;
|
|
5532
|
+
};
|
|
5275
5533
|
path?: never;
|
|
5276
5534
|
cookie?: never;
|
|
5277
5535
|
};
|
|
@@ -5286,6 +5544,13 @@ interface operations {
|
|
|
5286
5544
|
"application/json": components["schemas"]["ScopedHierarchyTreeResponseDto"];
|
|
5287
5545
|
};
|
|
5288
5546
|
};
|
|
5547
|
+
/** @description The hierarchy is unchanged since the supplied `ETag`. No body; keep using the cached tree. */
|
|
5548
|
+
304: {
|
|
5549
|
+
headers: {
|
|
5550
|
+
[name: string]: unknown;
|
|
5551
|
+
};
|
|
5552
|
+
content?: never;
|
|
5553
|
+
};
|
|
5289
5554
|
/** @description Invalid or expired token */
|
|
5290
5555
|
401: {
|
|
5291
5556
|
headers: {
|
|
@@ -5438,6 +5703,80 @@ interface operations {
|
|
|
5438
5703
|
};
|
|
5439
5704
|
};
|
|
5440
5705
|
};
|
|
5706
|
+
ApiNodesController_listNodeParents: {
|
|
5707
|
+
parameters: {
|
|
5708
|
+
query?: never;
|
|
5709
|
+
header?: {
|
|
5710
|
+
/** @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. */
|
|
5711
|
+
"If-None-Match"?: string;
|
|
5712
|
+
};
|
|
5713
|
+
path?: never;
|
|
5714
|
+
cookie?: never;
|
|
5715
|
+
};
|
|
5716
|
+
requestBody?: never;
|
|
5717
|
+
responses: {
|
|
5718
|
+
/** @description Parent edges returned */
|
|
5719
|
+
200: {
|
|
5720
|
+
headers: {
|
|
5721
|
+
[name: string]: unknown;
|
|
5722
|
+
};
|
|
5723
|
+
content: {
|
|
5724
|
+
"application/json": {
|
|
5725
|
+
items: components["schemas"]["NodeParentEdgeResponseDto"][];
|
|
5726
|
+
};
|
|
5727
|
+
};
|
|
5728
|
+
};
|
|
5729
|
+
/** @description The hierarchy is unchanged since the supplied `ETag`. No body; keep using the cached tree. */
|
|
5730
|
+
304: {
|
|
5731
|
+
headers: {
|
|
5732
|
+
[name: string]: unknown;
|
|
5733
|
+
};
|
|
5734
|
+
content?: never;
|
|
5735
|
+
};
|
|
5736
|
+
/** @description Invalid or expired token */
|
|
5737
|
+
401: {
|
|
5738
|
+
headers: {
|
|
5739
|
+
[name: string]: unknown;
|
|
5740
|
+
};
|
|
5741
|
+
content: {
|
|
5742
|
+
/**
|
|
5743
|
+
* @example {
|
|
5744
|
+
* "error": {
|
|
5745
|
+
* "statusCode": 401,
|
|
5746
|
+
* "code": null,
|
|
5747
|
+
* "message": "Invalid or expired token",
|
|
5748
|
+
* "timestamp": "2026-04-20T12:00:00.000Z",
|
|
5749
|
+
* "path": "/api/v1/nodes",
|
|
5750
|
+
* "method": "GET"
|
|
5751
|
+
* }
|
|
5752
|
+
* }
|
|
5753
|
+
*/
|
|
5754
|
+
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
5755
|
+
};
|
|
5756
|
+
};
|
|
5757
|
+
/** @description This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa) */
|
|
5758
|
+
403: {
|
|
5759
|
+
headers: {
|
|
5760
|
+
[name: string]: unknown;
|
|
5761
|
+
};
|
|
5762
|
+
content: {
|
|
5763
|
+
/**
|
|
5764
|
+
* @example {
|
|
5765
|
+
* "error": {
|
|
5766
|
+
* "statusCode": 403,
|
|
5767
|
+
* "code": null,
|
|
5768
|
+
* "message": "This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)",
|
|
5769
|
+
* "timestamp": "2026-04-20T12:00:00.000Z",
|
|
5770
|
+
* "path": "/api/v1/nodes",
|
|
5771
|
+
* "method": "GET"
|
|
5772
|
+
* }
|
|
5773
|
+
* }
|
|
5774
|
+
*/
|
|
5775
|
+
"application/json": components["schemas"]["ErrorResponseDto"];
|
|
5776
|
+
};
|
|
5777
|
+
};
|
|
5778
|
+
};
|
|
5779
|
+
};
|
|
5441
5780
|
ApiNodesController_getNode: {
|
|
5442
5781
|
parameters: {
|
|
5443
5782
|
query?: never;
|
|
@@ -9436,6 +9775,27 @@ type QueryParams<Id extends OperationId> = operations[Id] extends {
|
|
|
9436
9775
|
} ? NonNullable<Query> : Record<string, never>;
|
|
9437
9776
|
/** Shorthand for a named schema, e.g. `Schema<"EvaluateResponseDto">`. */
|
|
9438
9777
|
type Schema<Name extends keyof components["schemas"]> = components["schemas"][Name];
|
|
9778
|
+
/**
|
|
9779
|
+
* Optimistic concurrency, on the operations whose spec declares `If-Match`.
|
|
9780
|
+
*
|
|
9781
|
+
* Pass the `version` you read off the resource. If someone else has changed it
|
|
9782
|
+
* since, the API answers 409 rather than letting the write silently clobber
|
|
9783
|
+
* theirs — so this is how a read-modify-write is made safe.
|
|
9784
|
+
*/
|
|
9785
|
+
interface ConcurrencyOptions {
|
|
9786
|
+
ifMatch?: string;
|
|
9787
|
+
}
|
|
9788
|
+
/**
|
|
9789
|
+
* The `If-Match` header for a call that supports it, shaped to be spread into
|
|
9790
|
+
* `RequestOptions`.
|
|
9791
|
+
*
|
|
9792
|
+
* Spreadable rather than returning `headers: undefined`, which
|
|
9793
|
+
* `exactOptionalPropertyTypes` rejects — an absent property and one explicitly
|
|
9794
|
+
* set to undefined are not the same thing here.
|
|
9795
|
+
*/
|
|
9796
|
+
declare function withConcurrency(options?: ConcurrencyOptions): {
|
|
9797
|
+
headers: Record<string, string>;
|
|
9798
|
+
} | Record<string, never>;
|
|
9439
9799
|
|
|
9440
9800
|
/**
|
|
9441
9801
|
* Role grants: which identity holds which role, at which node.
|
|
@@ -9490,8 +9850,18 @@ declare class Identities {
|
|
|
9490
9850
|
*/
|
|
9491
9851
|
deactivate(id: string): Promise<ResponseBody<"ApiIdentitiesController_deactivateIdentity">>;
|
|
9492
9852
|
activate(id: string): Promise<ResponseBody<"ApiIdentitiesController_activateIdentity">>;
|
|
9493
|
-
/**
|
|
9494
|
-
|
|
9853
|
+
/**
|
|
9854
|
+
* Every role this identity holds, and where — across all pages.
|
|
9855
|
+
*
|
|
9856
|
+
* Paginated (20 per page by default), so this returns a `Paginator` rather
|
|
9857
|
+
* than one response. Reading a single page here would under-report what an
|
|
9858
|
+
* identity can do, which is the dangerous direction to be wrong in.
|
|
9859
|
+
*
|
|
9860
|
+
* ```ts
|
|
9861
|
+
* for await (const assignment of canopy.identities.assignments(id)) { … }
|
|
9862
|
+
* ```
|
|
9863
|
+
*/
|
|
9864
|
+
assignments(id: string, query?: QueryParams<"ApiIdentitiesController_getIdentityAssignments">): Paginator<IdentityAssignmentItem>;
|
|
9495
9865
|
/**
|
|
9496
9866
|
* The permissions this identity effectively holds, inheritance resolved.
|
|
9497
9867
|
*
|
|
@@ -9502,6 +9872,8 @@ declare class Identities {
|
|
|
9502
9872
|
permissions(id: string): Promise<ResponseBody<"ApiIdentitiesController_getIdentityPermissions">>;
|
|
9503
9873
|
}
|
|
9504
9874
|
type IdentityItem = ItemOf$2<ResponseBody<"ApiIdentitiesController_listIdentities">>;
|
|
9875
|
+
/** One role grant from an identity's assignments collection. */
|
|
9876
|
+
type IdentityAssignmentItem = ItemOf$2<ResponseBody<"ApiIdentitiesController_getIdentityAssignments">>;
|
|
9505
9877
|
type ItemOf$2<T> = T extends {
|
|
9506
9878
|
items: (infer Item)[];
|
|
9507
9879
|
} ? Item : never;
|
|
@@ -9524,28 +9896,37 @@ declare class Permissions {
|
|
|
9524
9896
|
* `effective_node_id: null` — that answer must never be used to guard a
|
|
9525
9897
|
* resource that belongs to a specific node, which is why the scope is a
|
|
9526
9898
|
* required field rather than a default.
|
|
9899
|
+
*
|
|
9900
|
+
* This runs on the request path, so it is the call most worth passing
|
|
9901
|
+
* `signal` and a tight `timeoutMs` to: without them a slow answer here holds
|
|
9902
|
+
* an inbound request open for the client-wide deadline on every attempt.
|
|
9527
9903
|
*/
|
|
9528
|
-
evaluate(input: RequestBody<"ApiPermissionsController_evaluate"
|
|
9904
|
+
evaluate(input: RequestBody<"ApiPermissionsController_evaluate">, options?: CallOptions): Promise<ResponseBody<"ApiPermissionsController_evaluate">>;
|
|
9529
9905
|
/**
|
|
9530
9906
|
* Evaluate many decisions in one round trip.
|
|
9531
9907
|
*
|
|
9532
9908
|
* Prefer this to a loop over `evaluate` when rendering a screen: the checks
|
|
9533
9909
|
* are answered together instead of paying request latency for each.
|
|
9534
9910
|
*/
|
|
9535
|
-
evaluateBulk(input: RequestBody<"ApiPermissionsController_evaluateBulk"
|
|
9911
|
+
evaluateBulk(input: RequestBody<"ApiPermissionsController_evaluateBulk">, options?: CallOptions): Promise<ResponseBody<"ApiPermissionsController_evaluateBulk">>;
|
|
9536
9912
|
/**
|
|
9537
9913
|
* The same decision with its reasoning — which role granted it, which node
|
|
9538
9914
|
* it was inherited from. For debugging an unexpected allow or deny, not for
|
|
9539
9915
|
* the enforcement path.
|
|
9540
9916
|
*/
|
|
9541
|
-
explain(input: RequestBody<"ApiPermissionsController_explain"
|
|
9917
|
+
explain(input: RequestBody<"ApiPermissionsController_explain">, options?: CallOptions): Promise<ResponseBody<"ApiPermissionsController_explain">>;
|
|
9542
9918
|
/** Every permission in the Environment, page by page. */
|
|
9543
9919
|
list(query?: QueryParams<"ApiPermissionsController_listPermissions">): Paginator<PermissionItem>;
|
|
9544
9920
|
get(id: string): Promise<ResponseBody<"ApiPermissionsController_getPermission">>;
|
|
9545
9921
|
/** Define permissions. The request takes a batch, not a single key. */
|
|
9546
9922
|
create(input: RequestBody<"ApiPermissionsController_createPermissions">): Promise<ResponseBody<"ApiPermissionsController_createPermissions">>;
|
|
9547
|
-
|
|
9548
|
-
|
|
9923
|
+
/**
|
|
9924
|
+
* Pass `ifMatch` with the permission's current `version` to make a
|
|
9925
|
+
* read-modify-write safe — a concurrent edit answers 409 instead of being
|
|
9926
|
+
* silently overwritten.
|
|
9927
|
+
*/
|
|
9928
|
+
update(id: string, input: RequestBody<"ApiPermissionsController_updatePermission">, options?: ConcurrencyOptions): Promise<ResponseBody<"ApiPermissionsController_updatePermission">>;
|
|
9929
|
+
delete(id: string, options?: ConcurrencyOptions): Promise<void>;
|
|
9549
9930
|
}
|
|
9550
9931
|
/**
|
|
9551
9932
|
* One item from the permissions collection.
|
|
@@ -9565,8 +9946,13 @@ declare class Roles {
|
|
|
9565
9946
|
list(query?: QueryParams<"ApiRolesController_listRoles">): Paginator<RoleItem>;
|
|
9566
9947
|
get(id: string): Promise<ResponseBody<"ApiRolesController_getRole">>;
|
|
9567
9948
|
create(input: RequestBody<"ApiRolesController_createRole">): Promise<ResponseBody<"ApiRolesController_createRole">>;
|
|
9568
|
-
|
|
9569
|
-
|
|
9949
|
+
/**
|
|
9950
|
+
* Pass `ifMatch` with the role's current `version` to make a
|
|
9951
|
+
* read-modify-write safe — a concurrent edit answers 409 instead of being
|
|
9952
|
+
* silently overwritten.
|
|
9953
|
+
*/
|
|
9954
|
+
update(id: string, input: RequestBody<"ApiRolesController_updateRole">, options?: ConcurrencyOptions): Promise<ResponseBody<"ApiRolesController_updateRole">>;
|
|
9955
|
+
delete(id: string, options?: ConcurrencyOptions): Promise<void>;
|
|
9570
9956
|
permissions(id: string): Promise<ResponseBody<"ApiRolesController_getRolePermissions">>;
|
|
9571
9957
|
/**
|
|
9572
9958
|
* Replaces the role's permissions wholesale — this is a PUT, so anything
|
|
@@ -9608,13 +9994,113 @@ declare class Canopy {
|
|
|
9608
9994
|
constructor(options: CanopyClientOptions);
|
|
9609
9995
|
}
|
|
9610
9996
|
|
|
9997
|
+
interface LocalAuthorizerOptions {
|
|
9998
|
+
/**
|
|
9999
|
+
* How long grant roots and the hierarchy are reused, in milliseconds.
|
|
10000
|
+
* Defaults to 60s.
|
|
10001
|
+
*
|
|
10002
|
+
* This is a staleness budget, not a performance dial. Shortening it makes
|
|
10003
|
+
* revocation take effect sooner and costs more calls — and below the gap
|
|
10004
|
+
* between a user's requests it stops saving anything at all, because every
|
|
10005
|
+
* request finds the cache expired and refetches. Human-paced traffic has
|
|
10006
|
+
* multi-second gaps, so a value of a few seconds can cost full price for no
|
|
10007
|
+
* benefit.
|
|
10008
|
+
*
|
|
10009
|
+
* **`0` disables caching**, reading fresh on every check. That is the escape
|
|
10010
|
+
* for a caller who cannot accept a stale allow at all — it costs a round trip
|
|
10011
|
+
* per request, which is what this class exists to avoid, so reach for it
|
|
10012
|
+
* knowingly rather than as a default.
|
|
10013
|
+
*/
|
|
10014
|
+
ttlMs?: number;
|
|
10015
|
+
/**
|
|
10016
|
+
* Per-attempt deadline for the reads this class makes.
|
|
10017
|
+
*
|
|
10018
|
+
* A cache hit costs nothing, but a miss happens on the request path and
|
|
10019
|
+
* holds an inbound request open exactly as a per-request check used to — so
|
|
10020
|
+
* the bound still matters, it just applies far less often.
|
|
10021
|
+
*/
|
|
10022
|
+
timeoutMs?: number;
|
|
10023
|
+
/** Retries for those reads. */
|
|
10024
|
+
maxRetries?: number;
|
|
10025
|
+
/** Injectable clock, for tests. */
|
|
10026
|
+
now?: () => number;
|
|
10027
|
+
}
|
|
10028
|
+
interface LocalAuthorizationQuery {
|
|
10029
|
+
identity_id: string;
|
|
10030
|
+
permission: string;
|
|
10031
|
+
/** Defaults to `node`, which is the strict question. */
|
|
10032
|
+
scope?: "node" | "app_wide";
|
|
10033
|
+
node_id?: string;
|
|
10034
|
+
}
|
|
10035
|
+
interface LocalAuthorizerStats {
|
|
10036
|
+
/** Calls made to read an identity's grant roots. */
|
|
10037
|
+
grantFetches: number;
|
|
10038
|
+
/** Calls made to read the hierarchy, including revalidations. */
|
|
10039
|
+
treeRequests: number;
|
|
10040
|
+
/** Revalidations the server answered `304`, so no tree was transferred. */
|
|
10041
|
+
treeNotModified: number;
|
|
10042
|
+
}
|
|
10043
|
+
declare class LocalAuthorizer {
|
|
10044
|
+
private readonly client;
|
|
10045
|
+
private readonly ttlMs;
|
|
10046
|
+
private readonly now;
|
|
10047
|
+
private readonly readOptions;
|
|
10048
|
+
private readonly grants;
|
|
10049
|
+
private tree;
|
|
10050
|
+
/**
|
|
10051
|
+
* In-flight reads, so concurrent requests for the same thing share one call.
|
|
10052
|
+
*
|
|
10053
|
+
* Without this a cold start under load fans out: a hundred simultaneous
|
|
10054
|
+
* requests for one identity would each miss the cache and each fetch, which
|
|
10055
|
+
* is the per-request traffic this class exists to remove, concentrated into
|
|
10056
|
+
* the worst possible moment.
|
|
10057
|
+
*/
|
|
10058
|
+
private readonly pendingGrants;
|
|
10059
|
+
private pendingTree;
|
|
10060
|
+
private stats;
|
|
10061
|
+
constructor(client: CanopyClient, options?: LocalAuthorizerOptions);
|
|
10062
|
+
/**
|
|
10063
|
+
* Whether the identity holds the permission.
|
|
10064
|
+
*
|
|
10065
|
+
* Shaped like the API's own evaluate so a caller can swap one for the other.
|
|
10066
|
+
* A `node` check with no node is a denial rather than an error: a request
|
|
10067
|
+
* whose subject cannot be established is exactly the one that must not pass.
|
|
10068
|
+
*/
|
|
10069
|
+
evaluate(query: LocalAuthorizationQuery, options?: {
|
|
10070
|
+
signal?: AbortSignal;
|
|
10071
|
+
}): Promise<{
|
|
10072
|
+
allowed: boolean;
|
|
10073
|
+
}>;
|
|
10074
|
+
private decide;
|
|
10075
|
+
/** Counters for observability — how much traffic the cache is actually saving. */
|
|
10076
|
+
snapshot(): LocalAuthorizerStats;
|
|
10077
|
+
/**
|
|
10078
|
+
* Drop everything held. Not needed in normal operation, where entries expire
|
|
10079
|
+
* on their own; useful in tests and after a known change.
|
|
10080
|
+
*/
|
|
10081
|
+
invalidate(): void;
|
|
10082
|
+
/** Climb from `nodeId` and look for a grant root among its ancestors. */
|
|
10083
|
+
private holdsAtNode;
|
|
10084
|
+
private grantRootsFor;
|
|
10085
|
+
private fetchGrantRoots;
|
|
10086
|
+
private parents;
|
|
10087
|
+
/**
|
|
10088
|
+
* Revalidate rather than re-read. The hierarchy is the expensive half and
|
|
10089
|
+
* the one that changes least, so the common case is a `304` and no transfer
|
|
10090
|
+
* at all — the tree stays in memory and only its expiry moves.
|
|
10091
|
+
*/
|
|
10092
|
+
private fetchTree;
|
|
10093
|
+
}
|
|
10094
|
+
|
|
9611
10095
|
/**
|
|
9612
10096
|
* Everything this client throws.
|
|
9613
10097
|
*
|
|
9614
|
-
*
|
|
9615
|
-
* API answered and said no, `CanopyConnectionError` means it never
|
|
9616
|
-
*
|
|
9617
|
-
*
|
|
10098
|
+
* Three classes, because callers act on the distinction: `CanopyError` means
|
|
10099
|
+
* the API answered and said no, `CanopyConnectionError` means it never
|
|
10100
|
+
* answered, and `CanopyTokenError` means a token failed local verification
|
|
10101
|
+
* without anything being asked of the API at all. The first is a decision you
|
|
10102
|
+
* may need to surface to a user; the second is usually worth retrying or
|
|
10103
|
+
* alerting on; the third is a 401 for the caller who presented the token.
|
|
9618
10104
|
*/
|
|
9619
10105
|
/** The `error` object inside Canopy's error envelope. */
|
|
9620
10106
|
interface CanopyErrorBody {
|
|
@@ -9651,18 +10137,29 @@ declare class CanopyError extends Error {
|
|
|
9651
10137
|
method: string;
|
|
9652
10138
|
path: string;
|
|
9653
10139
|
};
|
|
10140
|
+
/**
|
|
10141
|
+
* How long to wait before retrying, in milliseconds, when the server said so
|
|
10142
|
+
* via `Retry-After` — populated on a 429, and on any other response that
|
|
10143
|
+
* carries the header. Undefined when the server gave no guidance.
|
|
10144
|
+
*/
|
|
10145
|
+
readonly retryAfterMs: number | undefined;
|
|
9654
10146
|
constructor(body: CanopyErrorBody, request: {
|
|
9655
10147
|
method: string;
|
|
9656
10148
|
path: string;
|
|
9657
|
-
});
|
|
10149
|
+
}, retryAfterMs?: number);
|
|
9658
10150
|
/** A 429. `retryAfterMs` is populated when the server said how long to wait. */
|
|
9659
10151
|
get isRateLimited(): boolean;
|
|
9660
10152
|
/** 401 or 403 — the credential is wrong or not permitted here. */
|
|
9661
10153
|
get isAuthFailure(): boolean;
|
|
9662
10154
|
}
|
|
9663
10155
|
/**
|
|
9664
|
-
* The request never produced a response — DNS failure, connection reset,
|
|
9665
|
-
* timeout
|
|
10156
|
+
* The request never produced a response — DNS failure, connection reset, or a
|
|
10157
|
+
* timeout.
|
|
10158
|
+
*
|
|
10159
|
+
* Not this: a caller's own cancellation. Aborting the `signal` passed to a
|
|
10160
|
+
* request rejects with that abort (`AbortError`, or whatever `signal.reason`
|
|
10161
|
+
* holds) and is never retried, because the caller stopping is an intent rather
|
|
10162
|
+
* than a failure to report.
|
|
9666
10163
|
*
|
|
9667
10164
|
* Kept separate from `CanopyError` because there is no status code and no
|
|
9668
10165
|
* server opinion to report: nothing is known about whether the operation
|
|
@@ -9681,8 +10178,202 @@ declare class CanopyConnectionError extends Error {
|
|
|
9681
10178
|
cause?: unknown;
|
|
9682
10179
|
});
|
|
9683
10180
|
}
|
|
10181
|
+
/**
|
|
10182
|
+
* A token failed verification.
|
|
10183
|
+
*
|
|
10184
|
+
* Separate from `CanopyError` because nothing was asked of the API: the token
|
|
10185
|
+
* was checked here, against keys already held. There is no status code to
|
|
10186
|
+
* report and no server opinion to relay — the decision was local, which is the
|
|
10187
|
+
* whole point of verifying a signature rather than calling an endpoint.
|
|
10188
|
+
*
|
|
10189
|
+
* Branch on `code`; it follows the same dot-notation contract as the API's own
|
|
10190
|
+
* codes. Every one of them means the same thing to an HTTP caller — 401 — so
|
|
10191
|
+
* the code is for your logs and your tests, not usually for the response.
|
|
10192
|
+
*/
|
|
10193
|
+
declare class CanopyTokenError extends Error {
|
|
10194
|
+
readonly name = "CanopyTokenError";
|
|
10195
|
+
/**
|
|
10196
|
+
* One of:
|
|
10197
|
+
*
|
|
10198
|
+
* - `token.malformed` — not a JWS, or a segment would not decode
|
|
10199
|
+
* - `token.unsupported_algorithm` — not RS256; Canopy issues only RS256
|
|
10200
|
+
* - `token.key_not_found` — no published key matches the token's `kid`
|
|
10201
|
+
* - `token.jwks_unavailable` — the key set could not be fetched or parsed
|
|
10202
|
+
* - `token.signature_invalid` — signature does not match the signing key
|
|
10203
|
+
* - `token.expired` / `token.not_yet_valid` — outside its validity window
|
|
10204
|
+
* - `token.issuer_mismatch` — `iss` is not the configured issuer
|
|
10205
|
+
* - `token.audience_mismatch` — `aud` does not include the configured audience
|
|
10206
|
+
* - `token.audience_unverified` — token has an `aud` but none was configured
|
|
10207
|
+
* - `token.preauth_not_allowed` — a pre-auth token, which grants no access
|
|
10208
|
+
*/
|
|
10209
|
+
readonly code: string;
|
|
10210
|
+
constructor(code: string, message: string, options?: {
|
|
10211
|
+
cause?: unknown;
|
|
10212
|
+
});
|
|
10213
|
+
}
|
|
10214
|
+
/**
|
|
10215
|
+
* The local authorizer could not answer, and must not pretend otherwise.
|
|
10216
|
+
*
|
|
10217
|
+
* Distinct from a denial. It is raised when the data a decision needs is not
|
|
10218
|
+
* merely absent but *unreadable* — most often a credential that cannot read the
|
|
10219
|
+
* hierarchy, which comes back as an empty tree rather than an error. Denying
|
|
10220
|
+
* there would be a silent, total false-deny on every node-scoped route while
|
|
10221
|
+
* every response still looked healthy.
|
|
10222
|
+
*
|
|
10223
|
+
* No retry fixes it, so callers should treat it as a misconfiguration rather
|
|
10224
|
+
* than an outage.
|
|
10225
|
+
*/
|
|
10226
|
+
declare class CanopyAuthorizerError extends Error {
|
|
10227
|
+
readonly name = "CanopyAuthorizerError";
|
|
10228
|
+
readonly code: string;
|
|
10229
|
+
constructor(code: string, message: string, options?: {
|
|
10230
|
+
cause?: unknown;
|
|
10231
|
+
});
|
|
10232
|
+
}
|
|
10233
|
+
declare function isCanopyAuthorizerError(error: unknown): error is CanopyAuthorizerError;
|
|
9684
10234
|
/** Narrowing helper that survives bundling and duplicate copies of the package. */
|
|
9685
10235
|
declare function isCanopyError(error: unknown): error is CanopyError;
|
|
9686
10236
|
declare function isCanopyConnectionError(error: unknown): error is CanopyConnectionError;
|
|
10237
|
+
declare function isCanopyTokenError(error: unknown): error is CanopyTokenError;
|
|
10238
|
+
|
|
10239
|
+
interface TokenVerifierOptions {
|
|
10240
|
+
/**
|
|
10241
|
+
* The issuer whose tokens are accepted, matched against `iss` exactly.
|
|
10242
|
+
* Defaults to Canopy's hosted issuer; set it for a self-hosted instance.
|
|
10243
|
+
*
|
|
10244
|
+
* Safe to default because it fails closed: pointed at the wrong issuer, a
|
|
10245
|
+
* token is rejected rather than accepted.
|
|
10246
|
+
*/
|
|
10247
|
+
issuer?: string;
|
|
10248
|
+
/**
|
|
10249
|
+
* Required audience, matched against `aud`.
|
|
10250
|
+
*
|
|
10251
|
+
* Hosted Login (OAuth) tokens carry `aud` — your client id — and Direct API
|
|
10252
|
+
* identity tokens do not. Verifying an OAuth token without setting this
|
|
10253
|
+
* throws rather than ignoring the claim, because `aud` is what stops a token
|
|
10254
|
+
* minted for one client being replayed at another.
|
|
10255
|
+
*/
|
|
10256
|
+
audience?: string;
|
|
10257
|
+
/** Where the signing keys live. Defaults to `${issuer}/.well-known/jwks.json`. */
|
|
10258
|
+
jwksUri?: string;
|
|
10259
|
+
/** How long a fetched key set is reused. Defaults to 10 minutes. */
|
|
10260
|
+
jwksCacheMaxAgeMs?: number;
|
|
10261
|
+
/** Floor between refetches provoked by an unknown `kid`. Defaults to 30s. */
|
|
10262
|
+
jwksMinRefetchIntervalMs?: number;
|
|
10263
|
+
/** Leeway on `exp` and `nbf`, in seconds. Defaults to 60. */
|
|
10264
|
+
clockToleranceSec?: number;
|
|
10265
|
+
/**
|
|
10266
|
+
* Accept pre-auth tokens. Defaults to `false`, and should stay false unless
|
|
10267
|
+
* you are building an account picker.
|
|
10268
|
+
*
|
|
10269
|
+
* A pre-auth token is a genuine, correctly-signed Canopy token issued
|
|
10270
|
+
* partway through a multi-account login: the person proved their password
|
|
10271
|
+
* but has not yet chosen an Account, so the token carries no Account context
|
|
10272
|
+
* and grants nothing. Accepting one as a session is a privilege escalation,
|
|
10273
|
+
* and it is the failure a hand-rolled verifier is most likely to miss —
|
|
10274
|
+
* every signature check passes.
|
|
10275
|
+
*/
|
|
10276
|
+
allowPreAuthTokens?: boolean;
|
|
10277
|
+
/**
|
|
10278
|
+
* Deadline for a single JWKS read, in milliseconds. Defaults to 5s.
|
|
10279
|
+
*
|
|
10280
|
+
* Without one, an issuer that accepts a connection and then never answers
|
|
10281
|
+
* holds every inbound request that needs a key — the verification sits on the
|
|
10282
|
+
* request path, so an unbounded read there is an unbounded request.
|
|
10283
|
+
*/
|
|
10284
|
+
jwksTimeoutMs?: number;
|
|
10285
|
+
/** Injectable for tests and for runtimes with a non-global fetch. */
|
|
10286
|
+
fetch?: typeof globalThis.fetch;
|
|
10287
|
+
}
|
|
10288
|
+
/**
|
|
10289
|
+
* The claims Canopy puts in an access token.
|
|
10290
|
+
*
|
|
10291
|
+
* Named claims are the ones the API guarantees; the index signature keeps
|
|
10292
|
+
* anything added later reachable without a version bump.
|
|
10293
|
+
*/
|
|
10294
|
+
interface CanopyTokenClaims {
|
|
10295
|
+
/** The identity or user this token acts as. */
|
|
10296
|
+
sub: string;
|
|
10297
|
+
/** Which kind of principal `sub` is. */
|
|
10298
|
+
type: "user" | "identity" | "api_key" | "platform";
|
|
10299
|
+
iss: string;
|
|
10300
|
+
exp: number;
|
|
10301
|
+
iat?: number;
|
|
10302
|
+
nbf?: number;
|
|
10303
|
+
aud?: string | string[];
|
|
10304
|
+
account_id?: string;
|
|
10305
|
+
application_id?: string;
|
|
10306
|
+
/** Identity tokens carry this; admin tokens deliberately omit it. */
|
|
10307
|
+
environment_id?: string;
|
|
10308
|
+
account_slug?: string;
|
|
10309
|
+
application_slug?: string;
|
|
10310
|
+
environment_slug?: string;
|
|
10311
|
+
/** Present only on a pre-auth token — see `allowPreAuthTokens`. */
|
|
10312
|
+
token_type?: "preauth";
|
|
10313
|
+
console_access?: "granted" | "none";
|
|
10314
|
+
/** Emitted only when the `permissions` OAuth scope was granted. */
|
|
10315
|
+
permissions?: string[];
|
|
10316
|
+
/** True when `permissions` was truncated; query the API for the full set. */
|
|
10317
|
+
permissions_overflow?: boolean;
|
|
10318
|
+
[claim: string]: unknown;
|
|
10319
|
+
}
|
|
10320
|
+
declare class TokenVerifier {
|
|
10321
|
+
private readonly issuer;
|
|
10322
|
+
private readonly audience;
|
|
10323
|
+
private readonly jwksUri;
|
|
10324
|
+
private readonly jwksCacheMaxAgeMs;
|
|
10325
|
+
private readonly jwksMinRefetchIntervalMs;
|
|
10326
|
+
private readonly jwksTimeoutMs;
|
|
10327
|
+
private readonly clockToleranceSec;
|
|
10328
|
+
private readonly allowPreAuthTokens;
|
|
10329
|
+
private readonly fetchImpl;
|
|
10330
|
+
/** Imported keys by `kid`, so a repeat verification skips the import cost. */
|
|
10331
|
+
private keys;
|
|
10332
|
+
private keysFetchedAt;
|
|
10333
|
+
private lastFetchAttemptAt;
|
|
10334
|
+
/** In-flight fetch, so a burst of requests triggers one call, not N. */
|
|
10335
|
+
private inFlight;
|
|
10336
|
+
constructor(options?: TokenVerifierOptions);
|
|
10337
|
+
/**
|
|
10338
|
+
* Verify a token and return its claims. Throws {@link CanopyTokenError} on
|
|
10339
|
+
* anything short of a full pass — branch on `error.code`.
|
|
10340
|
+
*
|
|
10341
|
+
* Order matters: the signature is checked before any claim is believed, so
|
|
10342
|
+
* nothing downstream ever reads an unverified payload.
|
|
10343
|
+
*/
|
|
10344
|
+
verify(token: string): Promise<CanopyTokenClaims>;
|
|
10345
|
+
/** Everything checked after the signature is known good. */
|
|
10346
|
+
private assertClaims;
|
|
10347
|
+
/**
|
|
10348
|
+
* `aud` is checked when either side mentions it.
|
|
10349
|
+
*
|
|
10350
|
+
* The case worth stating: a token carries `aud` but the verifier was not
|
|
10351
|
+
* configured with one. That is not "no audience to check" — it is an OAuth
|
|
10352
|
+
* token being verified by something that never said which client it is, and
|
|
10353
|
+
* ignoring it would accept a token minted for a different client. So it
|
|
10354
|
+
* throws and names the option.
|
|
10355
|
+
*/
|
|
10356
|
+
private assertAudience;
|
|
10357
|
+
/**
|
|
10358
|
+
* The signing key for a `kid`, fetching the key set when it is stale or when
|
|
10359
|
+
* the `kid` is unknown — the latter is how key rotation is picked up
|
|
10360
|
+
* mid-process, bounded by `jwksMinRefetchIntervalMs`.
|
|
10361
|
+
*/
|
|
10362
|
+
private resolveKey;
|
|
10363
|
+
/**
|
|
10364
|
+
* Whether to await a key-set refresh.
|
|
10365
|
+
*
|
|
10366
|
+
* Two ways to qualify, and the first matters as much as the second. A read
|
|
10367
|
+
* already in flight is joined regardless of the floor: it costs no extra
|
|
10368
|
+
* outbound request, and it is what lets a concurrent burst share one fetch
|
|
10369
|
+
* instead of one caller winning and the rest being turned away.
|
|
10370
|
+
*
|
|
10371
|
+
* Otherwise the floor applies — the same one for every refetch path, so they
|
|
10372
|
+
* cannot drift into having different amplification properties.
|
|
10373
|
+
*/
|
|
10374
|
+
private shouldRefresh;
|
|
10375
|
+
private refreshKeys;
|
|
10376
|
+
private fetchKeys;
|
|
10377
|
+
}
|
|
9687
10378
|
|
|
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 };
|
|
10379
|
+
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 };
|