@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 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
- Self-hosted instances set `issuer`. Getting it wrong fails closed: tokens are
114
- rejected, never mistakenly accepted.
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
- knowing that shortening it below the gap between a user's requests saves
231
- nothing — every request then finds the cache expired and refetches.
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).replace(/\/+$/, "");
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
- * `deactivate` also refuses the next sign-in.
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 DEFAULT_ISSUER = "https://auth.canopy-io.com";
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.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
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;