@canopy-io/node 0.5.0 → 0.7.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/CHANGELOG.md +35 -0
- package/README.md +13 -4
- package/dist/index.cjs +47 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1116 -363
- package/dist/index.d.ts +1116 -363
- package/dist/index.js +47 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @canopy-io/node
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b336767: **Breaking:** the authorizer's cache window is capped at five minutes. `LocalAuthorizer`'s `ttlMs` (and `CanopyModule`'s `authorizationTtlMs`) above `300_000`, below `0`, or not a number now throws a `RangeError` when the authorizer is created. The window is how long a revoked user keeps access, so a longer one is refused rather than honoured. The default (60 seconds) and `0` (read fresh on every check) are unchanged. `MAX_TTL_MS` is exported.
|
|
8
|
+
- b336767: **Breaking:** `TokenVerifier` no longer defaults its issuer. Every Canopy Environment is its own issuer with its own signing keys (`https://auth.canopy-io.com/environments/<id>`), so the old default, the bare hosted origin, rejected every identity token. Pass `environmentId`, or `issuer` on a custom auth domain or self-hosted instance; the constructor throws with neither or both. The key set is read from the issuer's own `/.well-known/jwks.json`.
|
|
9
|
+
|
|
10
|
+
`CanopyModule` now requires `verify` with `environmentId` (or `issuer`), since it builds the verifier at startup.
|
|
11
|
+
|
|
12
|
+
The generated types follow the published API, which now includes OAuth client management (`/api/v1/oauth-clients`); those routes are typed but not wrapped as a resource, like API key management.
|
|
13
|
+
|
|
14
|
+
## 0.6.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 30b76ce: **Breaking:** `identities.deactivate()` and `identities.activate()` are removed, because the API no longer offers them to Environment-scoped keys. An identity's active flag is Account-wide, so a key for one Environment could lock a person out of every other one. To cut an identity off from this key's Environment, use `identities.delete()`, which removes it from this Environment only. Deactivating an identity everywhere is done in the Admin Workspace or through directory sync.
|
|
19
|
+
|
|
20
|
+
`identities.update()` no longer accepts `is_active`, for the same reason.
|
|
21
|
+
|
|
22
|
+
`identities.create()` now documents that `role_id` and `node_id` go together; the API answers 400 when only one is sent, where it used to create the identity with no assignment.
|
|
23
|
+
|
|
24
|
+
Identity invite responses no longer declare `environment_id` and `environment_name`, which the API never returned on an Environment-scoped key. Listing invites accepts `sort_by` (`name`, `email` or `created`) with `order`, which the API now honours.
|
|
25
|
+
|
|
26
|
+
An organization's SSO binding (`organizations.ssoConnections`) now carries `owner_organization_node_id` on its `sso_connection`, naming the organization when its administrator set the connection up from the organization portal. Unbinding such a connection from its organization answers 409 `organization.sso_connection_owned_cannot_unbind`; disable or delete it instead. The connection status enum no longer lists `failed`, which nothing ever set.
|
|
27
|
+
|
|
28
|
+
- 30b76ce: SSO connection responses no longer carry `last_login_failure_at`, `consecutive_failures` or `auto_disabled_at`. Nothing ever set them, so they only ever returned their defaults; a connection is disabled only by a person. `last_login_at` stays and now records each successful sign-in through the connection.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- 30b76ce: Types regenerated from the spec, with no shape change: the descriptions now say that a deactivated role grants nothing to its holders (`is_active`, the explain trace's `active`), and that an empty `allowed_children` list makes a node type a leaf.
|
|
33
|
+
|
|
34
|
+
`Authorizer.invalidate()` documents deactivating or reactivating a role as a change to clear the whole cache for, since it changes access for everyone holding the role at once.
|
|
35
|
+
|
|
36
|
+
The identity invite create and resend descriptions say a `password_reset` invite expires after 24 hours; every other invite still expires after 7 days.
|
|
37
|
+
|
|
3
38
|
## 0.5.0
|
|
4
39
|
|
|
5
40
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -110,8 +110,16 @@ rather than ignored when you have not said which client you are:
|
|
|
110
110
|
new TokenVerifier({ audience: process.env.CANOPY_OAUTH_CLIENT_ID });
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
Every Environment is its own issuer, so name yours:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
new TokenVerifier({ environmentId: process.env.CANOPY_ENVIRONMENT_ID });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
On a custom auth domain or a self-hosted instance, set `issuer` instead
|
|
120
|
+
(`https://auth.acme.com`, or `<origin>/environments/<id>`). The constructor
|
|
121
|
+
throws with neither. Getting it wrong fails closed: tokens are rejected, never
|
|
122
|
+
mistakenly accepted.
|
|
115
123
|
|
|
116
124
|
With an Environment's **organizations** container on, an identity token also
|
|
117
125
|
names the organization the session is acting in and the one role held there. Read the pair through `orgContext` rather than off the raw claims —
|
|
@@ -227,8 +235,9 @@ everything beneath it_, so the roots are never expanded into their descendants.
|
|
|
227
235
|
|
|
228
236
|
Both are held for at most 60 seconds, which is the delay between an access
|
|
229
237
|
change and it taking effect, including a moved node. Set `ttlMs` to change it,
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
up to five minutes (`300_000`); a longer value throws when the authorizer is
|
|
239
|
+
created. Shortening it below the gap between a user's requests saves nothing:
|
|
240
|
+
every request then finds the cache expired and refetches.
|
|
232
241
|
|
|
233
242
|
`canopy.permissions.evaluate` remains for a decision with no staleness at all.
|
|
234
243
|
|
package/dist/index.cjs
CHANGED
|
@@ -84,6 +84,15 @@ function isCanopyTokenError(error) {
|
|
|
84
84
|
return error instanceof Error && error.name === "CanopyTokenError";
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// src/strings.ts
|
|
88
|
+
function withoutTrailingSlashes(value) {
|
|
89
|
+
let end = value.length;
|
|
90
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
91
|
+
end -= 1;
|
|
92
|
+
}
|
|
93
|
+
return value.slice(0, end);
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
// src/client.ts
|
|
88
97
|
function isCursorPagination(pagination) {
|
|
89
98
|
return "next_cursor" in pagination;
|
|
@@ -108,7 +117,7 @@ var CanopyClient = class {
|
|
|
108
117
|
"CanopyClient requires either `apiKey` (server-to-server) or `accessToken` (a user or identity JWT)."
|
|
109
118
|
);
|
|
110
119
|
}
|
|
111
|
-
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL)
|
|
120
|
+
this.baseUrl = withoutTrailingSlashes(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
112
121
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
113
122
|
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
114
123
|
this.maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
@@ -552,29 +561,19 @@ var Identities = class {
|
|
|
552
561
|
{ body: input }
|
|
553
562
|
);
|
|
554
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Takes the identity out of this key's Environment: its membership here is
|
|
566
|
+
* deactivated and its role assignments here are dropped. The identity and
|
|
567
|
+
* its access to other Environments are untouched. Deactivating an identity
|
|
568
|
+
* everywhere is an Account-level action, not available to an
|
|
569
|
+
* Environment-scoped key.
|
|
570
|
+
*/
|
|
555
571
|
delete(id) {
|
|
556
572
|
return this.client.request(
|
|
557
573
|
"DELETE",
|
|
558
574
|
`/api/v1/identities/${encodeURIComponent(id)}`
|
|
559
575
|
);
|
|
560
576
|
}
|
|
561
|
-
/**
|
|
562
|
-
* Deactivation is the reversible counterpart to `delete` — sessions are
|
|
563
|
-
* revoked and sign-in refused, but the identity and its assignments survive.
|
|
564
|
-
* Prefer it for offboarding you may need to undo.
|
|
565
|
-
*/
|
|
566
|
-
deactivate(id) {
|
|
567
|
-
return this.client.request(
|
|
568
|
-
"POST",
|
|
569
|
-
`/api/v1/identities/${encodeURIComponent(id)}/deactivate`
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
activate(id) {
|
|
573
|
-
return this.client.request(
|
|
574
|
-
"POST",
|
|
575
|
-
`/api/v1/identities/${encodeURIComponent(id)}/activate`
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
577
|
/**
|
|
579
578
|
* Every role this identity holds, and where — across all pages.
|
|
580
579
|
*
|
|
@@ -636,7 +635,7 @@ var Identities = class {
|
|
|
636
635
|
* Ends every session the identity holds, so the next refresh on any device
|
|
637
636
|
* fails and the person signs in again. Access tokens already issued keep
|
|
638
637
|
* working until their short lifetime runs out. For a suspected compromise;
|
|
639
|
-
* `
|
|
638
|
+
* `delete` also takes the identity out of this Environment.
|
|
640
639
|
*/
|
|
641
640
|
revokeSessions(id) {
|
|
642
641
|
return this.client.request(
|
|
@@ -1119,6 +1118,7 @@ var Canopy = class {
|
|
|
1119
1118
|
|
|
1120
1119
|
// src/authorizer.ts
|
|
1121
1120
|
var DEFAULT_TTL_MS = 6e4;
|
|
1121
|
+
var MAX_TTL_MS = 3e5;
|
|
1122
1122
|
var MAX_WALK_DEPTH = 256;
|
|
1123
1123
|
var LocalAuthorizer = class {
|
|
1124
1124
|
client;
|
|
@@ -1145,6 +1145,11 @@ var LocalAuthorizer = class {
|
|
|
1145
1145
|
constructor(client, options = {}) {
|
|
1146
1146
|
this.client = client;
|
|
1147
1147
|
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
1148
|
+
if (!Number.isFinite(this.ttlMs) || this.ttlMs < 0 || this.ttlMs > MAX_TTL_MS) {
|
|
1149
|
+
throw new RangeError(
|
|
1150
|
+
`ttlMs must be between 0 and ${MAX_TTL_MS} (five minutes); got ${String(this.ttlMs)}. It is how long a revoked user keeps access, so it is capped.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1148
1153
|
this.now = options.now ?? (() => Date.now());
|
|
1149
1154
|
this.readOptions = {
|
|
1150
1155
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
@@ -1212,7 +1217,9 @@ var LocalAuthorizer = class {
|
|
|
1212
1217
|
* With no argument, everything goes: grants and the hierarchy tree. Not
|
|
1213
1218
|
* needed in normal operation, where entries expire on their own; useful in
|
|
1214
1219
|
* tests and after a change whose reach you cannot name (a role's
|
|
1215
|
-
* permissions edited, a node moved).
|
|
1220
|
+
* permissions edited, a role deactivated or reactivated, a node moved). A
|
|
1221
|
+
* deactivated role grants nothing to anyone holding it, so until the cache
|
|
1222
|
+
* is cleared or expires, its holders keep being allowed locally.
|
|
1216
1223
|
*
|
|
1217
1224
|
* Multi-instance honesty: an invalidation reaches THIS process only. A
|
|
1218
1225
|
* webhook lands on one instance behind a load balancer; the others serve
|
|
@@ -1332,12 +1339,29 @@ var LocalAuthorizer = class {
|
|
|
1332
1339
|
};
|
|
1333
1340
|
|
|
1334
1341
|
// src/verify.ts
|
|
1335
|
-
var
|
|
1342
|
+
var HOSTED_ORIGIN = "https://auth.canopy-io.com";
|
|
1336
1343
|
var DEFAULT_JWKS_CACHE_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
1337
1344
|
var DEFAULT_JWKS_MIN_REFETCH_INTERVAL_MS = 30 * 1e3;
|
|
1338
1345
|
var DEFAULT_JWKS_TIMEOUT_MS = 5e3;
|
|
1339
1346
|
var PRINCIPAL_TYPES = /* @__PURE__ */ new Set(["user", "identity", "api_key", "platform"]);
|
|
1340
1347
|
var DEFAULT_CLOCK_TOLERANCE_SEC = 60;
|
|
1348
|
+
function resolveIssuer(options) {
|
|
1349
|
+
const { environmentId, issuer } = options;
|
|
1350
|
+
if (environmentId && issuer) {
|
|
1351
|
+
throw new TypeError(
|
|
1352
|
+
"TokenVerifier takes environmentId or issuer, not both."
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
if (issuer) {
|
|
1356
|
+
return withoutTrailingSlashes(issuer);
|
|
1357
|
+
}
|
|
1358
|
+
if (environmentId) {
|
|
1359
|
+
return `${HOSTED_ORIGIN}/environments/${environmentId}`;
|
|
1360
|
+
}
|
|
1361
|
+
throw new TypeError(
|
|
1362
|
+
"TokenVerifier needs environmentId (the Environment your tokens come from) or, on a custom auth domain or self-hosted instance, issuer."
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1341
1365
|
function orgContext(claims) {
|
|
1342
1366
|
const { org_id, org_role } = claims;
|
|
1343
1367
|
if (typeof org_id !== "string" || org_id === "" || typeof org_role !== "string" || org_role === "") {
|
|
@@ -1382,7 +1406,7 @@ var TokenVerifier = class {
|
|
|
1382
1406
|
/** In-flight fetch, so a burst of requests triggers one call, not N. */
|
|
1383
1407
|
inFlight = null;
|
|
1384
1408
|
constructor(options = {}) {
|
|
1385
|
-
this.issuer = (options
|
|
1409
|
+
this.issuer = resolveIssuer(options);
|
|
1386
1410
|
this.audience = options.audience;
|
|
1387
1411
|
this.jwksUri = options.jwksUri ?? `${this.issuer}/.well-known/jwks.json`;
|
|
1388
1412
|
this.jwksCacheMaxAgeMs = options.jwksCacheMaxAgeMs ?? DEFAULT_JWKS_CACHE_MAX_AGE_MS;
|
|
@@ -1669,6 +1693,7 @@ exports.CanopyError = CanopyError;
|
|
|
1669
1693
|
exports.CanopyTokenError = CanopyTokenError;
|
|
1670
1694
|
exports.Identities = Identities;
|
|
1671
1695
|
exports.LocalAuthorizer = LocalAuthorizer;
|
|
1696
|
+
exports.MAX_TTL_MS = MAX_TTL_MS;
|
|
1672
1697
|
exports.Organizations = Organizations;
|
|
1673
1698
|
exports.Paginator = Paginator;
|
|
1674
1699
|
exports.Permissions = Permissions;
|