@canopy-io/node 0.6.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,16 @@
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
+
3
14
  ## 0.6.0
4
15
 
5
16
  ### 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;
@@ -1109,6 +1118,7 @@ var Canopy = class {
1109
1118
 
1110
1119
  // src/authorizer.ts
1111
1120
  var DEFAULT_TTL_MS = 6e4;
1121
+ var MAX_TTL_MS = 3e5;
1112
1122
  var MAX_WALK_DEPTH = 256;
1113
1123
  var LocalAuthorizer = class {
1114
1124
  client;
@@ -1135,6 +1145,11 @@ var LocalAuthorizer = class {
1135
1145
  constructor(client, options = {}) {
1136
1146
  this.client = client;
1137
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
+ }
1138
1153
  this.now = options.now ?? (() => Date.now());
1139
1154
  this.readOptions = {
1140
1155
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
@@ -1324,12 +1339,29 @@ var LocalAuthorizer = class {
1324
1339
  };
1325
1340
 
1326
1341
  // src/verify.ts
1327
- var DEFAULT_ISSUER = "https://auth.canopy-io.com";
1342
+ var HOSTED_ORIGIN = "https://auth.canopy-io.com";
1328
1343
  var DEFAULT_JWKS_CACHE_MAX_AGE_MS = 10 * 60 * 1e3;
1329
1344
  var DEFAULT_JWKS_MIN_REFETCH_INTERVAL_MS = 30 * 1e3;
1330
1345
  var DEFAULT_JWKS_TIMEOUT_MS = 5e3;
1331
1346
  var PRINCIPAL_TYPES = /* @__PURE__ */ new Set(["user", "identity", "api_key", "platform"]);
1332
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
+ }
1333
1365
  function orgContext(claims) {
1334
1366
  const { org_id, org_role } = claims;
1335
1367
  if (typeof org_id !== "string" || org_id === "" || typeof org_role !== "string" || org_role === "") {
@@ -1374,7 +1406,7 @@ var TokenVerifier = class {
1374
1406
  /** In-flight fetch, so a burst of requests triggers one call, not N. */
1375
1407
  inFlight = null;
1376
1408
  constructor(options = {}) {
1377
- this.issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1409
+ this.issuer = resolveIssuer(options);
1378
1410
  this.audience = options.audience;
1379
1411
  this.jwksUri = options.jwksUri ?? `${this.issuer}/.well-known/jwks.json`;
1380
1412
  this.jwksCacheMaxAgeMs = options.jwksCacheMaxAgeMs ?? DEFAULT_JWKS_CACHE_MAX_AGE_MS;
@@ -1661,6 +1693,7 @@ exports.CanopyError = CanopyError;
1661
1693
  exports.CanopyTokenError = CanopyTokenError;
1662
1694
  exports.Identities = Identities;
1663
1695
  exports.LocalAuthorizer = LocalAuthorizer;
1696
+ exports.MAX_TTL_MS = MAX_TTL_MS;
1664
1697
  exports.Organizations = Organizations;
1665
1698
  exports.Paginator = Paginator;
1666
1699
  exports.Permissions = Permissions;