@larc-iu/plaid-client 0.2.0-alpha.11 → 0.2.0-alpha.13

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/src/index.js CHANGED
@@ -85,6 +85,31 @@ async function anonymousPost(baseUrl, path, body, options = {}) {
85
85
  }
86
86
  }
87
87
 
88
+ /**
89
+ * GET a path with no authentication, for the two endpoints a client may need
90
+ * before it has a session: /health and /api/v1/info.
91
+ */
92
+ async function anonymousGet(baseUrl, path, options = {}) {
93
+ const base = baseUrl.replace(/\/$/, "");
94
+ const url = `${base}${path}`;
95
+ try {
96
+ const fetchOptions = { method: "GET" };
97
+ const signal = timeoutSignal(
98
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
99
+ );
100
+ if (signal) fetchOptions.signal = signal;
101
+
102
+ const response = await fetch(url, fetchOptions);
103
+ if (!response.ok) {
104
+ throw makeHttpError(response, await parseErrorBody(response), url, "GET");
105
+ }
106
+ return transformResponse(await response.json());
107
+ } catch (error) {
108
+ if (error.status) throw error;
109
+ throw makeNetworkError(error, url, "GET");
110
+ }
111
+ }
112
+
88
113
  /**
89
114
  * The server's cap on operations per batch request (plaid.rest-api.v1.batch).
90
115
  * A batch queued past it is submitted as consecutive requests.
@@ -450,6 +475,16 @@ class PlaidClient {
450
475
  auditMessage,
451
476
  body,
452
477
  }),
478
+ /**
479
+ * Update many relations in a single operation: set values and/or patch metadata.
480
+ * @param {Array} body - Objects of the shape `{id, value?, metadata?}`. `value` is set only when the key is present (null sends JSON null); `metadata` is a patch, a null value deleting that key. The relations may lie in several documents of one project; every document touched has its version bumped. An unknown id refuses the whole update.
481
+ * @returns {Promise<{count: number}>} How many relations were updated.
482
+ */
483
+ bulkUpdate: (body, auditMessage) =>
484
+ this._request("PATCH", "/api/v1/relations/bulk", {
485
+ auditMessage,
486
+ body,
487
+ }),
453
488
  };
454
489
 
455
490
  this.spanLayers = {
@@ -600,6 +635,16 @@ class PlaidClient {
600
635
  */
601
636
  bulkDelete: (body, auditMessage) =>
602
637
  this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
638
+ /**
639
+ * Update many spans in a single operation: set values and/or patch metadata.
640
+ * @param {Array} body - Objects of the shape `{id, value?, metadata?}`. `value` is set only when the key is present (null sends JSON null); `metadata` is a patch, a null value deleting that key. The spans may lie in several documents of one project; every document touched has its version bumped. An unknown id refuses the whole update.
641
+ * @returns {Promise<{count: number}>} How many spans were updated.
642
+ */
643
+ bulkUpdate: (body, auditMessage) =>
644
+ this._request("PATCH", "/api/v1/spans/bulk", {
645
+ auditMessage,
646
+ body,
647
+ }),
603
648
  /**
604
649
  * Replace all metadata for a span.
605
650
  * @param {string} spanId - The span ID
@@ -633,6 +678,42 @@ class PlaidClient {
633
678
  }),
634
679
  };
635
680
 
681
+ // Server-level facts. Fetched at most once: the limits cannot change while
682
+ // the server is up, and a caller asking "will this file be accepted" should
683
+ // not pay a round trip to find out.
684
+ let infoPromise = null;
685
+ this.server = {
686
+ /**
687
+ * This server's version and the limits it enforces, e.g.
688
+ * `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
689
+ * Sizes are in bytes. Unauthenticated.
690
+ */
691
+ info: () => {
692
+ if (!infoPromise) {
693
+ // bypassBatch: a read belongs to whoever asked for it, not to
694
+ // whatever batch happens to be open on this shared client.
695
+ infoPromise = this._request("GET", "/api/v1/info", {
696
+ bypassBatch: true,
697
+ }).catch((err) => {
698
+ // A failure must not be cached: a client that starts before the
699
+ // server is up would never see the limits at all.
700
+ infoPromise = null;
701
+ throw err;
702
+ });
703
+ }
704
+ return infoPromise;
705
+ },
706
+ /** Just the limits, which is what a caller almost always wants. */
707
+ limits: async () => (await this.server.info()).limits,
708
+ /**
709
+ * Liveness, version and database size. Unauthenticated, and served
710
+ * outside the REST router at /health, so it answers even while the API
711
+ * is refusing requests. Never cached — the point is that it is current.
712
+ * @returns {Promise<{ok: boolean, version: string, uptimeMs: number, audit: object}>}
713
+ */
714
+ health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
715
+ };
716
+
636
717
  this.batch = {
637
718
  /**
638
719
  * Execute multiple API operations atomically. If any operation fails, all
@@ -812,6 +893,31 @@ class PlaidClient {
812
893
  "op-types": opTypesParam(opTypes),
813
894
  },
814
895
  }),
896
+ /**
897
+ * One page of the same log, newest-first with `order: "desc"`. Use this
898
+ * rather than audit() wherever the caller wants the recent end of a log
899
+ * that may be long: audit() walks every page before it resolves.
900
+ * @param {object} [opts]
901
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
902
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
903
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
904
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
905
+ */
906
+ auditPage: (
907
+ userId,
908
+ { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
909
+ ) =>
910
+ listPage(this, `/api/v1/users/${userId}/audit`, {
911
+ limit,
912
+ cursor,
913
+ query: {
914
+ "start-time": startTime,
915
+ "end-time": endTime,
916
+ "as-of": asOf,
917
+ "op-types": opTypesParam(opTypes),
918
+ order,
919
+ },
920
+ }),
815
921
  /**
816
922
  * Get a user by ID
817
923
  * @param {string} id - The resource ID
@@ -924,14 +1030,21 @@ class PlaidClient {
924
1030
  /**
925
1031
  * List a user's private data entries ({key, updatedAt}, plus value when
926
1032
  * includeValues). Owner or admin only.
1033
+ *
1034
+ * Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
1035
+ * GLOB over the whole key (`*` any run, `?` one character) — the way to
1036
+ * ask for a key convention identified by a segment in the middle, e.g.
1037
+ * `igt:assistant:*:meta:*` for every conversation's sidebar entry across
1038
+ * every project without dragging down the transcripts beside them.
927
1039
  * @param {string} userId
928
1040
  * @param {object} [opts]
929
1041
  * @param {string} [opts.prefix] - Only keys starting with this prefix
1042
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
930
1043
  * @param {boolean} [opts.includeValues] - Also return each entry's value
931
1044
  */
932
- list: (userId, { prefix, includeValues } = {}) =>
1045
+ list: (userId, { prefix, pattern, includeValues } = {}) =>
933
1046
  this._request("GET", `/api/v1/users/${userId}/data`, {
934
- queryParams: { prefix, "include-values": includeValues },
1047
+ queryParams: { prefix, pattern, "include-values": includeValues },
935
1048
  }),
936
1049
  /**
937
1050
  * Read one private data entry ({key, updatedAt, value}); 404 if absent.
@@ -1044,37 +1157,40 @@ class PlaidClient {
1044
1157
  * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
1045
1158
  * @param {object} [opts]
1046
1159
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1160
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
1047
1161
  */
1048
- list: ({ projectId } = {}) =>
1162
+ list: ({ projectId, all } = {}) =>
1049
1163
  listAll(this, "/api/v1/invites", {
1050
- query: { "project-id": projectId },
1164
+ query: { "project-id": projectId, all },
1051
1165
  }),
1052
1166
  /**
1053
1167
  * Fetch a single page of invites.
1054
1168
  * @param {object} [opts]
1055
1169
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1170
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
1056
1171
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1057
1172
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1058
1173
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1059
1174
  */
1060
- listPage: ({ projectId, limit, cursor } = {}) =>
1175
+ listPage: ({ projectId, all, limit, cursor } = {}) =>
1061
1176
  listPage(this, "/api/v1/invites", {
1062
1177
  limit,
1063
1178
  cursor,
1064
- query: { "project-id": projectId },
1179
+ query: { "project-id": projectId, all },
1065
1180
  }),
1066
1181
  /**
1067
1182
  * Async-iterate invites page by page; yields each page's entries array.
1068
1183
  * @param {object} [opts]
1069
1184
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1185
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
1070
1186
  * @param {number} [opts.pageSize] - Per-request page size
1071
1187
  * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
1072
1188
  * @returns {AsyncGenerator<Array>}
1073
1189
  */
1074
- iterPages: ({ projectId, pageSize } = {}) =>
1190
+ iterPages: ({ projectId, all, pageSize } = {}) =>
1075
1191
  iterPages(this, "/api/v1/invites", {
1076
1192
  pageSize,
1077
- query: { "project-id": projectId },
1193
+ query: { "project-id": projectId, all },
1078
1194
  }),
1079
1195
  /**
1080
1196
  * Mint an invite. The returned `code` is shown ONLY here — it is never
@@ -1134,6 +1250,206 @@ class PlaidClient {
1134
1250
  this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
1135
1251
  };
1136
1252
 
1253
+ this.admin = {
1254
+ /**
1255
+ * Everything about the server in one read: version and JVM uptime,
1256
+ * database size and per-table row counts, media directory usage, backup
1257
+ * configuration and the backups on disk, and the settings an operator
1258
+ * gets asked to confirm. Carries no secrets. Runs a count per table and
1259
+ * walks the media directory, so open it, do not poll it. Admin only.
1260
+ * @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
1261
+ */
1262
+ server: () => this._request("GET", "/api/v1/admin/server"),
1263
+ /**
1264
+ * Take a database backup right now, outside the nightly schedule.
1265
+ * Resolves to the backup block with `ok` reporting whether the snapshot
1266
+ * succeeded. Uses VACUUM INTO, which only reads, so it is safe while
1267
+ * people are working. Admin only.
1268
+ * @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
1269
+ */
1270
+ backup: () => this._request("POST", "/api/v1/admin/backup"),
1271
+ /**
1272
+ * Documents currently held by an editing lock, with who holds each and
1273
+ * when it expires on its own. Admin only.
1274
+ * @returns {Promise<{entries: Array<{documentId: string, userId: string, expiresAt: number}>}>}
1275
+ */
1276
+ locks: () => this._request("GET", "/api/v1/admin/locks"),
1277
+ /**
1278
+ * Drop the lock on a document whoever holds it. Idempotent. For a client
1279
+ * that went away without releasing one. Admin only.
1280
+ * @param {string} documentId - The document ID
1281
+ */
1282
+ releaseLock: (documentId) =>
1283
+ this._request("DELETE", `/api/v1/admin/locks/${documentId}`),
1284
+ /**
1285
+ * Live login and invite rate-limit buckets: the address, the account
1286
+ * where there is one, failures inside the window, the limit, and whether
1287
+ * it is currently blocking. Admin only.
1288
+ * @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
1289
+ */
1290
+ rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
1291
+ /**
1292
+ * Forget recorded rate-limit failures. With `ip`, clears that address,
1293
+ * narrowed to one account with `userId`. With neither, clears every
1294
+ * bucket. Only ever unblocks. Admin only.
1295
+ * @param {object} [opts]
1296
+ * @param {string} [opts.ip] - Clear only this address
1297
+ * @param {string} [opts.userId] - Narrow to one account on that address
1298
+ */
1299
+ clearRateLimits: ({ ip, userId } = {}) =>
1300
+ this._request("DELETE", "/api/v1/admin/rate-limits", {
1301
+ queryParams: { ip, "user-id": userId },
1302
+ }),
1303
+ /**
1304
+ * The tail of the configured log file. Resolves with an `error` string
1305
+ * instead of lines when no log file is configured or it does not exist
1306
+ * yet. Admin only.
1307
+ * @param {object} [opts]
1308
+ * @param {number} [opts.lines] - How many lines (default 200, max 2000)
1309
+ * @returns {Promise<{file: (string|null), lines: string[], error?: string}>}
1310
+ */
1311
+ logs: ({ lines } = {}) =>
1312
+ this._request("GET", "/api/v1/admin/logs", {
1313
+ queryParams: { lines },
1314
+ }),
1315
+ /**
1316
+ * Private user-data entries across every account, each carrying its
1317
+ * `userId`. `/users/:userId/data` has always been owner-or-admin; this
1318
+ * is the same reach across accounts at once. Transparently follows
1319
+ * pagination cursors and returns the full flat array. Admin only.
1320
+ *
1321
+ * Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
1322
+ * GLOB over the whole key (`*` any run, `?` one character) — the way to
1323
+ * ask for a key convention identified by a segment in the middle, e.g.
1324
+ * `igt:assistant:*:meta:*`. Values come only with `includeValues`, and
1325
+ * are recased like any other body (see `userData.put`).
1326
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use userDataPage() for a single page in a batch.
1327
+ * @param {object} [opts]
1328
+ * @param {string} [opts.prefix] - Only keys starting with this
1329
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
1330
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
1331
+ * @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
1332
+ */
1333
+ userData: ({ prefix, pattern, includeValues } = {}) =>
1334
+ listAll(this, "/api/v1/admin/user-data", {
1335
+ query: {
1336
+ prefix,
1337
+ pattern,
1338
+ "include-values": includeValues,
1339
+ },
1340
+ }),
1341
+ /**
1342
+ * One page of private user-data entries across accounts, ordered by
1343
+ * (user, key). Admin only.
1344
+ * @param {object} [opts]
1345
+ * @param {string} [opts.prefix] - Only keys starting with this
1346
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
1347
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
1348
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1349
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1350
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1351
+ */
1352
+ userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
1353
+ listPage(this, "/api/v1/admin/user-data", {
1354
+ limit,
1355
+ cursor,
1356
+ query: {
1357
+ prefix,
1358
+ pattern,
1359
+ "include-values": includeValues,
1360
+ },
1361
+ }),
1362
+ };
1363
+
1364
+ this.audit = {
1365
+ /**
1366
+ * The audit log across every project, oldest first. Same fold, window
1367
+ * and op-type filter as the per-project read, with the entity scope
1368
+ * dropped. Admin only. Transparently follows pagination cursors and
1369
+ * returns the full flat array.
1370
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
1371
+ * @param {object} [opts]
1372
+ * @param {string} [opts.startTime] - Only operations at or after this instant
1373
+ * @param {string} [opts.endTime] - Only operations at or before this instant
1374
+ * @param {string[]|string} [opts.opTypes] - Only these op types (e.g.
1375
+ * `['span-layer/create']`). An entry appears when one of its operations
1376
+ * matches, carrying only the ones that did.
1377
+ */
1378
+ list: ({ startTime, endTime, opTypes } = {}) =>
1379
+ listAll(this, "/api/v1/audit", {
1380
+ query: {
1381
+ "start-time": startTime,
1382
+ "end-time": endTime,
1383
+ "op-types": opTypesParam(opTypes),
1384
+ },
1385
+ }),
1386
+ /**
1387
+ * Fetch a single page of the instance-wide audit log. Admin only.
1388
+ * @param {object} [opts]
1389
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
1390
+ * is what a feed wants. A cursor belongs to the direction that made it.
1391
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1392
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1393
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1394
+ */
1395
+ listPage: ({ startTime, endTime, opTypes, order, limit, cursor } = {}) =>
1396
+ listPage(this, "/api/v1/audit", {
1397
+ limit,
1398
+ cursor,
1399
+ query: {
1400
+ "start-time": startTime,
1401
+ "end-time": endTime,
1402
+ "op-types": opTypesParam(opTypes),
1403
+ order,
1404
+ },
1405
+ }),
1406
+ /**
1407
+ * Async-iterate the instance-wide audit log page by page. Admin only.
1408
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
1409
+ * @returns {AsyncGenerator<Array>}
1410
+ */
1411
+ iterPages: ({ startTime, endTime, opTypes, pageSize } = {}) =>
1412
+ iterPages(this, "/api/v1/audit", {
1413
+ pageSize,
1414
+ query: {
1415
+ "start-time": startTime,
1416
+ "end-time": endTime,
1417
+ "op-types": opTypesParam(opTypes),
1418
+ },
1419
+ }),
1420
+ /**
1421
+ * Per-user activity counts. `changes` is the number of logical actions,
1422
+ * folded the way the audit feed folds them, so one "Confirm word
1423
+ * analysis" counts once however many rows it wrote; `operations` is the
1424
+ * unfolded row count. Only users who did something appear — subtract
1425
+ * from the roster you already hold to find the ones who did not.
1426
+ *
1427
+ * With `projectId`, scoped to that project and open to its maintainers.
1428
+ * Without one, instance-wide and admin only.
1429
+ * @param {object} [opts]
1430
+ * @param {string} [opts.projectId] - Scope to one project
1431
+ * @param {string} [opts.startTime] - Only count at or after this instant
1432
+ * @param {string} [opts.endTime] - Only count at or before this instant
1433
+ * @param {boolean} [opts.daily] - Also return `byDay`, a list of
1434
+ * `{date, changes}` per user, oldest first, at the cost of a second
1435
+ * grouped scan
1436
+ * @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
1437
+ */
1438
+ tally: async ({ projectId, startTime, endTime, daily } = {}) => {
1439
+ const path = projectId
1440
+ ? `/api/v1/projects/${projectId}/audit/tally`
1441
+ : "/api/v1/audit/tally";
1442
+ const result = await this._request("GET", path, {
1443
+ queryParams: {
1444
+ "start-time": startTime,
1445
+ "end-time": endTime,
1446
+ daily,
1447
+ },
1448
+ });
1449
+ return result.entries;
1450
+ },
1451
+ };
1452
+
1137
1453
  this.tokenLayers = {
1138
1454
  /**
1139
1455
  * Shift a token layer's display order.
@@ -1345,6 +1661,31 @@ class PlaidClient {
1345
1661
  "op-types": opTypesParam(opTypes),
1346
1662
  },
1347
1663
  }),
1664
+ /**
1665
+ * One page of the same log, newest-first with `order: "desc"`. Use this
1666
+ * rather than audit() wherever the caller wants the recent end of a log
1667
+ * that may be long: audit() walks every page before it resolves.
1668
+ * @param {object} [opts]
1669
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1670
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1671
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1672
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1673
+ */
1674
+ auditPage: (
1675
+ documentId,
1676
+ { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
1677
+ ) =>
1678
+ listPage(this, `/api/v1/documents/${documentId}/audit`, {
1679
+ limit,
1680
+ cursor,
1681
+ query: {
1682
+ "start-time": startTime,
1683
+ "end-time": endTime,
1684
+ "as-of": asOf,
1685
+ "op-types": opTypesParam(opTypes),
1686
+ order,
1687
+ },
1688
+ }),
1348
1689
  /**
1349
1690
  * Restore a document to its state at an earlier time, as one
1350
1691
  * operation: what was deleted since then comes back under its original
@@ -1361,7 +1702,10 @@ class PlaidClient {
1361
1702
  */
1362
1703
  restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
1363
1704
  this._request("POST", `/api/v1/documents/${documentId}/restore`, {
1364
- queryParams: { "as-of": asOf, "dry-run": dryRun ? "true" : undefined },
1705
+ queryParams: {
1706
+ "as-of": asOf,
1707
+ "dry-run": dryRun ? "true" : undefined,
1708
+ },
1365
1709
  auditMessage,
1366
1710
  }),
1367
1711
  /**
@@ -1416,6 +1760,25 @@ class PlaidClient {
1416
1760
  auditMessage,
1417
1761
  body: bodyOf({ "project-id": projectId, name, metadata }),
1418
1762
  }),
1763
+ /**
1764
+ * Copy a document and everything in it into a new document of the same
1765
+ * project, as one operation. The copy shares the source's layers and
1766
+ * the vocabulary entries its links name, and holds the source's texts,
1767
+ * tokens, spans, relations and vocab links under fresh ids, with their
1768
+ * metadata. Comments do not travel. The media file does, unless
1769
+ * `includeMedia` is false. Resolves to `{ id }`, plus `mediaError` when
1770
+ * the source had media the copy could not take with it.
1771
+ * @param {string} documentId - The document to copy
1772
+ * @param {string} name - The new document's name
1773
+ * @param {object} [options] - `{ includeMedia }`: false leaves the media
1774
+ * file behind
1775
+ * @param {string} [auditMessage] - Custom audit message for this operation
1776
+ */
1777
+ copy: (documentId, name, { includeMedia } = {}, auditMessage) =>
1778
+ this._request("POST", `/api/v1/documents/${documentId}/copy`, {
1779
+ auditMessage,
1780
+ body: bodyOf({ name, "include-media": includeMedia }),
1781
+ }),
1419
1782
  };
1420
1783
 
1421
1784
  this.projects = {
@@ -1522,6 +1885,31 @@ class PlaidClient {
1522
1885
  "op-types": opTypesParam(opTypes),
1523
1886
  },
1524
1887
  }),
1888
+ /**
1889
+ * One page of the same log, newest-first with `order: "desc"`. Use this
1890
+ * rather than audit() wherever the caller wants the recent end of a log
1891
+ * that may be long: audit() walks every page before it resolves.
1892
+ * @param {object} [opts]
1893
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1894
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1895
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1896
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1897
+ */
1898
+ auditPage: (
1899
+ projectId,
1900
+ { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
1901
+ ) =>
1902
+ listPage(this, `/api/v1/projects/${projectId}/audit`, {
1903
+ limit,
1904
+ cursor,
1905
+ query: {
1906
+ "start-time": startTime,
1907
+ "end-time": endTime,
1908
+ "as-of": asOf,
1909
+ "op-types": opTypesParam(opTypes),
1910
+ order,
1911
+ },
1912
+ }),
1525
1913
  /**
1526
1914
  * When the calling user last wrote to each document in a project, as a
1527
1915
  * `{documentId: timestamp}` map. Documents they have never written to
@@ -2009,6 +2397,16 @@ class PlaidClient {
2009
2397
  */
2010
2398
  bulkDelete: (body, auditMessage) =>
2011
2399
  this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
2400
+ /**
2401
+ * Update many tokens in a single operation: patch metadata.
2402
+ * @param {Array} body - Objects of the shape `{id, metadata}`. `metadata` is a patch, a null value deleting that key. The tokens may lie in several documents of one project; every document touched has its version bumped. An unknown id refuses the whole update.
2403
+ * @returns {Promise<{count: number}>} How many tokens were updated.
2404
+ */
2405
+ bulkUpdate: (body, auditMessage) =>
2406
+ this._request("PATCH", "/api/v1/tokens/bulk", {
2407
+ auditMessage,
2408
+ body,
2409
+ }),
2012
2410
  /**
2013
2411
  * Split a token at a Unicode code-point offset. The original token becomes the
2014
2412
  * left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
@@ -2163,8 +2561,25 @@ class PlaidClient {
2163
2561
  * @param {Object} [opts] - {requestId, onAccepted}
2164
2562
  * @returns {Promise<any>} The service's result
2165
2563
  */
2166
- requestService: (projectId, serviceId, data, timeout, onProgress, signal, opts) =>
2167
- requestService(this, projectId, serviceId, data, timeout, onProgress, signal, opts),
2564
+ requestService: (
2565
+ projectId,
2566
+ serviceId,
2567
+ data,
2568
+ timeout,
2569
+ onProgress,
2570
+ signal,
2571
+ opts,
2572
+ ) =>
2573
+ requestService(
2574
+ this,
2575
+ projectId,
2576
+ serviceId,
2577
+ data,
2578
+ timeout,
2579
+ onProgress,
2580
+ signal,
2581
+ opts,
2582
+ ),
2168
2583
 
2169
2584
  /**
2170
2585
  * Rejoin a service request made earlier and await its result: the
@@ -2180,8 +2595,21 @@ class PlaidClient {
2180
2595
  * @param {AbortSignal} [signal] - Abort to stop waiting
2181
2596
  * @returns {Promise<any>} The service's result
2182
2597
  */
2183
- attachServiceRequest: (projectId, requestId, timeout, onProgress, signal) =>
2184
- attachServiceRequest(this, projectId, requestId, timeout, onProgress, signal),
2598
+ attachServiceRequest: (
2599
+ projectId,
2600
+ requestId,
2601
+ timeout,
2602
+ onProgress,
2603
+ signal,
2604
+ ) =>
2605
+ attachServiceRequest(
2606
+ this,
2607
+ projectId,
2608
+ requestId,
2609
+ timeout,
2610
+ onProgress,
2611
+ signal,
2612
+ ),
2185
2613
 
2186
2614
  /**
2187
2615
  * Ask the service to stop a request made earlier. The request still
@@ -2238,7 +2666,11 @@ class PlaidClient {
2238
2666
  // and the server ran the query against the batch's tx Connection, which
2239
2667
  // 500s and rolls back every write in that batch.
2240
2668
  this.query = (body, auditMessage) =>
2241
- this._request("POST", "/api/v1/query", { auditMessage, body, bypassBatch: true });
2669
+ this._request("POST", "/api/v1/query", {
2670
+ auditMessage,
2671
+ body,
2672
+ bypassBatch: true,
2673
+ });
2242
2674
 
2243
2675
  // Logical-operation groups (audit-log grouping). There is no create: a
2244
2676
  // group row is made lazily by the first write carrying `?group-id=`
@@ -2333,7 +2765,10 @@ class PlaidClient {
2333
2765
  * @param {string} [opts.entityId] - With entityType, one entity's thread
2334
2766
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2335
2767
  */
2336
- listPage: (projectId, { limit, cursor, documentId, entityType, entityId } = {}) =>
2768
+ listPage: (
2769
+ projectId,
2770
+ { limit, cursor, documentId, entityType, entityId } = {},
2771
+ ) =>
2337
2772
  listPage(this, `/api/v1/projects/${projectId}/comments`, {
2338
2773
  limit,
2339
2774
  cursor,
@@ -2354,7 +2789,10 @@ class PlaidClient {
2354
2789
  * @param {string} [opts.entityId] - With entityType, one entity's thread
2355
2790
  * @returns {AsyncGenerator<Array>}
2356
2791
  */
2357
- iterPages: (projectId, { pageSize, documentId, entityType, entityId } = {}) =>
2792
+ iterPages: (
2793
+ projectId,
2794
+ { pageSize, documentId, entityType, entityId } = {},
2795
+ ) =>
2358
2796
  iterPages(this, `/api/v1/projects/${projectId}/comments`, {
2359
2797
  pageSize,
2360
2798
  query: {
@@ -2421,10 +2859,14 @@ class PlaidClient {
2421
2859
  * @returns {Promise<Object<string, number>>}
2422
2860
  */
2423
2861
  countsInVocab: (vocabId, { entityId } = {}) =>
2424
- this._request("GET", `/api/v1/vocab-layers/${vocabId}/comments/counts`, {
2425
- queryParams: { "entity-id": entityId },
2426
- skipResponseTransform: true,
2427
- }),
2862
+ this._request(
2863
+ "GET",
2864
+ `/api/v1/vocab-layers/${vocabId}/comments/counts`,
2865
+ {
2866
+ queryParams: { "entity-id": entityId },
2867
+ skipResponseTransform: true,
2868
+ },
2869
+ ),
2428
2870
  };
2429
2871
  }
2430
2872
 
@@ -2580,7 +3022,9 @@ class PlaidClient {
2580
3022
  // must not fail on its size alone.
2581
3023
  const results = [];
2582
3024
  for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
2583
- results.push(...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))));
3025
+ results.push(
3026
+ ...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))),
3027
+ );
2584
3028
  }
2585
3029
  return results;
2586
3030
  } finally {
@@ -2703,6 +3147,27 @@ class PlaidClient {
2703
3147
  * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
2704
3148
  * @returns {Promise<PlaidClient>} - Authenticated client instance
2705
3149
  */
3150
+ /**
3151
+ * Liveness, version and database size, with NO authentication and no client
3152
+ * instance — for a launcher or status page checking whether a server is up
3153
+ * before anyone logs in.
3154
+ * @param {string} baseUrl - The API base URL
3155
+ * @param {object} [options]
3156
+ */
3157
+ static async health(baseUrl, options = {}) {
3158
+ return anonymousGet(baseUrl, "/health", options);
3159
+ }
3160
+
3161
+ /**
3162
+ * The limits this server enforces, with NO authentication and no client
3163
+ * instance. See the instance method for the shape.
3164
+ * @param {string} baseUrl - The API base URL
3165
+ * @param {object} [options]
3166
+ */
3167
+ static async info(baseUrl, options = {}) {
3168
+ return anonymousGet(baseUrl, "/api/v1/info", options);
3169
+ }
3170
+
2706
3171
  /**
2707
3172
  * Build the link to hand someone for an invite code. The server never sees
2708
3173
  * an app URL, so the app that minted the invite is the one that names it.
@@ -2730,12 +3195,7 @@ class PlaidClient {
2730
3195
  * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
2731
3196
  */
2732
3197
  static async lookupInvite(baseUrl, code, options = {}) {
2733
- return anonymousPost(
2734
- baseUrl,
2735
- "/api/v1/invite-codes/lookup",
2736
- { code },
2737
- options,
2738
- );
3198
+ return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
2739
3199
  }
2740
3200
 
2741
3201
  /**
@@ -2766,7 +3226,7 @@ class PlaidClient {
2766
3226
  ) {
2767
3227
  const data = await anonymousPost(
2768
3228
  baseUrl,
2769
- "/api/v1/invite-codes/redeem",
3229
+ "/api/v1/invites/redeem",
2770
3230
  bodyOf({ code, email, password, "display-name": displayName }),
2771
3231
  options,
2772
3232
  );
@@ -2831,6 +3291,7 @@ export {
2831
3291
  export {
2832
3292
  PLAID_NAMESPACE,
2833
3293
  ROLE_KEY,
3294
+ PRESERVE_ON_SPLIT_KEY,
2834
3295
  ROLES,
2835
3296
  readRole,
2836
3297
  findByRole,
@@ -2847,6 +3308,11 @@ export {
2847
3308
  buildDefaultValues,
2848
3309
  coerceParamValues,
2849
3310
  } from "./serviceSchema.js";
3311
+ // Cooperative cancellation for a service AUTHORED in JS: `progress()` is a
3312
+ // checkpoint that throws ServiceCancelled once the requester stops the
3313
+ // request, and `critical()` holds that off around writes. See ./services.js
3314
+ // and the manual, "Stopping a request".
3315
+ export { ServiceCancelled, createCancelScope } from "./services.js";
2850
3316
  // Provenance: the cross-app convention for who made an annotation (flat
2851
3317
  // prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
2852
3318
  // a machine; 'contributed' = a person whose work is reviewed), plus the
@@ -2855,6 +3321,7 @@ export {
2855
3321
  PROV,
2856
3322
  PROV_STATES,
2857
3323
  PROV_CONFIRMED,
3324
+ PROVENANCE_KEYS,
2858
3325
  stampInferred,
2859
3326
  confirmedInferred,
2860
3327
  stampContributed,