@canopy-io/node 0.8.0 → 0.9.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 +17 -0
- package/README.md +19 -2
- package/dist/index.cjs +154 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +245 -126
- package/dist/index.d.ts +245 -126
- package/dist/index.js +154 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -613,11 +613,23 @@ var Identities = class {
|
|
|
613
613
|
* newest first: when and where it was signed in, when it expires, and the
|
|
614
614
|
* organization it is acting in. Read it back after a revoke to confirm,
|
|
615
615
|
* from your own side, that a sign-out took.
|
|
616
|
+
*
|
|
617
|
+
* Paginated (20 per page by default, `take` up to 100), so this returns a
|
|
618
|
+
* `Paginator`: iterate it for every session, or call `.first()` for the
|
|
619
|
+
* newest. The list takes `page` and `take` only; its order is fixed.
|
|
620
|
+
*
|
|
621
|
+
* ```ts
|
|
622
|
+
* for await (const session of canopy.identities.sessions(id)) { … }
|
|
623
|
+
* ```
|
|
616
624
|
*/
|
|
617
|
-
sessions(id) {
|
|
618
|
-
return
|
|
619
|
-
|
|
620
|
-
|
|
625
|
+
sessions(id, query = {}) {
|
|
626
|
+
return paginate(
|
|
627
|
+
(params) => this.client.request(
|
|
628
|
+
"GET",
|
|
629
|
+
`/api/v1/identities/${encodeURIComponent(id)}/sessions`,
|
|
630
|
+
{ query: params }
|
|
631
|
+
),
|
|
632
|
+
{ ...query }
|
|
621
633
|
);
|
|
622
634
|
}
|
|
623
635
|
/**
|
|
@@ -913,11 +925,25 @@ var Organizations = class {
|
|
|
913
925
|
`/api/v1/organizations/${enc(id)}/directory/activity`
|
|
914
926
|
);
|
|
915
927
|
}
|
|
916
|
-
/**
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
928
|
+
/**
|
|
929
|
+
* The groups the provider has pushed, each with its role mapping.
|
|
930
|
+
*
|
|
931
|
+
* Paginated (20 per page by default, `take` up to 100), so this returns a
|
|
932
|
+
* `Paginator`. Sorted by display name unless `sort_by: "created"`; `q`
|
|
933
|
+
* searches display names and external ids.
|
|
934
|
+
*
|
|
935
|
+
* ```ts
|
|
936
|
+
* for await (const group of canopy.organizations.listDirectoryGroups(id)) { … }
|
|
937
|
+
* ```
|
|
938
|
+
*/
|
|
939
|
+
listDirectoryGroups(id, query = {}) {
|
|
940
|
+
return paginate(
|
|
941
|
+
(params) => this.client.request(
|
|
942
|
+
"GET",
|
|
943
|
+
`/api/v1/organizations/${enc(id)}/directory/groups`,
|
|
944
|
+
{ query: params }
|
|
945
|
+
),
|
|
946
|
+
{ ...query }
|
|
921
947
|
);
|
|
922
948
|
}
|
|
923
949
|
/**
|
|
@@ -979,6 +1005,10 @@ var Permissions = class {
|
|
|
979
1005
|
*
|
|
980
1006
|
* Prefer this to a loop over `evaluate` when rendering a screen: the checks
|
|
981
1007
|
* are answered together instead of paying request latency for each.
|
|
1008
|
+
*
|
|
1009
|
+
* One request carries between 1 and 100 checks. More is refused with a
|
|
1010
|
+
* `400` (`CanopyError`, `statusCode` 400) and nothing is evaluated, so split
|
|
1011
|
+
* a larger set into batches of 100.
|
|
982
1012
|
*/
|
|
983
1013
|
evaluateBulk(input, options = {}) {
|
|
984
1014
|
return this.client.request("POST", "/api/v1/permissions/evaluate/bulk", {
|
|
@@ -1120,6 +1150,11 @@ var Canopy = class {
|
|
|
1120
1150
|
var DEFAULT_TTL_MS = 6e4;
|
|
1121
1151
|
var MAX_TTL_MS = 3e5;
|
|
1122
1152
|
var MAX_WALK_DEPTH = 256;
|
|
1153
|
+
var PAGE_SIZE = 100;
|
|
1154
|
+
var MAX_PAGES = 1e4;
|
|
1155
|
+
var MAX_TREE_WALKS = 3;
|
|
1156
|
+
var GRANTS_PATH = (identityId) => `/api/v1/identities/${encodeURIComponent(identityId)}/grants`;
|
|
1157
|
+
var PARENTS_PATH = "/api/v1/nodes/parents";
|
|
1123
1158
|
var LocalAuthorizer = class {
|
|
1124
1159
|
client;
|
|
1125
1160
|
ttlMs;
|
|
@@ -1139,7 +1174,9 @@ var LocalAuthorizer = class {
|
|
|
1139
1174
|
pendingTree;
|
|
1140
1175
|
stats = {
|
|
1141
1176
|
grantFetches: 0,
|
|
1177
|
+
grantPages: 0,
|
|
1142
1178
|
treeRequests: 0,
|
|
1179
|
+
treePages: 0,
|
|
1143
1180
|
treeNotModified: 0
|
|
1144
1181
|
};
|
|
1145
1182
|
constructor(client, options = {}) {
|
|
@@ -1274,16 +1311,36 @@ var LocalAuthorizer = class {
|
|
|
1274
1311
|
this.pendingGrants.set(identityId, read);
|
|
1275
1312
|
return read;
|
|
1276
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Every page of the identity's grant roots, folded into permission → nodes.
|
|
1316
|
+
*
|
|
1317
|
+
* Each row is one permission at one node, so a permission granted at three
|
|
1318
|
+
* nodes arrives as three rows, possibly on different pages. Stopping after
|
|
1319
|
+
* the first page would under-report what the identity holds: a false deny,
|
|
1320
|
+
* the safe direction, but a wrong answer all the same.
|
|
1321
|
+
*/
|
|
1277
1322
|
async fetchGrantRoots(identityId) {
|
|
1278
1323
|
this.stats.grantFetches += 1;
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1324
|
+
const path = GRANTS_PATH(identityId);
|
|
1325
|
+
const rows = paginate(
|
|
1326
|
+
(query) => {
|
|
1327
|
+
this.stats.grantPages += 1;
|
|
1328
|
+
return this.client.request("GET", path, {
|
|
1329
|
+
...this.readOptions,
|
|
1330
|
+
query
|
|
1331
|
+
});
|
|
1332
|
+
},
|
|
1333
|
+
{ page: 1, take: PAGE_SIZE },
|
|
1334
|
+
{ maxPages: MAX_PAGES }
|
|
1283
1335
|
);
|
|
1284
1336
|
const roots = /* @__PURE__ */ new Map();
|
|
1285
|
-
for (const row of
|
|
1286
|
-
roots.
|
|
1337
|
+
for await (const row of rows) {
|
|
1338
|
+
const nodes = roots.get(row.permission);
|
|
1339
|
+
if (nodes) {
|
|
1340
|
+
nodes.add(row.node_id);
|
|
1341
|
+
} else {
|
|
1342
|
+
roots.set(row.permission, /* @__PURE__ */ new Set([row.node_id]));
|
|
1343
|
+
}
|
|
1287
1344
|
}
|
|
1288
1345
|
this.grants.set(identityId, {
|
|
1289
1346
|
roots,
|
|
@@ -1308,43 +1365,104 @@ var LocalAuthorizer = class {
|
|
|
1308
1365
|
* Revalidate rather than re-read. The hierarchy is the expensive half and
|
|
1309
1366
|
* the one that changes least, so the common case is a `304` and no transfer
|
|
1310
1367
|
* at all — the tree stays in memory and only its expiry moves.
|
|
1368
|
+
*
|
|
1369
|
+
* The edges come a page at a time, so only the first page is conditional:
|
|
1370
|
+
* its validator covers the whole hierarchy, and a `304` to it means no page
|
|
1371
|
+
* has changed. On a `200` the remaining pages are read plainly and the map
|
|
1372
|
+
* is rebuilt from every page.
|
|
1373
|
+
*
|
|
1374
|
+
* A walk over several pages is not one snapshot. A node added or removed
|
|
1375
|
+
* between two page reads shifts the rows under the offset, so a row can be
|
|
1376
|
+
* skipped, which would leave a node out of the map until the next refresh.
|
|
1377
|
+
* So the walk is confirmed by asking for the first page again with the
|
|
1378
|
+
* validator it came with: a `304` proves nothing changed while the pages
|
|
1379
|
+
* were read. A `200` is a fresh first page, and the walk starts again from
|
|
1380
|
+
* it, up to {@link MAX_TREE_WALKS} times.
|
|
1311
1381
|
*/
|
|
1312
1382
|
async fetchTree() {
|
|
1313
1383
|
this.stats.treeRequests += 1;
|
|
1314
1384
|
const held = this.tree;
|
|
1315
|
-
|
|
1385
|
+
let first = await this.parentsFirstPage(held?.etag ?? void 0);
|
|
1386
|
+
if (!first.modified) {
|
|
1387
|
+
if (held) {
|
|
1388
|
+
this.stats.treeNotModified += 1;
|
|
1389
|
+
this.tree = { ...held, expiresAt: this.now() + this.ttlMs };
|
|
1390
|
+
return held.parents;
|
|
1391
|
+
}
|
|
1392
|
+
return this.keepTree([], null);
|
|
1393
|
+
}
|
|
1394
|
+
for (let walk = 1; ; walk++) {
|
|
1395
|
+
const edges = await this.parentsRemainingPages(first.data);
|
|
1396
|
+
if (!spansPages(first.data) || first.etag === null) {
|
|
1397
|
+
return this.keepTree(edges, first.etag);
|
|
1398
|
+
}
|
|
1399
|
+
const confirm = await this.parentsFirstPage(first.etag);
|
|
1400
|
+
if (!confirm.modified) {
|
|
1401
|
+
return this.keepTree(edges, first.etag);
|
|
1402
|
+
}
|
|
1403
|
+
if (walk >= MAX_TREE_WALKS) {
|
|
1404
|
+
return this.keepTree(edges, null);
|
|
1405
|
+
}
|
|
1406
|
+
first = confirm;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
parentsFirstPage(etag) {
|
|
1410
|
+
this.stats.treePages += 1;
|
|
1411
|
+
return this.client.requestConditional(
|
|
1316
1412
|
"GET",
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
this.readOptions
|
|
1413
|
+
PARENTS_PATH,
|
|
1414
|
+
etag,
|
|
1415
|
+
{ ...this.readOptions, query: { page: 1, take: PAGE_SIZE } }
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
/** The first page's edges followed by every later page's. */
|
|
1419
|
+
async parentsRemainingPages(first) {
|
|
1420
|
+
const edges = [...first.items];
|
|
1421
|
+
if (!spansPages(first)) {
|
|
1422
|
+
return edges;
|
|
1423
|
+
}
|
|
1424
|
+
const rest = paginate(
|
|
1425
|
+
(query) => {
|
|
1426
|
+
this.stats.treePages += 1;
|
|
1427
|
+
return this.client.request("GET", PARENTS_PATH, {
|
|
1428
|
+
...this.readOptions,
|
|
1429
|
+
query
|
|
1430
|
+
});
|
|
1431
|
+
},
|
|
1432
|
+
{ page: first.pagination.page + 1, take: PAGE_SIZE },
|
|
1433
|
+
{ maxPages: MAX_PAGES - 1 }
|
|
1320
1434
|
);
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1435
|
+
for await (const page of rest.pages()) {
|
|
1436
|
+
for (const edge of page.items) {
|
|
1437
|
+
edges.push(edge);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
return edges;
|
|
1441
|
+
}
|
|
1442
|
+
keepTree(edges, etag) {
|
|
1443
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1444
|
+
for (const edge of edges) {
|
|
1445
|
+
ids.set(edge.id, edge.id);
|
|
1325
1446
|
}
|
|
1326
1447
|
const parents = /* @__PURE__ */ new Map();
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const parent = edge.parent_node_id ?? null;
|
|
1334
|
-
parents.set(
|
|
1335
|
-
edge.id,
|
|
1336
|
-
parent === null ? null : ids.get(parent) ?? parent
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1448
|
+
for (const edge of edges) {
|
|
1449
|
+
const parent = edge.parent_node_id ?? null;
|
|
1450
|
+
parents.set(
|
|
1451
|
+
edge.id,
|
|
1452
|
+
parent === null ? null : ids.get(parent) ?? parent
|
|
1453
|
+
);
|
|
1339
1454
|
}
|
|
1340
1455
|
this.tree = {
|
|
1341
1456
|
parents,
|
|
1342
|
-
etag
|
|
1457
|
+
etag,
|
|
1343
1458
|
expiresAt: this.now() + this.ttlMs
|
|
1344
1459
|
};
|
|
1345
1460
|
return parents;
|
|
1346
1461
|
}
|
|
1347
1462
|
};
|
|
1463
|
+
function spansPages(first) {
|
|
1464
|
+
return first.pagination.has_next_page && first.items.length > 0;
|
|
1465
|
+
}
|
|
1348
1466
|
|
|
1349
1467
|
// src/status.ts
|
|
1350
1468
|
function tokenErrorStatus(error) {
|