@canopy-io/node 0.10.0 → 0.12.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 +25 -0
- package/README.md +61 -4
- package/dist/index.cjs +288 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1819 -277
- package/dist/index.d.ts +1819 -277
- package/dist/index.js +288 -68
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -471,6 +471,14 @@ function paginate(fetchPage, initial = {}, options = {}) {
|
|
|
471
471
|
return new Paginator(fetchPage, initial, options);
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// src/schema.ts
|
|
475
|
+
function withConcurrency(options = {}) {
|
|
476
|
+
if (options.ifMatch === void 0) {
|
|
477
|
+
return {};
|
|
478
|
+
}
|
|
479
|
+
return { headers: { "If-Match": options.ifMatch } };
|
|
480
|
+
}
|
|
481
|
+
|
|
474
482
|
// src/resources/assignments.ts
|
|
475
483
|
var Assignments = class {
|
|
476
484
|
constructor(client) {
|
|
@@ -496,11 +504,16 @@ var Assignments = class {
|
|
|
496
504
|
create(input) {
|
|
497
505
|
return this.client.request("POST", "/api/v1/assignments", { body: input });
|
|
498
506
|
}
|
|
499
|
-
|
|
507
|
+
/**
|
|
508
|
+
* Change the role or the window of a grant. Pass the `version` the
|
|
509
|
+
* assignment was read with as `ifMatch`; a stale one answers 409
|
|
510
|
+
* `assignment.version_mismatch` rather than overwriting a newer change.
|
|
511
|
+
*/
|
|
512
|
+
update(id, input, options = {}) {
|
|
500
513
|
return this.client.request(
|
|
501
514
|
"PATCH",
|
|
502
515
|
`/api/v1/assignments/${encodeURIComponent(id)}`,
|
|
503
|
-
{ body: input }
|
|
516
|
+
{ body: input, ...withConcurrency(options) }
|
|
504
517
|
);
|
|
505
518
|
}
|
|
506
519
|
delete(id) {
|
|
@@ -602,10 +615,28 @@ var Identities = class {
|
|
|
602
615
|
* question at the node that matters. This is for showing a person what
|
|
603
616
|
* someone can do.
|
|
604
617
|
*/
|
|
605
|
-
permissions(id) {
|
|
618
|
+
permissions(id, query = {}) {
|
|
619
|
+
return this.client.request(
|
|
620
|
+
"GET",
|
|
621
|
+
`/api/v1/identities/${encodeURIComponent(id)}/permissions`,
|
|
622
|
+
{ query: { ...query } }
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Where the identity may act with one permission, answered by Canopy for a
|
|
627
|
+
* list query: the minimal grant roots, and every node beneath them up to
|
|
628
|
+
* `max_nodes` (null past it, with `complete: false`), or `env_wide` when a
|
|
629
|
+
* root is the Environment's own root and no filter is needed.
|
|
630
|
+
*
|
|
631
|
+
* `LocalAuthorizer.accessibleNodes` answers the same question from its
|
|
632
|
+
* cache without a call; this is the fresh answer, and the one for a process
|
|
633
|
+
* that does not run the authorizer.
|
|
634
|
+
*/
|
|
635
|
+
accessibleNodes(id, query) {
|
|
606
636
|
return this.client.request(
|
|
607
637
|
"GET",
|
|
608
|
-
`/api/v1/identities/${encodeURIComponent(id)}/
|
|
638
|
+
`/api/v1/identities/${encodeURIComponent(id)}/accessible-nodes`,
|
|
639
|
+
{ query: { ...query } }
|
|
609
640
|
);
|
|
610
641
|
}
|
|
611
642
|
/**
|
|
@@ -657,14 +688,6 @@ var Identities = class {
|
|
|
657
688
|
}
|
|
658
689
|
};
|
|
659
690
|
|
|
660
|
-
// src/schema.ts
|
|
661
|
-
function withConcurrency(options = {}) {
|
|
662
|
-
if (options.ifMatch === void 0) {
|
|
663
|
-
return {};
|
|
664
|
-
}
|
|
665
|
-
return { headers: { "If-Match": options.ifMatch } };
|
|
666
|
-
}
|
|
667
|
-
|
|
668
691
|
// src/resources/organizations.ts
|
|
669
692
|
var Organizations = class {
|
|
670
693
|
constructor(client) {
|
|
@@ -714,7 +737,7 @@ var Organizations = class {
|
|
|
714
737
|
deleteAll() {
|
|
715
738
|
return this.client.request("DELETE", "/api/v1/organizations");
|
|
716
739
|
}
|
|
717
|
-
// ── Members:
|
|
740
|
+
// ── Members: admission to the organization, apart from the roles held in it ──
|
|
718
741
|
listMembers(id, query = {}) {
|
|
719
742
|
return paginate(
|
|
720
743
|
(params) => this.client.request("GET", `/api/v1/organizations/${enc(id)}/members`, {
|
|
@@ -730,14 +753,12 @@ var Organizations = class {
|
|
|
730
753
|
{ body: input }
|
|
731
754
|
);
|
|
732
755
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
/** Revokes that organization's one role; the identity and its other memberships are untouched. */
|
|
756
|
+
/**
|
|
757
|
+
* Deactivates the membership and revokes every role the member holds at
|
|
758
|
+
* the organization's nodes; the identity and its other memberships are
|
|
759
|
+
* untouched. A role in the organization is changed through the
|
|
760
|
+
* organization's assignment routes, not here.
|
|
761
|
+
*/
|
|
741
762
|
removeMember(id, identityId) {
|
|
742
763
|
return this.client.request(
|
|
743
764
|
"DELETE",
|
|
@@ -1149,6 +1170,7 @@ var Canopy = class {
|
|
|
1149
1170
|
// src/authorizer.ts
|
|
1150
1171
|
var DEFAULT_TTL_MS = 6e4;
|
|
1151
1172
|
var MAX_TTL_MS = 3e5;
|
|
1173
|
+
var DEFAULT_MAX_NODES = 1e4;
|
|
1152
1174
|
var MAX_WALK_DEPTH = 256;
|
|
1153
1175
|
var PAGE_SIZE = 100;
|
|
1154
1176
|
var MAX_PAGES = 1e4;
|
|
@@ -1165,11 +1187,20 @@ function understood(page, path) {
|
|
|
1165
1187
|
}
|
|
1166
1188
|
var GRANTS_PATH = (identityId) => `/api/v1/identities/${encodeURIComponent(identityId)}/grants`;
|
|
1167
1189
|
var PARENTS_PATH = "/api/v1/nodes/parents";
|
|
1190
|
+
function inEffect(window, now) {
|
|
1191
|
+
return (window.from === null || window.from <= now) && (window.to === null || window.to > now);
|
|
1192
|
+
}
|
|
1193
|
+
function reasonAmong(windows, now) {
|
|
1194
|
+
return windows.some((window) => window.to !== null && window.to <= now) ? "expired" : "not_yet_effective";
|
|
1195
|
+
}
|
|
1168
1196
|
var LocalAuthorizer = class {
|
|
1169
1197
|
client;
|
|
1170
1198
|
ttlMs;
|
|
1199
|
+
maxNodes;
|
|
1171
1200
|
now;
|
|
1172
1201
|
readOptions;
|
|
1202
|
+
onDecision;
|
|
1203
|
+
onDenied;
|
|
1173
1204
|
grants = /* @__PURE__ */ new Map();
|
|
1174
1205
|
tree;
|
|
1175
1206
|
/**
|
|
@@ -1197,22 +1228,39 @@ var LocalAuthorizer = class {
|
|
|
1197
1228
|
`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.`
|
|
1198
1229
|
);
|
|
1199
1230
|
}
|
|
1231
|
+
this.maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
1232
|
+
if (!Number.isInteger(this.maxNodes) || this.maxNodes < 1) {
|
|
1233
|
+
throw new RangeError(
|
|
1234
|
+
`maxNodes must be a positive integer; got ${String(this.maxNodes)}.`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1200
1237
|
this.now = options.now ?? (() => Date.now());
|
|
1238
|
+
this.onDecision = options.onDecision;
|
|
1239
|
+
this.onDenied = options.onDenied;
|
|
1201
1240
|
this.readOptions = {
|
|
1202
1241
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
1203
1242
|
...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries, maxBackoffMs: options.timeoutMs }
|
|
1204
1243
|
};
|
|
1205
1244
|
}
|
|
1206
1245
|
/**
|
|
1207
|
-
* Whether the identity holds the permission.
|
|
1246
|
+
* Whether the identity holds the permission, and why not when it does not.
|
|
1208
1247
|
*
|
|
1209
1248
|
* Shaped like the API's own evaluate so a caller can swap one for the other.
|
|
1210
1249
|
* A `node` check with no node is a denial rather than an error: a request
|
|
1211
1250
|
* whose subject cannot be established is exactly the one that must not pass.
|
|
1212
1251
|
*/
|
|
1213
1252
|
async evaluate(query, options = {}) {
|
|
1253
|
+
const started = this.now();
|
|
1254
|
+
const decided = (decision) => {
|
|
1255
|
+
this.announce({
|
|
1256
|
+
query,
|
|
1257
|
+
decision,
|
|
1258
|
+
durationMs: Math.max(0, this.now() - started)
|
|
1259
|
+
});
|
|
1260
|
+
return decision;
|
|
1261
|
+
};
|
|
1214
1262
|
if (!options.signal) {
|
|
1215
|
-
return this.decide(query);
|
|
1263
|
+
return decided(await this.decide(query));
|
|
1216
1264
|
}
|
|
1217
1265
|
const signal = options.signal;
|
|
1218
1266
|
let onAbort;
|
|
@@ -1229,25 +1277,157 @@ var LocalAuthorizer = class {
|
|
|
1229
1277
|
onAbort = fail;
|
|
1230
1278
|
signal.addEventListener("abort", fail, { once: true });
|
|
1231
1279
|
});
|
|
1232
|
-
return Promise.race([this.decide(query), aborted]).finally(() => {
|
|
1280
|
+
return Promise.race([this.decide(query), aborted]).then(decided).finally(() => {
|
|
1233
1281
|
if (onAbort) {
|
|
1234
1282
|
signal.removeEventListener("abort", onAbort);
|
|
1235
1283
|
}
|
|
1236
1284
|
});
|
|
1237
1285
|
}
|
|
1286
|
+
/**
|
|
1287
|
+
* The nodes the identity holds the permission at, in effect now, reduced to
|
|
1288
|
+
* the ones no other such node already covers, and whether one of them is
|
|
1289
|
+
* the Environment's root. The roots alone are enough for an application
|
|
1290
|
+
* that keeps a closure table or a materialised path: it filters by "beneath
|
|
1291
|
+
* one of these" without an expanded set.
|
|
1292
|
+
*
|
|
1293
|
+
* Reads the tree, since minimality and `envWide` are questions about it; a
|
|
1294
|
+
* root the readable hierarchy does not contain raises
|
|
1295
|
+
* `authorizer.hierarchy_incomplete`, as a check at such a node does.
|
|
1296
|
+
*/
|
|
1297
|
+
async grantRoots(identityId, permission) {
|
|
1298
|
+
const now = this.now();
|
|
1299
|
+
const held = (await this.grantRootsFor(identityId)).get(permission) ?? [];
|
|
1300
|
+
const granted = new Set(
|
|
1301
|
+
held.filter((window) => inEffect(window, now)).map((w) => w.node)
|
|
1302
|
+
);
|
|
1303
|
+
if (granted.size === 0) {
|
|
1304
|
+
return { roots: [], envWide: false };
|
|
1305
|
+
}
|
|
1306
|
+
const { parents } = await this.treeEntry();
|
|
1307
|
+
const roots = [];
|
|
1308
|
+
let envWide = false;
|
|
1309
|
+
for (const node of granted) {
|
|
1310
|
+
if (!parents.has(node)) {
|
|
1311
|
+
throw this.hierarchyIncomplete(node);
|
|
1312
|
+
}
|
|
1313
|
+
if (parents.get(node) === null) {
|
|
1314
|
+
envWide = true;
|
|
1315
|
+
}
|
|
1316
|
+
let covered = false;
|
|
1317
|
+
let depth = 0;
|
|
1318
|
+
for (let current = parents.get(node); current && depth < MAX_WALK_DEPTH; current = parents.get(current), depth += 1) {
|
|
1319
|
+
if (granted.has(current)) {
|
|
1320
|
+
covered = true;
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
if (!covered) {
|
|
1325
|
+
roots.push(node);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
return { roots, envWide };
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Where the identity may act with the permission, as the set a list query
|
|
1332
|
+
* filters by: {@link grantRoots} expanded through their descendants, up to
|
|
1333
|
+
* `maxNodes` (the authorizer's default unless given here).
|
|
1334
|
+
*
|
|
1335
|
+
* Past the cap `nodes` is null and `complete` false: the set is too large
|
|
1336
|
+
* to be worth an `IN (...)` list, and the caller filters by `roots`
|
|
1337
|
+
* instead. An `envWide` answer carries no set either, since every node
|
|
1338
|
+
* qualifies and a filter would only slow the query down.
|
|
1339
|
+
*
|
|
1340
|
+
* Computed from the same cached grant roots and tree a check uses, so a
|
|
1341
|
+
* list filtered by it and the check on a write cannot disagree, and it goes
|
|
1342
|
+
* stale on the same `ttlMs`.
|
|
1343
|
+
*/
|
|
1344
|
+
async accessibleNodes(identityId, permission, options = {}) {
|
|
1345
|
+
const cap = options.maxNodes ?? this.maxNodes;
|
|
1346
|
+
const { roots, envWide } = await this.grantRoots(identityId, permission);
|
|
1347
|
+
if (envWide) {
|
|
1348
|
+
return { roots, envWide: true, nodes: null, complete: true };
|
|
1349
|
+
}
|
|
1350
|
+
if (roots.length === 0) {
|
|
1351
|
+
return { roots, envWide: false, nodes: /* @__PURE__ */ new Set(), complete: true };
|
|
1352
|
+
}
|
|
1353
|
+
const { children } = await this.treeEntry();
|
|
1354
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
1355
|
+
const pending = [...roots];
|
|
1356
|
+
while (pending.length > 0) {
|
|
1357
|
+
const node = pending.pop();
|
|
1358
|
+
if (nodes.has(node)) {
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
nodes.add(node);
|
|
1362
|
+
if (nodes.size > cap) {
|
|
1363
|
+
return { roots, envWide: false, nodes: null, complete: false };
|
|
1364
|
+
}
|
|
1365
|
+
for (const child of children.get(node) ?? []) {
|
|
1366
|
+
pending.push(child);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return { roots, envWide: false, nodes, complete: true };
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Every permission the identity holds at the node, by grant or by
|
|
1373
|
+
* inheritance: the list a screen renders its controls from, answered from
|
|
1374
|
+
* the same cached grant roots and tree a check uses.
|
|
1375
|
+
*/
|
|
1376
|
+
async permissionsAt(identityId, nodeId) {
|
|
1377
|
+
const answers = await this.permissionsAtMany(identityId, [nodeId]);
|
|
1378
|
+
return answers.get(nodeId) ?? [];
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* {@link permissionsAt} for several nodes at once, as node → permissions:
|
|
1382
|
+
* the rows of a list each with their own controls, answered from one read
|
|
1383
|
+
* of the grant roots and one walk per node.
|
|
1384
|
+
*/
|
|
1385
|
+
async permissionsAtMany(identityId, nodeIds) {
|
|
1386
|
+
const now = this.now();
|
|
1387
|
+
const roots = await this.grantRootsFor(identityId);
|
|
1388
|
+
const answers = /* @__PURE__ */ new Map();
|
|
1389
|
+
for (const nodeId of new Set(nodeIds)) {
|
|
1390
|
+
const path = await this.lineageOf(nodeId);
|
|
1391
|
+
const held = [];
|
|
1392
|
+
for (const [permission, windows] of roots) {
|
|
1393
|
+
if (windows.some(
|
|
1394
|
+
(window) => path.has(window.node) && inEffect(window, now)
|
|
1395
|
+
)) {
|
|
1396
|
+
held.push(permission);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
answers.set(nodeId, held.sort());
|
|
1400
|
+
}
|
|
1401
|
+
return answers;
|
|
1402
|
+
}
|
|
1238
1403
|
async decide(query) {
|
|
1239
|
-
const
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1242
|
-
|
|
1404
|
+
const now = this.now();
|
|
1405
|
+
const held = (await this.grantRootsFor(query.identity_id)).get(
|
|
1406
|
+
query.permission
|
|
1407
|
+
);
|
|
1408
|
+
if (!held || held.length === 0) {
|
|
1409
|
+
return { allowed: false, reason: "permission_not_held" };
|
|
1243
1410
|
}
|
|
1244
1411
|
if ((query.scope ?? "node") === "app_wide") {
|
|
1245
|
-
return { allowed: true };
|
|
1412
|
+
return held.some((window) => inEffect(window, now)) ? { allowed: true, reason: null } : { allowed: false, reason: reasonAmong(held, now) };
|
|
1246
1413
|
}
|
|
1247
1414
|
if (!query.node_id) {
|
|
1248
|
-
return { allowed: false };
|
|
1415
|
+
return { allowed: false, reason: "node_missing" };
|
|
1416
|
+
}
|
|
1417
|
+
if (held.some(
|
|
1418
|
+
(window) => window.node === query.node_id && inEffect(window, now)
|
|
1419
|
+
)) {
|
|
1420
|
+
return { allowed: true, reason: null };
|
|
1421
|
+
}
|
|
1422
|
+
const path = await this.lineageOf(query.node_id);
|
|
1423
|
+
const reachable = held.filter((window) => path.has(window.node));
|
|
1424
|
+
if (reachable.some((window) => inEffect(window, now))) {
|
|
1425
|
+
return { allowed: true, reason: null };
|
|
1249
1426
|
}
|
|
1250
|
-
|
|
1427
|
+
if (reachable.length === 0) {
|
|
1428
|
+
return { allowed: false, reason: "outside_grants" };
|
|
1429
|
+
}
|
|
1430
|
+
return { allowed: false, reason: reasonAmong(reachable, now) };
|
|
1251
1431
|
}
|
|
1252
1432
|
/** Counters for observability — how much traffic the cache is actually saving. */
|
|
1253
1433
|
snapshot() {
|
|
@@ -1283,28 +1463,42 @@ var LocalAuthorizer = class {
|
|
|
1283
1463
|
this.grants.clear();
|
|
1284
1464
|
this.tree = void 0;
|
|
1285
1465
|
}
|
|
1286
|
-
/**
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1466
|
+
/** Fire the decision hooks, after the verdict and without waiting on them. */
|
|
1467
|
+
announce(event) {
|
|
1468
|
+
for (const hook of [
|
|
1469
|
+
this.onDecision,
|
|
1470
|
+
event.decision.allowed ? void 0 : this.onDenied
|
|
1471
|
+
]) {
|
|
1472
|
+
if (!hook) {
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
try {
|
|
1476
|
+
hook(event);
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1290
1479
|
}
|
|
1291
|
-
|
|
1480
|
+
}
|
|
1481
|
+
/** The node and its ancestors, from the tree this client can read. */
|
|
1482
|
+
async lineageOf(nodeId) {
|
|
1483
|
+
const { parents } = await this.treeEntry();
|
|
1292
1484
|
if (!parents.has(nodeId)) {
|
|
1293
|
-
throw
|
|
1294
|
-
"authorizer.hierarchy_incomplete",
|
|
1295
|
-
`The hierarchy this client can read does not contain node "${nodeId}", so authorization at that node cannot be decided. A scoped API key needs the \`hierarchy.view\` scope to read the tree.`
|
|
1296
|
-
);
|
|
1485
|
+
throw this.hierarchyIncomplete(nodeId);
|
|
1297
1486
|
}
|
|
1487
|
+
const path = /* @__PURE__ */ new Set([nodeId]);
|
|
1298
1488
|
let current = parents.get(nodeId);
|
|
1299
1489
|
let depth = 0;
|
|
1300
1490
|
while (current && depth < MAX_WALK_DEPTH) {
|
|
1301
|
-
|
|
1302
|
-
return true;
|
|
1303
|
-
}
|
|
1491
|
+
path.add(current);
|
|
1304
1492
|
current = parents.get(current);
|
|
1305
1493
|
depth += 1;
|
|
1306
1494
|
}
|
|
1307
|
-
return
|
|
1495
|
+
return path;
|
|
1496
|
+
}
|
|
1497
|
+
hierarchyIncomplete(nodeId) {
|
|
1498
|
+
return new CanopyAuthorizerError(
|
|
1499
|
+
"authorizer.hierarchy_incomplete",
|
|
1500
|
+
`The hierarchy this client can read does not contain node "${nodeId}", so authorization at that node cannot be decided. A scoped API key needs the \`hierarchy.view\` scope to read the tree.`
|
|
1501
|
+
);
|
|
1308
1502
|
}
|
|
1309
1503
|
async grantRootsFor(identityId) {
|
|
1310
1504
|
const cached = this.grants.get(identityId);
|
|
@@ -1322,12 +1516,19 @@ var LocalAuthorizer = class {
|
|
|
1322
1516
|
return read;
|
|
1323
1517
|
}
|
|
1324
1518
|
/**
|
|
1325
|
-
* Every page of the identity's grant roots,
|
|
1519
|
+
* Every page of the identity's grant roots, with their windows, folded
|
|
1520
|
+
* into permission → roots.
|
|
1326
1521
|
*
|
|
1327
|
-
* Each row is one permission at one node, so a
|
|
1328
|
-
* nodes arrives as three rows, possibly on
|
|
1329
|
-
* the first page would under-report what
|
|
1330
|
-
* the safe direction, but a wrong answer
|
|
1522
|
+
* Each row is one permission at one node for one assignment, so a
|
|
1523
|
+
* permission granted at three nodes arrives as three rows, possibly on
|
|
1524
|
+
* different pages. Stopping after the first page would under-report what
|
|
1525
|
+
* the identity holds: a false deny, the safe direction, but a wrong answer
|
|
1526
|
+
* all the same.
|
|
1527
|
+
*
|
|
1528
|
+
* Rows outside their window are kept: they are what lets a denial say
|
|
1529
|
+
* `expired` or `not_yet_effective`, and the entry expires at the nearest
|
|
1530
|
+
* window boundary when that comes before the TTL, so a grant takes effect
|
|
1531
|
+
* or ends on time rather than up to `ttlMs` late.
|
|
1331
1532
|
*/
|
|
1332
1533
|
async fetchGrantRoots(identityId) {
|
|
1333
1534
|
this.stats.grantFetches += 1;
|
|
@@ -1337,30 +1538,42 @@ var LocalAuthorizer = class {
|
|
|
1337
1538
|
this.stats.grantPages += 1;
|
|
1338
1539
|
return this.client.request("GET", path, {
|
|
1339
1540
|
...this.readOptions,
|
|
1340
|
-
query
|
|
1541
|
+
query: { ...query, include: "windows" }
|
|
1341
1542
|
}).then((page) => understood(page, path));
|
|
1342
1543
|
},
|
|
1343
1544
|
{ page: 1, take: PAGE_SIZE },
|
|
1344
1545
|
{ maxPages: MAX_PAGES }
|
|
1345
1546
|
);
|
|
1346
1547
|
const roots = /* @__PURE__ */ new Map();
|
|
1548
|
+
const now = this.now();
|
|
1549
|
+
let boundary = Number.POSITIVE_INFINITY;
|
|
1347
1550
|
for await (const row of rows) {
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1551
|
+
const window = {
|
|
1552
|
+
node: row.node_id,
|
|
1553
|
+
from: row.effective_from ? Date.parse(row.effective_from) : null,
|
|
1554
|
+
to: row.effective_to ? Date.parse(row.effective_to) : null
|
|
1555
|
+
};
|
|
1556
|
+
for (const edge of [window.from, window.to]) {
|
|
1557
|
+
if (edge !== null && edge > now && edge < boundary) {
|
|
1558
|
+
boundary = edge;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
const windows = roots.get(row.permission);
|
|
1562
|
+
if (windows) {
|
|
1563
|
+
windows.push(window);
|
|
1351
1564
|
} else {
|
|
1352
|
-
roots.set(row.permission,
|
|
1565
|
+
roots.set(row.permission, [window]);
|
|
1353
1566
|
}
|
|
1354
1567
|
}
|
|
1355
1568
|
this.grants.set(identityId, {
|
|
1356
1569
|
roots,
|
|
1357
|
-
expiresAt:
|
|
1570
|
+
expiresAt: Math.min(now + this.ttlMs, boundary)
|
|
1358
1571
|
});
|
|
1359
1572
|
return roots;
|
|
1360
1573
|
}
|
|
1361
|
-
async
|
|
1574
|
+
async treeEntry() {
|
|
1362
1575
|
if (this.tree && this.tree.expiresAt > this.now()) {
|
|
1363
|
-
return this.tree
|
|
1576
|
+
return this.tree;
|
|
1364
1577
|
}
|
|
1365
1578
|
if (this.pendingTree) {
|
|
1366
1579
|
return this.pendingTree;
|
|
@@ -1397,7 +1610,7 @@ var LocalAuthorizer = class {
|
|
|
1397
1610
|
if (held) {
|
|
1398
1611
|
this.stats.treeNotModified += 1;
|
|
1399
1612
|
this.tree = { ...held, expiresAt: this.now() + this.ttlMs };
|
|
1400
|
-
return
|
|
1613
|
+
return this.tree;
|
|
1401
1614
|
}
|
|
1402
1615
|
return this.keepTree([], null);
|
|
1403
1616
|
}
|
|
@@ -1458,19 +1671,26 @@ var LocalAuthorizer = class {
|
|
|
1458
1671
|
ids.set(edge.id, edge.id);
|
|
1459
1672
|
}
|
|
1460
1673
|
const parents = /* @__PURE__ */ new Map();
|
|
1674
|
+
const children = /* @__PURE__ */ new Map();
|
|
1461
1675
|
for (const edge of edges) {
|
|
1462
|
-
const parent = edge.parent_node_id ??
|
|
1463
|
-
parents.set(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1676
|
+
const parent = edge.parent_node_id === null || edge.parent_node_id === void 0 ? null : ids.get(edge.parent_node_id) ?? edge.parent_node_id;
|
|
1677
|
+
parents.set(edge.id, parent);
|
|
1678
|
+
if (parent !== null) {
|
|
1679
|
+
const siblings = children.get(parent);
|
|
1680
|
+
if (siblings) {
|
|
1681
|
+
siblings.push(edge.id);
|
|
1682
|
+
} else {
|
|
1683
|
+
children.set(parent, [edge.id]);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1467
1686
|
}
|
|
1468
1687
|
this.tree = {
|
|
1469
1688
|
parents,
|
|
1689
|
+
children,
|
|
1470
1690
|
etag,
|
|
1471
1691
|
expiresAt: this.now() + this.ttlMs
|
|
1472
1692
|
};
|
|
1473
|
-
return
|
|
1693
|
+
return this.tree;
|
|
1474
1694
|
}
|
|
1475
1695
|
};
|
|
1476
1696
|
function spansPages(first) {
|
|
@@ -1552,11 +1772,11 @@ function resolveIssuer(options) {
|
|
|
1552
1772
|
);
|
|
1553
1773
|
}
|
|
1554
1774
|
function orgContext(claims) {
|
|
1555
|
-
const { org_id,
|
|
1556
|
-
if (typeof org_id !== "string" || org_id === "" || typeof
|
|
1775
|
+
const { org_id, org_roles } = claims;
|
|
1776
|
+
if (typeof org_id !== "string" || org_id === "" || !Array.isArray(org_roles) || org_roles.some((role) => typeof role !== "string" || role === "")) {
|
|
1557
1777
|
return null;
|
|
1558
1778
|
}
|
|
1559
|
-
return { orgId: org_id,
|
|
1779
|
+
return { orgId: org_id, orgRoles: [...org_roles] };
|
|
1560
1780
|
}
|
|
1561
1781
|
function decodeBase64Url(value) {
|
|
1562
1782
|
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -1906,6 +2126,6 @@ function isWebhookEvent(delivery, name) {
|
|
|
1906
2126
|
return delivery.event === name;
|
|
1907
2127
|
}
|
|
1908
2128
|
|
|
1909
|
-
export { Assignments, Canopy, CanopyAuthorizerError, CanopyClient, CanopyConnectionError, CanopyError, CanopyTokenError, Identities, LocalAuthorizer, MAX_TTL_MS, Organizations, Paginator, Permissions, Roles, TokenVerifier, authorizationErrorStatus, isCanopyAuthorizerError, isCanopyConnectionError, isCanopyError, isCanopyTokenError, isCursorPagination, isWebhookEvent, orgContext, paginate, tokenErrorStatus, withConcurrency };
|
|
2129
|
+
export { Assignments, Canopy, CanopyAuthorizerError, CanopyClient, CanopyConnectionError, CanopyError, CanopyTokenError, DEFAULT_MAX_NODES, Identities, LocalAuthorizer, MAX_TTL_MS, Organizations, Paginator, Permissions, Roles, TokenVerifier, authorizationErrorStatus, isCanopyAuthorizerError, isCanopyConnectionError, isCanopyError, isCanopyTokenError, isCursorPagination, isWebhookEvent, orgContext, paginate, tokenErrorStatus, withConcurrency };
|
|
1910
2130
|
//# sourceMappingURL=index.js.map
|
|
1911
2131
|
//# sourceMappingURL=index.js.map
|