@canopy-io/node 0.9.0 → 0.11.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 +27 -0
- package/README.md +53 -2
- package/dist/index.cjs +284 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -20
- package/dist/index.d.ts +316 -20
- package/dist/index.js +284 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -602,10 +602,28 @@ var Identities = class {
|
|
|
602
602
|
* question at the node that matters. This is for showing a person what
|
|
603
603
|
* someone can do.
|
|
604
604
|
*/
|
|
605
|
-
permissions(id) {
|
|
605
|
+
permissions(id, query = {}) {
|
|
606
|
+
return this.client.request(
|
|
607
|
+
"GET",
|
|
608
|
+
`/api/v1/identities/${encodeURIComponent(id)}/permissions`,
|
|
609
|
+
{ query: { ...query } }
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Where the identity may act with one permission, answered by Canopy for a
|
|
614
|
+
* list query: the minimal grant roots, and every node beneath them up to
|
|
615
|
+
* `max_nodes` (null past it, with `complete: false`), or `env_wide` when a
|
|
616
|
+
* root is the Environment's own root and no filter is needed.
|
|
617
|
+
*
|
|
618
|
+
* `LocalAuthorizer.accessibleNodes` answers the same question from its
|
|
619
|
+
* cache without a call; this is the fresh answer, and the one for a process
|
|
620
|
+
* that does not run the authorizer.
|
|
621
|
+
*/
|
|
622
|
+
accessibleNodes(id, query) {
|
|
606
623
|
return this.client.request(
|
|
607
624
|
"GET",
|
|
608
|
-
`/api/v1/identities/${encodeURIComponent(id)}/
|
|
625
|
+
`/api/v1/identities/${encodeURIComponent(id)}/accessible-nodes`,
|
|
626
|
+
{ query: { ...query } }
|
|
609
627
|
);
|
|
610
628
|
}
|
|
611
629
|
/**
|
|
@@ -1149,17 +1167,37 @@ var Canopy = class {
|
|
|
1149
1167
|
// src/authorizer.ts
|
|
1150
1168
|
var DEFAULT_TTL_MS = 6e4;
|
|
1151
1169
|
var MAX_TTL_MS = 3e5;
|
|
1170
|
+
var DEFAULT_MAX_NODES = 1e4;
|
|
1152
1171
|
var MAX_WALK_DEPTH = 256;
|
|
1153
1172
|
var PAGE_SIZE = 100;
|
|
1154
1173
|
var MAX_PAGES = 1e4;
|
|
1155
1174
|
var MAX_TREE_WALKS = 3;
|
|
1175
|
+
var KNOWN_MODEL_VERSIONS = /* @__PURE__ */ new Set([1]);
|
|
1176
|
+
function understood(page, path) {
|
|
1177
|
+
if (!KNOWN_MODEL_VERSIONS.has(page.model_version)) {
|
|
1178
|
+
throw new CanopyAuthorizerError(
|
|
1179
|
+
"authorizer.model_version_unknown",
|
|
1180
|
+
`${path} describes authorization model version ${page.model_version}, which this version of @canopy-io/node does not understand. It knows ${[...KNOWN_MODEL_VERSIONS].join(", ")}. Upgrade @canopy-io/node; until then this authorizer cannot decide, rather than guess.`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
return page;
|
|
1184
|
+
}
|
|
1156
1185
|
var GRANTS_PATH = (identityId) => `/api/v1/identities/${encodeURIComponent(identityId)}/grants`;
|
|
1157
1186
|
var PARENTS_PATH = "/api/v1/nodes/parents";
|
|
1187
|
+
function inEffect(window, now) {
|
|
1188
|
+
return (window.from === null || window.from <= now) && (window.to === null || window.to > now);
|
|
1189
|
+
}
|
|
1190
|
+
function reasonAmong(windows, now) {
|
|
1191
|
+
return windows.some((window) => window.to !== null && window.to <= now) ? "expired" : "not_yet_effective";
|
|
1192
|
+
}
|
|
1158
1193
|
var LocalAuthorizer = class {
|
|
1159
1194
|
client;
|
|
1160
1195
|
ttlMs;
|
|
1196
|
+
maxNodes;
|
|
1161
1197
|
now;
|
|
1162
1198
|
readOptions;
|
|
1199
|
+
onDecision;
|
|
1200
|
+
onDenied;
|
|
1163
1201
|
grants = /* @__PURE__ */ new Map();
|
|
1164
1202
|
tree;
|
|
1165
1203
|
/**
|
|
@@ -1187,22 +1225,39 @@ var LocalAuthorizer = class {
|
|
|
1187
1225
|
`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.`
|
|
1188
1226
|
);
|
|
1189
1227
|
}
|
|
1228
|
+
this.maxNodes = options.maxNodes ?? DEFAULT_MAX_NODES;
|
|
1229
|
+
if (!Number.isInteger(this.maxNodes) || this.maxNodes < 1) {
|
|
1230
|
+
throw new RangeError(
|
|
1231
|
+
`maxNodes must be a positive integer; got ${String(this.maxNodes)}.`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1190
1234
|
this.now = options.now ?? (() => Date.now());
|
|
1235
|
+
this.onDecision = options.onDecision;
|
|
1236
|
+
this.onDenied = options.onDenied;
|
|
1191
1237
|
this.readOptions = {
|
|
1192
1238
|
...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
|
|
1193
1239
|
...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries, maxBackoffMs: options.timeoutMs }
|
|
1194
1240
|
};
|
|
1195
1241
|
}
|
|
1196
1242
|
/**
|
|
1197
|
-
* Whether the identity holds the permission.
|
|
1243
|
+
* Whether the identity holds the permission, and why not when it does not.
|
|
1198
1244
|
*
|
|
1199
1245
|
* Shaped like the API's own evaluate so a caller can swap one for the other.
|
|
1200
1246
|
* A `node` check with no node is a denial rather than an error: a request
|
|
1201
1247
|
* whose subject cannot be established is exactly the one that must not pass.
|
|
1202
1248
|
*/
|
|
1203
1249
|
async evaluate(query, options = {}) {
|
|
1250
|
+
const started = this.now();
|
|
1251
|
+
const decided = (decision) => {
|
|
1252
|
+
this.announce({
|
|
1253
|
+
query,
|
|
1254
|
+
decision,
|
|
1255
|
+
durationMs: Math.max(0, this.now() - started)
|
|
1256
|
+
});
|
|
1257
|
+
return decision;
|
|
1258
|
+
};
|
|
1204
1259
|
if (!options.signal) {
|
|
1205
|
-
return this.decide(query);
|
|
1260
|
+
return decided(await this.decide(query));
|
|
1206
1261
|
}
|
|
1207
1262
|
const signal = options.signal;
|
|
1208
1263
|
let onAbort;
|
|
@@ -1219,25 +1274,157 @@ var LocalAuthorizer = class {
|
|
|
1219
1274
|
onAbort = fail;
|
|
1220
1275
|
signal.addEventListener("abort", fail, { once: true });
|
|
1221
1276
|
});
|
|
1222
|
-
return Promise.race([this.decide(query), aborted]).finally(() => {
|
|
1277
|
+
return Promise.race([this.decide(query), aborted]).then(decided).finally(() => {
|
|
1223
1278
|
if (onAbort) {
|
|
1224
1279
|
signal.removeEventListener("abort", onAbort);
|
|
1225
1280
|
}
|
|
1226
1281
|
});
|
|
1227
1282
|
}
|
|
1283
|
+
/**
|
|
1284
|
+
* The nodes the identity holds the permission at, in effect now, reduced to
|
|
1285
|
+
* the ones no other such node already covers, and whether one of them is
|
|
1286
|
+
* the Environment's root. The roots alone are enough for an application
|
|
1287
|
+
* that keeps a closure table or a materialised path: it filters by "beneath
|
|
1288
|
+
* one of these" without an expanded set.
|
|
1289
|
+
*
|
|
1290
|
+
* Reads the tree, since minimality and `envWide` are questions about it; a
|
|
1291
|
+
* root the readable hierarchy does not contain raises
|
|
1292
|
+
* `authorizer.hierarchy_incomplete`, as a check at such a node does.
|
|
1293
|
+
*/
|
|
1294
|
+
async grantRoots(identityId, permission) {
|
|
1295
|
+
const now = this.now();
|
|
1296
|
+
const held = (await this.grantRootsFor(identityId)).get(permission) ?? [];
|
|
1297
|
+
const granted = new Set(
|
|
1298
|
+
held.filter((window) => inEffect(window, now)).map((w) => w.node)
|
|
1299
|
+
);
|
|
1300
|
+
if (granted.size === 0) {
|
|
1301
|
+
return { roots: [], envWide: false };
|
|
1302
|
+
}
|
|
1303
|
+
const { parents } = await this.treeEntry();
|
|
1304
|
+
const roots = [];
|
|
1305
|
+
let envWide = false;
|
|
1306
|
+
for (const node of granted) {
|
|
1307
|
+
if (!parents.has(node)) {
|
|
1308
|
+
throw this.hierarchyIncomplete(node);
|
|
1309
|
+
}
|
|
1310
|
+
if (parents.get(node) === null) {
|
|
1311
|
+
envWide = true;
|
|
1312
|
+
}
|
|
1313
|
+
let covered = false;
|
|
1314
|
+
let depth = 0;
|
|
1315
|
+
for (let current = parents.get(node); current && depth < MAX_WALK_DEPTH; current = parents.get(current), depth += 1) {
|
|
1316
|
+
if (granted.has(current)) {
|
|
1317
|
+
covered = true;
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (!covered) {
|
|
1322
|
+
roots.push(node);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return { roots, envWide };
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Where the identity may act with the permission, as the set a list query
|
|
1329
|
+
* filters by: {@link grantRoots} expanded through their descendants, up to
|
|
1330
|
+
* `maxNodes` (the authorizer's default unless given here).
|
|
1331
|
+
*
|
|
1332
|
+
* Past the cap `nodes` is null and `complete` false: the set is too large
|
|
1333
|
+
* to be worth an `IN (...)` list, and the caller filters by `roots`
|
|
1334
|
+
* instead. An `envWide` answer carries no set either, since every node
|
|
1335
|
+
* qualifies and a filter would only slow the query down.
|
|
1336
|
+
*
|
|
1337
|
+
* Computed from the same cached grant roots and tree a check uses, so a
|
|
1338
|
+
* list filtered by it and the check on a write cannot disagree, and it goes
|
|
1339
|
+
* stale on the same `ttlMs`.
|
|
1340
|
+
*/
|
|
1341
|
+
async accessibleNodes(identityId, permission, options = {}) {
|
|
1342
|
+
const cap = options.maxNodes ?? this.maxNodes;
|
|
1343
|
+
const { roots, envWide } = await this.grantRoots(identityId, permission);
|
|
1344
|
+
if (envWide) {
|
|
1345
|
+
return { roots, envWide: true, nodes: null, complete: true };
|
|
1346
|
+
}
|
|
1347
|
+
if (roots.length === 0) {
|
|
1348
|
+
return { roots, envWide: false, nodes: /* @__PURE__ */ new Set(), complete: true };
|
|
1349
|
+
}
|
|
1350
|
+
const { children } = await this.treeEntry();
|
|
1351
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
1352
|
+
const pending = [...roots];
|
|
1353
|
+
while (pending.length > 0) {
|
|
1354
|
+
const node = pending.pop();
|
|
1355
|
+
if (nodes.has(node)) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1358
|
+
nodes.add(node);
|
|
1359
|
+
if (nodes.size > cap) {
|
|
1360
|
+
return { roots, envWide: false, nodes: null, complete: false };
|
|
1361
|
+
}
|
|
1362
|
+
for (const child of children.get(node) ?? []) {
|
|
1363
|
+
pending.push(child);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return { roots, envWide: false, nodes, complete: true };
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Every permission the identity holds at the node, by grant or by
|
|
1370
|
+
* inheritance: the list a screen renders its controls from, answered from
|
|
1371
|
+
* the same cached grant roots and tree a check uses.
|
|
1372
|
+
*/
|
|
1373
|
+
async permissionsAt(identityId, nodeId) {
|
|
1374
|
+
const answers = await this.permissionsAtMany(identityId, [nodeId]);
|
|
1375
|
+
return answers.get(nodeId) ?? [];
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* {@link permissionsAt} for several nodes at once, as node → permissions:
|
|
1379
|
+
* the rows of a list each with their own controls, answered from one read
|
|
1380
|
+
* of the grant roots and one walk per node.
|
|
1381
|
+
*/
|
|
1382
|
+
async permissionsAtMany(identityId, nodeIds) {
|
|
1383
|
+
const now = this.now();
|
|
1384
|
+
const roots = await this.grantRootsFor(identityId);
|
|
1385
|
+
const answers = /* @__PURE__ */ new Map();
|
|
1386
|
+
for (const nodeId of new Set(nodeIds)) {
|
|
1387
|
+
const path = await this.lineageOf(nodeId);
|
|
1388
|
+
const held = [];
|
|
1389
|
+
for (const [permission, windows] of roots) {
|
|
1390
|
+
if (windows.some(
|
|
1391
|
+
(window) => path.has(window.node) && inEffect(window, now)
|
|
1392
|
+
)) {
|
|
1393
|
+
held.push(permission);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
answers.set(nodeId, held.sort());
|
|
1397
|
+
}
|
|
1398
|
+
return answers;
|
|
1399
|
+
}
|
|
1228
1400
|
async decide(query) {
|
|
1229
|
-
const
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1401
|
+
const now = this.now();
|
|
1402
|
+
const held = (await this.grantRootsFor(query.identity_id)).get(
|
|
1403
|
+
query.permission
|
|
1404
|
+
);
|
|
1405
|
+
if (!held || held.length === 0) {
|
|
1406
|
+
return { allowed: false, reason: "permission_not_held" };
|
|
1233
1407
|
}
|
|
1234
1408
|
if ((query.scope ?? "node") === "app_wide") {
|
|
1235
|
-
return { allowed: true };
|
|
1409
|
+
return held.some((window) => inEffect(window, now)) ? { allowed: true, reason: null } : { allowed: false, reason: reasonAmong(held, now) };
|
|
1236
1410
|
}
|
|
1237
1411
|
if (!query.node_id) {
|
|
1238
|
-
return { allowed: false };
|
|
1412
|
+
return { allowed: false, reason: "node_missing" };
|
|
1239
1413
|
}
|
|
1240
|
-
|
|
1414
|
+
if (held.some(
|
|
1415
|
+
(window) => window.node === query.node_id && inEffect(window, now)
|
|
1416
|
+
)) {
|
|
1417
|
+
return { allowed: true, reason: null };
|
|
1418
|
+
}
|
|
1419
|
+
const path = await this.lineageOf(query.node_id);
|
|
1420
|
+
const reachable = held.filter((window) => path.has(window.node));
|
|
1421
|
+
if (reachable.some((window) => inEffect(window, now))) {
|
|
1422
|
+
return { allowed: true, reason: null };
|
|
1423
|
+
}
|
|
1424
|
+
if (reachable.length === 0) {
|
|
1425
|
+
return { allowed: false, reason: "outside_grants" };
|
|
1426
|
+
}
|
|
1427
|
+
return { allowed: false, reason: reasonAmong(reachable, now) };
|
|
1241
1428
|
}
|
|
1242
1429
|
/** Counters for observability — how much traffic the cache is actually saving. */
|
|
1243
1430
|
snapshot() {
|
|
@@ -1273,28 +1460,42 @@ var LocalAuthorizer = class {
|
|
|
1273
1460
|
this.grants.clear();
|
|
1274
1461
|
this.tree = void 0;
|
|
1275
1462
|
}
|
|
1276
|
-
/**
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1463
|
+
/** Fire the decision hooks, after the verdict and without waiting on them. */
|
|
1464
|
+
announce(event) {
|
|
1465
|
+
for (const hook of [
|
|
1466
|
+
this.onDecision,
|
|
1467
|
+
event.decision.allowed ? void 0 : this.onDenied
|
|
1468
|
+
]) {
|
|
1469
|
+
if (!hook) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
hook(event);
|
|
1474
|
+
} catch {
|
|
1475
|
+
}
|
|
1280
1476
|
}
|
|
1281
|
-
|
|
1477
|
+
}
|
|
1478
|
+
/** The node and its ancestors, from the tree this client can read. */
|
|
1479
|
+
async lineageOf(nodeId) {
|
|
1480
|
+
const { parents } = await this.treeEntry();
|
|
1282
1481
|
if (!parents.has(nodeId)) {
|
|
1283
|
-
throw
|
|
1284
|
-
"authorizer.hierarchy_incomplete",
|
|
1285
|
-
`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.`
|
|
1286
|
-
);
|
|
1482
|
+
throw this.hierarchyIncomplete(nodeId);
|
|
1287
1483
|
}
|
|
1484
|
+
const path = /* @__PURE__ */ new Set([nodeId]);
|
|
1288
1485
|
let current = parents.get(nodeId);
|
|
1289
1486
|
let depth = 0;
|
|
1290
1487
|
while (current && depth < MAX_WALK_DEPTH) {
|
|
1291
|
-
|
|
1292
|
-
return true;
|
|
1293
|
-
}
|
|
1488
|
+
path.add(current);
|
|
1294
1489
|
current = parents.get(current);
|
|
1295
1490
|
depth += 1;
|
|
1296
1491
|
}
|
|
1297
|
-
return
|
|
1492
|
+
return path;
|
|
1493
|
+
}
|
|
1494
|
+
hierarchyIncomplete(nodeId) {
|
|
1495
|
+
return new CanopyAuthorizerError(
|
|
1496
|
+
"authorizer.hierarchy_incomplete",
|
|
1497
|
+
`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.`
|
|
1498
|
+
);
|
|
1298
1499
|
}
|
|
1299
1500
|
async grantRootsFor(identityId) {
|
|
1300
1501
|
const cached = this.grants.get(identityId);
|
|
@@ -1312,12 +1513,19 @@ var LocalAuthorizer = class {
|
|
|
1312
1513
|
return read;
|
|
1313
1514
|
}
|
|
1314
1515
|
/**
|
|
1315
|
-
* Every page of the identity's grant roots,
|
|
1516
|
+
* Every page of the identity's grant roots, with their windows, folded
|
|
1517
|
+
* into permission → roots.
|
|
1316
1518
|
*
|
|
1317
|
-
* Each row is one permission at one node, so a
|
|
1318
|
-
* nodes arrives as three rows, possibly on
|
|
1319
|
-
* the first page would under-report what
|
|
1320
|
-
* the safe direction, but a wrong answer
|
|
1519
|
+
* Each row is one permission at one node for one assignment, so a
|
|
1520
|
+
* permission granted at three nodes arrives as three rows, possibly on
|
|
1521
|
+
* different pages. Stopping after the first page would under-report what
|
|
1522
|
+
* the identity holds: a false deny, the safe direction, but a wrong answer
|
|
1523
|
+
* all the same.
|
|
1524
|
+
*
|
|
1525
|
+
* Rows outside their window are kept: they are what lets a denial say
|
|
1526
|
+
* `expired` or `not_yet_effective`, and the entry expires at the nearest
|
|
1527
|
+
* window boundary when that comes before the TTL, so a grant takes effect
|
|
1528
|
+
* or ends on time rather than up to `ttlMs` late.
|
|
1321
1529
|
*/
|
|
1322
1530
|
async fetchGrantRoots(identityId) {
|
|
1323
1531
|
this.stats.grantFetches += 1;
|
|
@@ -1327,30 +1535,42 @@ var LocalAuthorizer = class {
|
|
|
1327
1535
|
this.stats.grantPages += 1;
|
|
1328
1536
|
return this.client.request("GET", path, {
|
|
1329
1537
|
...this.readOptions,
|
|
1330
|
-
query
|
|
1331
|
-
});
|
|
1538
|
+
query: { ...query, include: "windows" }
|
|
1539
|
+
}).then((page) => understood(page, path));
|
|
1332
1540
|
},
|
|
1333
1541
|
{ page: 1, take: PAGE_SIZE },
|
|
1334
1542
|
{ maxPages: MAX_PAGES }
|
|
1335
1543
|
);
|
|
1336
1544
|
const roots = /* @__PURE__ */ new Map();
|
|
1545
|
+
const now = this.now();
|
|
1546
|
+
let boundary = Number.POSITIVE_INFINITY;
|
|
1337
1547
|
for await (const row of rows) {
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1548
|
+
const window = {
|
|
1549
|
+
node: row.node_id,
|
|
1550
|
+
from: row.effective_from ? Date.parse(row.effective_from) : null,
|
|
1551
|
+
to: row.effective_to ? Date.parse(row.effective_to) : null
|
|
1552
|
+
};
|
|
1553
|
+
for (const edge of [window.from, window.to]) {
|
|
1554
|
+
if (edge !== null && edge > now && edge < boundary) {
|
|
1555
|
+
boundary = edge;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
const windows = roots.get(row.permission);
|
|
1559
|
+
if (windows) {
|
|
1560
|
+
windows.push(window);
|
|
1341
1561
|
} else {
|
|
1342
|
-
roots.set(row.permission,
|
|
1562
|
+
roots.set(row.permission, [window]);
|
|
1343
1563
|
}
|
|
1344
1564
|
}
|
|
1345
1565
|
this.grants.set(identityId, {
|
|
1346
1566
|
roots,
|
|
1347
|
-
expiresAt:
|
|
1567
|
+
expiresAt: Math.min(now + this.ttlMs, boundary)
|
|
1348
1568
|
});
|
|
1349
1569
|
return roots;
|
|
1350
1570
|
}
|
|
1351
|
-
async
|
|
1571
|
+
async treeEntry() {
|
|
1352
1572
|
if (this.tree && this.tree.expiresAt > this.now()) {
|
|
1353
|
-
return this.tree
|
|
1573
|
+
return this.tree;
|
|
1354
1574
|
}
|
|
1355
1575
|
if (this.pendingTree) {
|
|
1356
1576
|
return this.pendingTree;
|
|
@@ -1387,7 +1607,7 @@ var LocalAuthorizer = class {
|
|
|
1387
1607
|
if (held) {
|
|
1388
1608
|
this.stats.treeNotModified += 1;
|
|
1389
1609
|
this.tree = { ...held, expiresAt: this.now() + this.ttlMs };
|
|
1390
|
-
return
|
|
1610
|
+
return this.tree;
|
|
1391
1611
|
}
|
|
1392
1612
|
return this.keepTree([], null);
|
|
1393
1613
|
}
|
|
@@ -1408,12 +1628,15 @@ var LocalAuthorizer = class {
|
|
|
1408
1628
|
}
|
|
1409
1629
|
parentsFirstPage(etag) {
|
|
1410
1630
|
this.stats.treePages += 1;
|
|
1411
|
-
return this.client.requestConditional(
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1631
|
+
return this.client.requestConditional("GET", PARENTS_PATH, etag, {
|
|
1632
|
+
...this.readOptions,
|
|
1633
|
+
query: { page: 1, take: PAGE_SIZE }
|
|
1634
|
+
}).then((result) => {
|
|
1635
|
+
if (result.modified) {
|
|
1636
|
+
understood(result.data, PARENTS_PATH);
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
});
|
|
1417
1640
|
}
|
|
1418
1641
|
/** The first page's edges followed by every later page's. */
|
|
1419
1642
|
async parentsRemainingPages(first) {
|
|
@@ -1427,7 +1650,7 @@ var LocalAuthorizer = class {
|
|
|
1427
1650
|
return this.client.request("GET", PARENTS_PATH, {
|
|
1428
1651
|
...this.readOptions,
|
|
1429
1652
|
query
|
|
1430
|
-
});
|
|
1653
|
+
}).then((page) => understood(page, PARENTS_PATH));
|
|
1431
1654
|
},
|
|
1432
1655
|
{ page: first.pagination.page + 1, take: PAGE_SIZE },
|
|
1433
1656
|
{ maxPages: MAX_PAGES - 1 }
|
|
@@ -1445,19 +1668,26 @@ var LocalAuthorizer = class {
|
|
|
1445
1668
|
ids.set(edge.id, edge.id);
|
|
1446
1669
|
}
|
|
1447
1670
|
const parents = /* @__PURE__ */ new Map();
|
|
1671
|
+
const children = /* @__PURE__ */ new Map();
|
|
1448
1672
|
for (const edge of edges) {
|
|
1449
|
-
const parent = edge.parent_node_id ??
|
|
1450
|
-
parents.set(
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1673
|
+
const parent = edge.parent_node_id === null || edge.parent_node_id === void 0 ? null : ids.get(edge.parent_node_id) ?? edge.parent_node_id;
|
|
1674
|
+
parents.set(edge.id, parent);
|
|
1675
|
+
if (parent !== null) {
|
|
1676
|
+
const siblings = children.get(parent);
|
|
1677
|
+
if (siblings) {
|
|
1678
|
+
siblings.push(edge.id);
|
|
1679
|
+
} else {
|
|
1680
|
+
children.set(parent, [edge.id]);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1454
1683
|
}
|
|
1455
1684
|
this.tree = {
|
|
1456
1685
|
parents,
|
|
1686
|
+
children,
|
|
1457
1687
|
etag,
|
|
1458
1688
|
expiresAt: this.now() + this.ttlMs
|
|
1459
1689
|
};
|
|
1460
|
-
return
|
|
1690
|
+
return this.tree;
|
|
1461
1691
|
}
|
|
1462
1692
|
};
|
|
1463
1693
|
function spansPages(first) {
|
|
@@ -1893,6 +2123,6 @@ function isWebhookEvent(delivery, name) {
|
|
|
1893
2123
|
return delivery.event === name;
|
|
1894
2124
|
}
|
|
1895
2125
|
|
|
1896
|
-
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 };
|
|
2126
|
+
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 };
|
|
1897
2127
|
//# sourceMappingURL=index.js.map
|
|
1898
2128
|
//# sourceMappingURL=index.js.map
|