@canopy-io/node 0.6.0 → 0.7.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/CHANGELOG.md +17 -0
- package/README.md +13 -4
- package/dist/index.cjs +45 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1107 -114
- package/dist/index.d.ts +1107 -114
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @canopy-io/node
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 221e502: `LocalAuthorizer` keeps its copy of the hierarchy in about 40% less memory: one id string per node instead of two. A parsed `GET /api/v1/nodes/parents` response holds each node id twice, as the node's own `id` and again in its children's `parent_node_id`, and the cache used to keep both copies. A 50,000-node hierarchy now costs about 4.6 MB rather than 7.4 MB. Answers are unchanged.
|
|
8
|
+
|
|
9
|
+
## 0.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- 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`.
|
|
15
|
+
|
|
16
|
+
`CanopyModule` now requires `verify` with `environmentId` (or `issuer`), since it builds the verifier at startup.
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
3
20
|
## 0.6.0
|
|
4
21
|
|
|
5
22
|
### 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;
|
|
@@ -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 },
|
|
@@ -1310,8 +1325,16 @@ var LocalAuthorizer = class {
|
|
|
1310
1325
|
}
|
|
1311
1326
|
const parents = /* @__PURE__ */ new Map();
|
|
1312
1327
|
if (result.modified) {
|
|
1328
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1313
1329
|
for (const edge of result.data.items) {
|
|
1314
|
-
|
|
1330
|
+
ids.set(edge.id, edge.id);
|
|
1331
|
+
}
|
|
1332
|
+
for (const edge of result.data.items) {
|
|
1333
|
+
const parent = edge.parent_node_id ?? null;
|
|
1334
|
+
parents.set(
|
|
1335
|
+
edge.id,
|
|
1336
|
+
parent === null ? null : ids.get(parent) ?? parent
|
|
1337
|
+
);
|
|
1315
1338
|
}
|
|
1316
1339
|
}
|
|
1317
1340
|
this.tree = {
|
|
@@ -1324,12 +1347,29 @@ var LocalAuthorizer = class {
|
|
|
1324
1347
|
};
|
|
1325
1348
|
|
|
1326
1349
|
// src/verify.ts
|
|
1327
|
-
var
|
|
1350
|
+
var HOSTED_ORIGIN = "https://auth.canopy-io.com";
|
|
1328
1351
|
var DEFAULT_JWKS_CACHE_MAX_AGE_MS = 10 * 60 * 1e3;
|
|
1329
1352
|
var DEFAULT_JWKS_MIN_REFETCH_INTERVAL_MS = 30 * 1e3;
|
|
1330
1353
|
var DEFAULT_JWKS_TIMEOUT_MS = 5e3;
|
|
1331
1354
|
var PRINCIPAL_TYPES = /* @__PURE__ */ new Set(["user", "identity", "api_key", "platform"]);
|
|
1332
1355
|
var DEFAULT_CLOCK_TOLERANCE_SEC = 60;
|
|
1356
|
+
function resolveIssuer(options) {
|
|
1357
|
+
const { environmentId, issuer } = options;
|
|
1358
|
+
if (environmentId && issuer) {
|
|
1359
|
+
throw new TypeError(
|
|
1360
|
+
"TokenVerifier takes environmentId or issuer, not both."
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (issuer) {
|
|
1364
|
+
return withoutTrailingSlashes(issuer);
|
|
1365
|
+
}
|
|
1366
|
+
if (environmentId) {
|
|
1367
|
+
return `${HOSTED_ORIGIN}/environments/${environmentId}`;
|
|
1368
|
+
}
|
|
1369
|
+
throw new TypeError(
|
|
1370
|
+
"TokenVerifier needs environmentId (the Environment your tokens come from) or, on a custom auth domain or self-hosted instance, issuer."
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1333
1373
|
function orgContext(claims) {
|
|
1334
1374
|
const { org_id, org_role } = claims;
|
|
1335
1375
|
if (typeof org_id !== "string" || org_id === "" || typeof org_role !== "string" || org_role === "") {
|
|
@@ -1374,7 +1414,7 @@ var TokenVerifier = class {
|
|
|
1374
1414
|
/** In-flight fetch, so a burst of requests triggers one call, not N. */
|
|
1375
1415
|
inFlight = null;
|
|
1376
1416
|
constructor(options = {}) {
|
|
1377
|
-
this.issuer = (options
|
|
1417
|
+
this.issuer = resolveIssuer(options);
|
|
1378
1418
|
this.audience = options.audience;
|
|
1379
1419
|
this.jwksUri = options.jwksUri ?? `${this.issuer}/.well-known/jwks.json`;
|
|
1380
1420
|
this.jwksCacheMaxAgeMs = options.jwksCacheMaxAgeMs ?? DEFAULT_JWKS_CACHE_MAX_AGE_MS;
|
|
@@ -1661,6 +1701,7 @@ exports.CanopyError = CanopyError;
|
|
|
1661
1701
|
exports.CanopyTokenError = CanopyTokenError;
|
|
1662
1702
|
exports.Identities = Identities;
|
|
1663
1703
|
exports.LocalAuthorizer = LocalAuthorizer;
|
|
1704
|
+
exports.MAX_TTL_MS = MAX_TTL_MS;
|
|
1664
1705
|
exports.Organizations = Organizations;
|
|
1665
1706
|
exports.Paginator = Paginator;
|
|
1666
1707
|
exports.Permissions = Permissions;
|