@larc-iu/plaid-client 0.2.0-alpha.1 → 0.2.0-alpha.12

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
@@ -2,19 +2,46 @@
2
2
  * plaid-client - JavaScript client for the Plaid annotation API
3
3
  */
4
4
 
5
- import { transformRequest, transformResponse } from './transforms.js';
5
+ import { transformRequest, transformResponse } from "./transforms.js";
6
6
  import {
7
- makeRequest, extractDocumentVersions, parseErrorBody, makeHttpError,
8
- makeNetworkError, timeoutSignal, DEFAULT_TIMEOUT_MS,
9
- } from './http.js';
10
- import { listAll, listPage, iterPages } from './pagination.js';
11
- import { createSSEConnection } from './sse.js';
7
+ makeRequest,
8
+ extractDocumentVersions,
9
+ parseErrorBody,
10
+ makeHttpError,
11
+ makeNetworkError,
12
+ timeoutSignal,
13
+ retryWhileBusy,
14
+ DEFAULT_TIMEOUT_MS,
15
+ DEFAULT_BATCH_TIMEOUT_MS,
16
+ } from "./http.js";
17
+ import { listAll, listPage, iterPages } from "./pagination.js";
18
+ import { createSSEConnection } from "./sse.js";
12
19
  import {
13
20
  discoverServices,
14
21
  discardService,
15
22
  serve,
16
23
  requestService,
17
- } from './services.js';
24
+ attachServiceRequest,
25
+ cancelServiceRequest,
26
+ } from "./services.js";
27
+
28
+ // Helper: normalize a document-read `layers` filter to the wire's
29
+ // comma-separated form. Accepts an array of layer ids or a ready-made string;
30
+ // anything empty becomes undefined so no `?layers=` is sent at all.
31
+ function layersParam(layers) {
32
+ if (layers === undefined || layers === null) return undefined;
33
+ const joined = Array.isArray(layers) ? layers.join(",") : String(layers);
34
+ return joined.length > 0 ? joined : undefined;
35
+ }
36
+
37
+ // Helper: normalize an audit `opTypes` filter to the wire's comma-separated
38
+ // form. Accepts an array of op types or a ready-made string; anything empty
39
+ // becomes undefined so no `?op-types=` is sent at all.
40
+ function opTypesParam(opTypes) {
41
+ if (opTypes === undefined || opTypes === null) return undefined;
42
+ const joined = Array.isArray(opTypes) ? opTypes.join(",") : String(opTypes);
43
+ return joined.length > 0 ? joined : undefined;
44
+ }
18
45
 
19
46
  // Helper: build body object, filtering out undefined values
20
47
  function bodyOf(obj) {
@@ -29,28 +56,66 @@ function bodyOf(obj) {
29
56
  // and redemption). Deliberately does not go through PlaidClient._request: these
30
57
  // are called before any client exists, which is the whole point.
31
58
  async function anonymousPost(baseUrl, path, body, options = {}) {
32
- const base = baseUrl.replace(/\/$/, '');
59
+ const base = baseUrl.replace(/\/$/, "");
33
60
  const url = `${base}${path}`;
34
61
  try {
35
62
  const fetchOptions = {
36
- method: 'POST',
37
- headers: { 'Content-Type': 'application/json' },
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
38
65
  body: JSON.stringify(body),
39
66
  };
40
- const signal = timeoutSignal(options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS);
67
+ const signal = timeoutSignal(
68
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
69
+ );
41
70
  if (signal) fetchOptions.signal = signal;
42
71
 
43
72
  const response = await fetch(url, fetchOptions);
44
73
  if (!response.ok) {
45
- throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
74
+ throw makeHttpError(
75
+ response,
76
+ await parseErrorBody(response),
77
+ url,
78
+ "POST",
79
+ );
46
80
  }
47
81
  return transformResponse(await response.json());
48
82
  } catch (error) {
49
83
  if (error.status) throw error;
50
- throw makeNetworkError(error, url, 'POST');
84
+ throw makeNetworkError(error, url, "POST");
51
85
  }
52
86
  }
53
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
+
113
+ /**
114
+ * The server's cap on operations per batch request (plaid.rest-api.v1.batch).
115
+ * A batch queued past it is submitted as consecutive requests.
116
+ */
117
+ const MAX_BATCH_OPS = 1000;
118
+
54
119
  class PlaidClient {
55
120
  /**
56
121
  * Create a new PlaidClient instance
@@ -58,11 +123,24 @@ class PlaidClient {
58
123
  * @param {string} token - The authentication token
59
124
  * @param {object} [options] - Client options
60
125
  * @param {number} [options.timeout=30000] - Per-request timeout in ms (0 or null disables it)
126
+ * @param {number} [options.batchTimeout=180000] - Timeout for batch submissions in ms (0 or null disables it)
61
127
  */
62
128
  constructor(baseUrl, token, options = {}) {
63
- this.baseUrl = baseUrl.replace(/\/$/, '');
129
+ this.baseUrl = baseUrl.replace(/\/$/, "");
64
130
  this.token = token;
65
- this.timeout = options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
131
+ this.timeout =
132
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
133
+ // Batch submissions get a longer budget than a single request. Aborting a
134
+ // batch does NOT stop the server: it keeps running the transaction and
135
+ // holding the single SQLite write lock, so a client that gives up early
136
+ // just adds a retry on top of a write that is still in progress. Falls
137
+ // back to an explicit `timeout` when one was given.
138
+ this.batchTimeout =
139
+ options.batchTimeout !== undefined
140
+ ? options.batchTimeout
141
+ : options.timeout !== undefined
142
+ ? options.timeout
143
+ : DEFAULT_BATCH_TIMEOUT_MS;
66
144
  this.isBatching = false;
67
145
  this.batchOperations = [];
68
146
  this.documentVersions = {};
@@ -88,8 +166,9 @@ class PlaidClient {
88
166
  * @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
89
167
  */
90
168
  create: (vocabItem, tokens, metadata, auditMessage) =>
91
- this._request('POST', '/api/v1/vocab-links', { auditMessage,
92
- body: bodyOf({ 'vocab-item': vocabItem, tokens, metadata }),
169
+ this._request("POST", "/api/v1/vocab-links", {
170
+ auditMessage,
171
+ body: bodyOf({ "vocab-item": vocabItem, tokens, metadata }),
93
172
  }),
94
173
  /**
95
174
  * Create multiple vocab links in a single operation. Entries may
@@ -99,28 +178,37 @@ class PlaidClient {
99
178
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
100
179
  */
101
180
  bulkCreate: (body, auditMessage) =>
102
- this._request('POST', '/api/v1/vocab-links/bulk', { auditMessage, body }),
181
+ this._request("POST", "/api/v1/vocab-links/bulk", {
182
+ auditMessage,
183
+ body,
184
+ }),
103
185
  /**
104
186
  * Delete multiple vocab links in a single operation. Provide an array of IDs.
105
187
  * @param {string[]} body - The vocab link IDs to delete
106
188
  */
107
189
  bulkDelete: (body, auditMessage) =>
108
- this._request('DELETE', '/api/v1/vocab-links/bulk', { auditMessage, body }),
190
+ this._request("DELETE", "/api/v1/vocab-links/bulk", {
191
+ auditMessage,
192
+ body,
193
+ }),
109
194
  /**
110
195
  * Replace all metadata for a vocab link. The entire metadata map is replaced - existing metadata keys not included in the request will be removed.
111
196
  * @param {string} id - The resource ID
112
197
  * @param {any} body - The request body
113
198
  */
114
199
  setMetadata: (id, body, auditMessage) =>
115
- this._request('PUT', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
116
- rawBody: body, skipResponseTransform: true,
200
+ this._request("PUT", `/api/v1/vocab-links/${id}/metadata`, {
201
+ auditMessage,
202
+ rawBody: body,
203
+ skipResponseTransform: true,
117
204
  }),
118
205
  /**
119
206
  * Remove all metadata from a vocab link.
120
207
  * @param {string} id - The resource ID
121
208
  */
122
209
  deleteMetadata: (id, auditMessage) =>
123
- this._request('DELETE', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
210
+ this._request("DELETE", `/api/v1/vocab-links/${id}/metadata`, {
211
+ auditMessage,
124
212
  skipResponseTransform: true,
125
213
  }),
126
214
  /**
@@ -129,8 +217,10 @@ class PlaidClient {
129
217
  * @param {any} body - The metadata patch
130
218
  */
131
219
  patchMetadata: (id, body, auditMessage) =>
132
- this._request('PATCH', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
133
- rawBody: body, skipResponseTransform: true,
220
+ this._request("PATCH", `/api/v1/vocab-links/${id}/metadata`, {
221
+ auditMessage,
222
+ rawBody: body,
223
+ skipResponseTransform: true,
134
224
  }),
135
225
  /**
136
226
  * Get a vocab link by ID
@@ -138,15 +228,15 @@ class PlaidClient {
138
228
  * @param {string} [asOf] - Temporal query timestamp
139
229
  */
140
230
  get: (id, asOf) =>
141
- this._request('GET', `/api/v1/vocab-links/${id}`, {
142
- queryParams: { 'as-of': asOf },
231
+ this._request("GET", `/api/v1/vocab-links/${id}`, {
232
+ queryParams: { "as-of": asOf },
143
233
  }),
144
234
  /**
145
235
  * Delete a vocab link
146
236
  * @param {string} id - The resource ID
147
237
  */
148
238
  delete: (id, auditMessage) =>
149
- this._request('DELETE', `/api/v1/vocab-links/${id}`, { auditMessage }),
239
+ this._request("DELETE", `/api/v1/vocab-links/${id}`, { auditMessage }),
150
240
  };
151
241
 
152
242
  this.vocabLayers = {
@@ -157,22 +247,23 @@ class PlaidClient {
157
247
  * @param {string} [asOf] - Temporal query timestamp
158
248
  */
159
249
  get: (id, includeItems, asOf) =>
160
- this._request('GET', `/api/v1/vocab-layers/${id}`, {
161
- queryParams: { 'include-items': includeItems, 'as-of': asOf },
250
+ this._request("GET", `/api/v1/vocab-layers/${id}`, {
251
+ queryParams: { "include-items": includeItems, "as-of": asOf },
162
252
  }),
163
253
  /**
164
254
  * Delete a vocab layer.
165
255
  * @param {string} id - The resource ID
166
256
  */
167
257
  delete: (id, auditMessage) =>
168
- this._request('DELETE', `/api/v1/vocab-layers/${id}`, { auditMessage }),
258
+ this._request("DELETE", `/api/v1/vocab-layers/${id}`, { auditMessage }),
169
259
  /**
170
260
  * Update a vocab layer's name.
171
261
  * @param {string} id - The resource ID
172
262
  * @param {string} name - The name
173
263
  */
174
264
  update: (id, name, auditMessage) =>
175
- this._request('PATCH', `/api/v1/vocab-layers/${id}`, { auditMessage,
265
+ this._request("PATCH", `/api/v1/vocab-layers/${id}`, {
266
+ auditMessage,
176
267
  body: bodyOf({ name }),
177
268
  }),
178
269
  /**
@@ -183,9 +274,11 @@ class PlaidClient {
183
274
  * @param {any} configValue - Configuration value to set
184
275
  */
185
276
  setConfig: (id, namespace, configKey, configValue, auditMessage) =>
186
- this._request('PUT', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, { auditMessage,
187
- rawBody: configValue, skipResponseTransform: true,
188
- }),
277
+ this._request(
278
+ "PUT",
279
+ `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
280
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
281
+ ),
189
282
  /**
190
283
  * Remove a configuration value for a layer.
191
284
  * @param {string} id - The resource ID
@@ -193,9 +286,11 @@ class PlaidClient {
193
286
  * @param {string} configKey - The config key
194
287
  */
195
288
  deleteConfig: (id, namespace, configKey, auditMessage) =>
196
- this._request('DELETE', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, { auditMessage,
197
- skipResponseTransform: true,
198
- }),
289
+ this._request(
290
+ "DELETE",
291
+ `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
292
+ { auditMessage, skipResponseTransform: true },
293
+ ),
199
294
  /**
200
295
  * List all vocab layers accessible to user. Transparently follows
201
296
  * pagination cursors and returns the full flat array.
@@ -203,7 +298,7 @@ class PlaidClient {
203
298
  * @param {string} [asOf] - Temporal query timestamp
204
299
  */
205
300
  list: (asOf) =>
206
- listAll(this, '/api/v1/vocab-layers', { query: { 'as-of': asOf } }),
301
+ listAll(this, "/api/v1/vocab-layers", { query: { "as-of": asOf } }),
207
302
  /**
208
303
  * Fetch a single page of vocab layers.
209
304
  * @param {object} [opts]
@@ -213,7 +308,11 @@ class PlaidClient {
213
308
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
214
309
  */
215
310
  listPage: ({ limit, cursor, asOf } = {}) =>
216
- listPage(this, '/api/v1/vocab-layers', { limit, cursor, query: { 'as-of': asOf } }),
311
+ listPage(this, "/api/v1/vocab-layers", {
312
+ limit,
313
+ cursor,
314
+ query: { "as-of": asOf },
315
+ }),
217
316
  /**
218
317
  * Async-iterate vocab layers page by page; yields each page's entries array.
219
318
  * @param {object} [opts]
@@ -223,13 +322,17 @@ class PlaidClient {
223
322
  * @returns {AsyncGenerator<Array>}
224
323
  */
225
324
  iterPages: ({ pageSize, asOf } = {}) =>
226
- iterPages(this, '/api/v1/vocab-layers', { pageSize, query: { 'as-of': asOf } }),
325
+ iterPages(this, "/api/v1/vocab-layers", {
326
+ pageSize,
327
+ query: { "as-of": asOf },
328
+ }),
227
329
  /**
228
330
  * Create a new vocab layer. Note: this also registers the user as a maintainer.
229
331
  * @param {string} name - The name
230
332
  */
231
333
  create: (name, auditMessage) =>
232
- this._request('POST', '/api/v1/vocab-layers', { auditMessage,
334
+ this._request("POST", "/api/v1/vocab-layers", {
335
+ auditMessage,
233
336
  body: bodyOf({ name }),
234
337
  }),
235
338
  /**
@@ -238,14 +341,22 @@ class PlaidClient {
238
341
  * @param {string} userId - The user ID
239
342
  */
240
343
  addMaintainer: (id, userId, auditMessage) =>
241
- this._request('POST', `/api/v1/vocab-layers/${id}/maintainers/${userId}`, { auditMessage }),
344
+ this._request(
345
+ "POST",
346
+ `/api/v1/vocab-layers/${id}/maintainers/${userId}`,
347
+ { auditMessage },
348
+ ),
242
349
  /**
243
350
  * Remove a user's maintainer privileges for this vocab layer.
244
351
  * @param {string} id - The resource ID
245
352
  * @param {string} userId - The user ID
246
353
  */
247
354
  removeMaintainer: (id, userId, auditMessage) =>
248
- this._request('DELETE', `/api/v1/vocab-layers/${id}/maintainers/${userId}`, { auditMessage }),
355
+ this._request(
356
+ "DELETE",
357
+ `/api/v1/vocab-layers/${id}/maintainers/${userId}`,
358
+ { auditMessage },
359
+ ),
249
360
  };
250
361
 
251
362
  this.relations = {
@@ -255,15 +366,18 @@ class PlaidClient {
255
366
  * @param {any} body - The request body
256
367
  */
257
368
  setMetadata: (relationId, body, auditMessage) =>
258
- this._request('PUT', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
259
- rawBody: body, skipResponseTransform: true,
369
+ this._request("PUT", `/api/v1/relations/${relationId}/metadata`, {
370
+ auditMessage,
371
+ rawBody: body,
372
+ skipResponseTransform: true,
260
373
  }),
261
374
  /**
262
375
  * Remove all metadata from a relation.
263
376
  * @param {string} relationId - The relation ID
264
377
  */
265
378
  deleteMetadata: (relationId, auditMessage) =>
266
- this._request('DELETE', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
379
+ this._request("DELETE", `/api/v1/relations/${relationId}/metadata`, {
380
+ auditMessage,
267
381
  skipResponseTransform: true,
268
382
  }),
269
383
  /**
@@ -272,8 +386,10 @@ class PlaidClient {
272
386
  * @param {any} body - The metadata patch
273
387
  */
274
388
  patchMetadata: (relationId, body, auditMessage) =>
275
- this._request('PATCH', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
276
- rawBody: body, skipResponseTransform: true,
389
+ this._request("PATCH", `/api/v1/relations/${relationId}/metadata`, {
390
+ auditMessage,
391
+ rawBody: body,
392
+ skipResponseTransform: true,
277
393
  }),
278
394
  /**
279
395
  * Update the target span of a relation.
@@ -281,8 +397,9 @@ class PlaidClient {
281
397
  * @param {string} spanId - The span ID
282
398
  */
283
399
  setTarget: (relationId, spanId, auditMessage) =>
284
- this._request('PUT', `/api/v1/relations/${relationId}/target`, { auditMessage,
285
- body: bodyOf({ 'span-id': spanId }),
400
+ this._request("PUT", `/api/v1/relations/${relationId}/target`, {
401
+ auditMessage,
402
+ body: bodyOf({ "span-id": spanId }),
286
403
  }),
287
404
  /**
288
405
  * Get a relation by ID.
@@ -290,22 +407,25 @@ class PlaidClient {
290
407
  * @param {string} [asOf] - Temporal query timestamp
291
408
  */
292
409
  get: (relationId, asOf) =>
293
- this._request('GET', `/api/v1/relations/${relationId}`, {
294
- queryParams: { 'as-of': asOf },
410
+ this._request("GET", `/api/v1/relations/${relationId}`, {
411
+ queryParams: { "as-of": asOf },
295
412
  }),
296
413
  /**
297
414
  * Delete a relation.
298
415
  * @param {string} relationId - The relation ID
299
416
  */
300
417
  delete: (relationId, auditMessage) =>
301
- this._request('DELETE', `/api/v1/relations/${relationId}`, { auditMessage }),
418
+ this._request("DELETE", `/api/v1/relations/${relationId}`, {
419
+ auditMessage,
420
+ }),
302
421
  /**
303
422
  * Update a relation's value.
304
423
  * @param {string} relationId - The relation ID
305
424
  * @param {any} value - The value
306
425
  */
307
426
  update: (relationId, value, auditMessage) =>
308
- this._request('PATCH', `/api/v1/relations/${relationId}`, { auditMessage,
427
+ this._request("PATCH", `/api/v1/relations/${relationId}`, {
428
+ auditMessage,
309
429
  body: bodyOf({ value }),
310
430
  }),
311
431
  /**
@@ -314,8 +434,9 @@ class PlaidClient {
314
434
  * @param {string} spanId - The span ID
315
435
  */
316
436
  setSource: (relationId, spanId, auditMessage) =>
317
- this._request('PUT', `/api/v1/relations/${relationId}/source`, { auditMessage,
318
- body: bodyOf({ 'span-id': spanId }),
437
+ this._request("PUT", `/api/v1/relations/${relationId}/source`, {
438
+ auditMessage,
439
+ body: bodyOf({ "span-id": spanId }),
319
440
  }),
320
441
  /**
321
442
  * Create a new relation. A relation is a directed edge between two spans
@@ -328,8 +449,15 @@ class PlaidClient {
328
449
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
329
450
  */
330
451
  create: (layerId, sourceId, targetId, value, metadata, auditMessage) =>
331
- this._request('POST', '/api/v1/relations', { auditMessage,
332
- body: bodyOf({ 'layer-id': layerId, 'source-id': sourceId, 'target-id': targetId, value, metadata }),
452
+ this._request("POST", "/api/v1/relations", {
453
+ auditMessage,
454
+ body: bodyOf({
455
+ "layer-id": layerId,
456
+ "source-id": sourceId,
457
+ "target-id": targetId,
458
+ value,
459
+ metadata,
460
+ }),
333
461
  }),
334
462
  /**
335
463
  * Create multiple relations in a single operation.
@@ -337,13 +465,16 @@ class PlaidClient {
337
465
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
338
466
  */
339
467
  bulkCreate: (body, auditMessage) =>
340
- this._request('POST', '/api/v1/relations/bulk', { auditMessage, body }),
468
+ this._request("POST", "/api/v1/relations/bulk", { auditMessage, body }),
341
469
  /**
342
470
  * Delete multiple relations in a single operation. Provide an array of IDs.
343
471
  * @param {Array} body - The request body
344
472
  */
345
473
  bulkDelete: (body, auditMessage) =>
346
- this._request('DELETE', '/api/v1/relations/bulk', { auditMessage, body }),
474
+ this._request("DELETE", "/api/v1/relations/bulk", {
475
+ auditMessage,
476
+ body,
477
+ }),
347
478
  };
348
479
 
349
480
  this.spanLayers = {
@@ -354,10 +485,18 @@ class PlaidClient {
354
485
  * @param {string} configKey - The config key
355
486
  * @param {any} configValue - Configuration value to set
356
487
  */
357
- setConfig: (spanLayerId, namespace, configKey, configValue, auditMessage) =>
358
- this._request('PUT', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, { auditMessage,
359
- rawBody: configValue, skipResponseTransform: true,
360
- }),
488
+ setConfig: (
489
+ spanLayerId,
490
+ namespace,
491
+ configKey,
492
+ configValue,
493
+ auditMessage,
494
+ ) =>
495
+ this._request(
496
+ "PUT",
497
+ `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
498
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
499
+ ),
361
500
  /**
362
501
  * Remove a configuration value for a layer.
363
502
  * @param {string} spanLayerId - The span layer ID
@@ -365,31 +504,36 @@ class PlaidClient {
365
504
  * @param {string} configKey - The config key
366
505
  */
367
506
  deleteConfig: (spanLayerId, namespace, configKey, auditMessage) =>
368
- this._request('DELETE', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, { auditMessage,
369
- skipResponseTransform: true,
370
- }),
507
+ this._request(
508
+ "DELETE",
509
+ `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
510
+ { auditMessage, skipResponseTransform: true },
511
+ ),
371
512
  /**
372
513
  * Get a span layer by ID.
373
514
  * @param {string} spanLayerId - The span layer ID
374
515
  * @param {string} [asOf] - Temporal query timestamp
375
516
  */
376
517
  get: (spanLayerId, asOf) =>
377
- this._request('GET', `/api/v1/span-layers/${spanLayerId}`, {
378
- queryParams: { 'as-of': asOf },
518
+ this._request("GET", `/api/v1/span-layers/${spanLayerId}`, {
519
+ queryParams: { "as-of": asOf },
379
520
  }),
380
521
  /**
381
522
  * Delete a span layer.
382
523
  * @param {string} spanLayerId - The span layer ID
383
524
  */
384
525
  delete: (spanLayerId, auditMessage) =>
385
- this._request('DELETE', `/api/v1/span-layers/${spanLayerId}`, { auditMessage }),
526
+ this._request("DELETE", `/api/v1/span-layers/${spanLayerId}`, {
527
+ auditMessage,
528
+ }),
386
529
  /**
387
530
  * Update a span layer's name.
388
531
  * @param {string} spanLayerId - The span layer ID
389
532
  * @param {string} name - The name
390
533
  */
391
534
  update: (spanLayerId, name, auditMessage) =>
392
- this._request('PATCH', `/api/v1/span-layers/${spanLayerId}`, { auditMessage,
535
+ this._request("PATCH", `/api/v1/span-layers/${spanLayerId}`, {
536
+ auditMessage,
393
537
  body: bodyOf({ name }),
394
538
  }),
395
539
  /**
@@ -398,8 +542,9 @@ class PlaidClient {
398
542
  * @param {string} name - The name
399
543
  */
400
544
  create: (tokenLayerId, name, auditMessage) =>
401
- this._request('POST', '/api/v1/span-layers', { auditMessage,
402
- body: bodyOf({ 'token-layer-id': tokenLayerId, name }),
545
+ this._request("POST", "/api/v1/span-layers", {
546
+ auditMessage,
547
+ body: bodyOf({ "token-layer-id": tokenLayerId, name }),
403
548
  }),
404
549
  /**
405
550
  * Shift a span layer's display order.
@@ -407,7 +552,8 @@ class PlaidClient {
407
552
  * @param {string} direction - The direction ("up" or "down")
408
553
  */
409
554
  shift: (spanLayerId, direction, auditMessage) =>
410
- this._request('POST', `/api/v1/span-layers/${spanLayerId}/shift`, { auditMessage,
555
+ this._request("POST", `/api/v1/span-layers/${spanLayerId}/shift`, {
556
+ auditMessage,
411
557
  body: bodyOf({ direction }),
412
558
  }),
413
559
  };
@@ -419,7 +565,8 @@ class PlaidClient {
419
565
  * @param {Array} tokens - The tokens
420
566
  */
421
567
  setTokens: (spanId, tokens, auditMessage) =>
422
- this._request('PUT', `/api/v1/spans/${spanId}/tokens`, { auditMessage,
568
+ this._request("PUT", `/api/v1/spans/${spanId}/tokens`, {
569
+ auditMessage,
423
570
  body: bodyOf({ tokens }),
424
571
  }),
425
572
  /**
@@ -431,8 +578,14 @@ class PlaidClient {
431
578
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
432
579
  */
433
580
  create: (spanLayerId, tokens, value, metadata, auditMessage) =>
434
- this._request('POST', '/api/v1/spans', { auditMessage,
435
- body: bodyOf({ 'span-layer-id': spanLayerId, tokens, value, metadata }),
581
+ this._request("POST", "/api/v1/spans", {
582
+ auditMessage,
583
+ body: bodyOf({
584
+ "span-layer-id": spanLayerId,
585
+ tokens,
586
+ value,
587
+ metadata,
588
+ }),
436
589
  }),
437
590
  /**
438
591
  * Get a span by ID.
@@ -440,22 +593,23 @@ class PlaidClient {
440
593
  * @param {string} [asOf] - Temporal query timestamp
441
594
  */
442
595
  get: (spanId, asOf) =>
443
- this._request('GET', `/api/v1/spans/${spanId}`, {
444
- queryParams: { 'as-of': asOf },
596
+ this._request("GET", `/api/v1/spans/${spanId}`, {
597
+ queryParams: { "as-of": asOf },
445
598
  }),
446
599
  /**
447
600
  * Delete a span.
448
601
  * @param {string} spanId - The span ID
449
602
  */
450
603
  delete: (spanId, auditMessage) =>
451
- this._request('DELETE', `/api/v1/spans/${spanId}`, { auditMessage }),
604
+ this._request("DELETE", `/api/v1/spans/${spanId}`, { auditMessage }),
452
605
  /**
453
606
  * Update a span's value.
454
607
  * @param {string} spanId - The span ID
455
608
  * @param {any} value - The value
456
609
  */
457
610
  update: (spanId, value, auditMessage) =>
458
- this._request('PATCH', `/api/v1/spans/${spanId}`, { auditMessage,
611
+ this._request("PATCH", `/api/v1/spans/${spanId}`, {
612
+ auditMessage,
459
613
  body: bodyOf({ value }),
460
614
  }),
461
615
  /**
@@ -464,28 +618,31 @@ class PlaidClient {
464
618
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
465
619
  */
466
620
  bulkCreate: (body, auditMessage) =>
467
- this._request('POST', '/api/v1/spans/bulk', { auditMessage, body }),
621
+ this._request("POST", "/api/v1/spans/bulk", { auditMessage, body }),
468
622
  /**
469
623
  * Delete multiple spans in a single operation. Provide an array of IDs.
470
624
  * @param {Array} body - The request body
471
625
  */
472
626
  bulkDelete: (body, auditMessage) =>
473
- this._request('DELETE', '/api/v1/spans/bulk', { auditMessage, body }),
627
+ this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
474
628
  /**
475
629
  * Replace all metadata for a span.
476
630
  * @param {string} spanId - The span ID
477
631
  * @param {any} body - The request body
478
632
  */
479
633
  setMetadata: (spanId, body, auditMessage) =>
480
- this._request('PUT', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
481
- rawBody: body, skipResponseTransform: true,
634
+ this._request("PUT", `/api/v1/spans/${spanId}/metadata`, {
635
+ auditMessage,
636
+ rawBody: body,
637
+ skipResponseTransform: true,
482
638
  }),
483
639
  /**
484
640
  * Remove all metadata from a span.
485
641
  * @param {string} spanId - The span ID
486
642
  */
487
643
  deleteMetadata: (spanId, auditMessage) =>
488
- this._request('DELETE', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
644
+ this._request("DELETE", `/api/v1/spans/${spanId}/metadata`, {
645
+ auditMessage,
489
646
  skipResponseTransform: true,
490
647
  }),
491
648
  /**
@@ -494,11 +651,49 @@ class PlaidClient {
494
651
  * @param {any} body - The metadata patch
495
652
  */
496
653
  patchMetadata: (spanId, body, auditMessage) =>
497
- this._request('PATCH', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
498
- rawBody: body, skipResponseTransform: true,
654
+ this._request("PATCH", `/api/v1/spans/${spanId}/metadata`, {
655
+ auditMessage,
656
+ rawBody: body,
657
+ skipResponseTransform: true,
499
658
  }),
500
659
  };
501
660
 
661
+ // Server-level facts. Fetched at most once: the limits cannot change while
662
+ // the server is up, and a caller asking "will this file be accepted" should
663
+ // not pay a round trip to find out.
664
+ let infoPromise = null;
665
+ this.server = {
666
+ /**
667
+ * This server's version and the limits it enforces, e.g.
668
+ * `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
669
+ * Sizes are in bytes. Unauthenticated.
670
+ */
671
+ info: () => {
672
+ if (!infoPromise) {
673
+ // bypassBatch: a read belongs to whoever asked for it, not to
674
+ // whatever batch happens to be open on this shared client.
675
+ infoPromise = this._request("GET", "/api/v1/info", {
676
+ bypassBatch: true,
677
+ }).catch((err) => {
678
+ // A failure must not be cached: a client that starts before the
679
+ // server is up would never see the limits at all.
680
+ infoPromise = null;
681
+ throw err;
682
+ });
683
+ }
684
+ return infoPromise;
685
+ },
686
+ /** Just the limits, which is what a caller almost always wants. */
687
+ limits: async () => (await this.server.info()).limits,
688
+ /**
689
+ * Liveness, version and database size. Unauthenticated, and served
690
+ * outside the REST router at /health, so it answers even while the API
691
+ * is refusing requests. Never cached — the point is that it is current.
692
+ * @returns {Promise<{ok: boolean, version: string, uptimeMs: number, audit: object}>}
693
+ */
694
+ health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
695
+ };
696
+
502
697
  this.batch = {
503
698
  /**
504
699
  * Execute multiple API operations atomically. If any operation fails, all
@@ -506,8 +701,10 @@ class PlaidClient {
506
701
  * @param {Array} body - The request body
507
702
  */
508
703
  submit: (body, auditMessage) =>
509
- this._request('POST', '/api/v1/batch', { auditMessage,
510
- body, noBatch: true,
704
+ this._request("POST", "/api/v1/batch", {
705
+ auditMessage,
706
+ body,
707
+ noBatch: true,
511
708
  }),
512
709
  };
513
710
 
@@ -518,15 +715,18 @@ class PlaidClient {
518
715
  * @param {any} body - The request body
519
716
  */
520
717
  setMetadata: (textId, body, auditMessage) =>
521
- this._request('PUT', `/api/v1/texts/${textId}/metadata`, { auditMessage,
522
- rawBody: body, skipResponseTransform: true,
718
+ this._request("PUT", `/api/v1/texts/${textId}/metadata`, {
719
+ auditMessage,
720
+ rawBody: body,
721
+ skipResponseTransform: true,
523
722
  }),
524
723
  /**
525
724
  * Remove all metadata from a text.
526
725
  * @param {string} textId - The text ID
527
726
  */
528
727
  deleteMetadata: (textId, auditMessage) =>
529
- this._request('DELETE', `/api/v1/texts/${textId}/metadata`, { auditMessage,
728
+ this._request("DELETE", `/api/v1/texts/${textId}/metadata`, {
729
+ auditMessage,
530
730
  skipResponseTransform: true,
531
731
  }),
532
732
  /**
@@ -535,8 +735,10 @@ class PlaidClient {
535
735
  * @param {any} body - The metadata patch
536
736
  */
537
737
  patchMetadata: (textId, body, auditMessage) =>
538
- this._request('PATCH', `/api/v1/texts/${textId}/metadata`, { auditMessage,
539
- rawBody: body, skipResponseTransform: true,
738
+ this._request("PATCH", `/api/v1/texts/${textId}/metadata`, {
739
+ auditMessage,
740
+ rawBody: body,
741
+ skipResponseTransform: true,
540
742
  }),
541
743
  /**
542
744
  * Create a new text in a document's text layer. A text is a container for
@@ -547,8 +749,14 @@ class PlaidClient {
547
749
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
548
750
  */
549
751
  create: (textLayerId, documentId, body, metadata, auditMessage) =>
550
- this._request('POST', '/api/v1/texts', { auditMessage,
551
- body: bodyOf({ 'text-layer-id': textLayerId, 'document-id': documentId, body, metadata }),
752
+ this._request("POST", "/api/v1/texts", {
753
+ auditMessage,
754
+ body: bodyOf({
755
+ "text-layer-id": textLayerId,
756
+ "document-id": documentId,
757
+ body,
758
+ metadata,
759
+ }),
552
760
  }),
553
761
  /**
554
762
  * Get a text.
@@ -556,24 +764,30 @@ class PlaidClient {
556
764
  * @param {string} [asOf] - Temporal query timestamp
557
765
  */
558
766
  get: (textId, asOf) =>
559
- this._request('GET', `/api/v1/texts/${textId}`, {
560
- queryParams: { 'as-of': asOf },
767
+ this._request("GET", `/api/v1/texts/${textId}`, {
768
+ queryParams: { "as-of": asOf },
561
769
  }),
562
770
  /**
563
771
  * Delete a text and all dependent data.
564
772
  * @param {string} textId - The text ID
565
773
  */
566
774
  delete: (textId, auditMessage) =>
567
- this._request('DELETE', `/api/v1/texts/${textId}`, { auditMessage }),
775
+ this._request("DELETE", `/api/v1/texts/${textId}`, { auditMessage }),
568
776
  /**
569
777
  * Update a text's body. A diff is computed and token indices are updated
570
778
  * so that tokens remain intact. Alternatively, `body` can be a list of
571
- * edit directives.
779
+ * edit directives (code-point indices, applied in order):
780
+ * {type: 'delete', index, value: count}
781
+ * {type: 'insert', index, value: string}
782
+ * {type: 'replace', index, length, value: string} — unlike
783
+ * delete+insert, a token covering the whole range is resized to keep
784
+ * it (respell a word in place without losing its annotations).
572
785
  * @param {string} textId - The text ID
573
786
  * @param {any} body - The request body
574
787
  */
575
788
  update: (textId, body, auditMessage) =>
576
- this._request('PATCH', `/api/v1/texts/${textId}`, { auditMessage,
789
+ this._request("PATCH", `/api/v1/texts/${textId}`, {
790
+ auditMessage,
577
791
  body: bodyOf({ body }),
578
792
  }),
579
793
  };
@@ -584,42 +798,58 @@ class PlaidClient {
584
798
  * returns the full flat array. Admin-or-maintainer only.
585
799
  * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
586
800
  * @param {object} [opts]
587
- * @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
801
+ * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
588
802
  * @param {string} [opts.asOf] - Temporal query timestamp
589
803
  */
590
804
  list: ({ q, asOf } = {}) =>
591
- listAll(this, '/api/v1/users', { query: { q, 'as-of': asOf } }),
805
+ listAll(this, "/api/v1/users", { query: { q, "as-of": asOf } }),
592
806
  /**
593
807
  * Fetch a single page of users (optionally filtered by `q`).
594
808
  * @param {object} [opts]
595
- * @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
809
+ * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
596
810
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
597
811
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
598
812
  * @param {string} [opts.asOf] - Temporal query timestamp
599
813
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
600
814
  */
601
815
  listPage: ({ q, limit, cursor, asOf } = {}) =>
602
- listPage(this, '/api/v1/users', { limit, cursor, query: { q, 'as-of': asOf } }),
816
+ listPage(this, "/api/v1/users", {
817
+ limit,
818
+ cursor,
819
+ query: { q, "as-of": asOf },
820
+ }),
603
821
  /**
604
822
  * Async-iterate users page by page; yields each page's entries array.
605
823
  * @param {object} [opts]
606
- * @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
824
+ * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
607
825
  * @param {number} [opts.pageSize] - Per-request page size
608
826
  * @param {string} [opts.asOf] - Temporal query timestamp
609
827
  * 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.
610
828
  * @returns {AsyncGenerator<Array>}
611
829
  */
612
830
  iterPages: ({ q, pageSize, asOf } = {}) =>
613
- iterPages(this, '/api/v1/users', { pageSize, query: { q, 'as-of': asOf } }),
831
+ iterPages(this, "/api/v1/users", {
832
+ pageSize,
833
+ query: { q, "as-of": asOf },
834
+ }),
614
835
  /**
615
- * Create a new user
616
- * @param {string} username - The username
836
+ * Create a new user.
837
+ * @param {string} email - The account's email address. It becomes the
838
+ * user's id and is what they log in with; it can never be changed.
617
839
  * @param {string} password - The password
618
840
  * @param {boolean} isAdmin - Whether the user is an admin
841
+ * @param {string} [displayName] - How the user is shown in the UI.
842
+ * Defaults to the local part of the email.
619
843
  */
620
- create: (username, password, isAdmin, auditMessage) =>
621
- this._request('POST', '/api/v1/users', { auditMessage,
622
- body: bodyOf({ username, password, 'is-admin': isAdmin }),
844
+ create: (email, password, isAdmin, displayName, auditMessage) =>
845
+ this._request("POST", "/api/v1/users", {
846
+ auditMessage,
847
+ body: bodyOf({
848
+ email,
849
+ password,
850
+ "is-admin": isAdmin,
851
+ "display-name": displayName,
852
+ }),
623
853
  }),
624
854
  /**
625
855
  * Get audit log for a user's actions. Transparently follows pagination
@@ -629,10 +859,44 @@ class PlaidClient {
629
859
  * @param {string} [startTime] - Start of time range
630
860
  * @param {string} [endTime] - End of time range
631
861
  * @param {string} [asOf] - Temporal query timestamp
862
+ * @param {string[]|string} [opTypes] - Only return operations of these
863
+ * types, spelled as in an entry's `op/type` (e.g.
864
+ * `['span-layer/create', 'span-layer/delete']`). An entry appears when
865
+ * one of its operations matches, carrying only the ones that did.
632
866
  */
633
- audit: (userId, startTime, endTime, asOf) =>
867
+ audit: (userId, startTime, endTime, asOf, opTypes) =>
634
868
  listAll(this, `/api/v1/users/${userId}/audit`, {
635
- query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
869
+ query: {
870
+ "start-time": startTime,
871
+ "end-time": endTime,
872
+ "as-of": asOf,
873
+ "op-types": opTypesParam(opTypes),
874
+ },
875
+ }),
876
+ /**
877
+ * One page of the same log, newest-first with `order: "desc"`. Use this
878
+ * rather than audit() wherever the caller wants the recent end of a log
879
+ * that may be long: audit() walks every page before it resolves.
880
+ * @param {object} [opts]
881
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
882
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
883
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
884
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
885
+ */
886
+ auditPage: (
887
+ userId,
888
+ { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
889
+ ) =>
890
+ listPage(this, `/api/v1/users/${userId}/audit`, {
891
+ limit,
892
+ cursor,
893
+ query: {
894
+ "start-time": startTime,
895
+ "end-time": endTime,
896
+ "as-of": asOf,
897
+ "op-types": opTypesParam(opTypes),
898
+ order,
899
+ },
636
900
  }),
637
901
  /**
638
902
  * Get a user by ID
@@ -640,8 +904,8 @@ class PlaidClient {
640
904
  * @param {string} [asOf] - Temporal query timestamp
641
905
  */
642
906
  get: (id, asOf) =>
643
- this._request('GET', `/api/v1/users/${id}`, {
644
- queryParams: { 'as-of': asOf },
907
+ this._request("GET", `/api/v1/users/${id}`, {
908
+ queryParams: { "as-of": asOf },
645
909
  }),
646
910
  /**
647
911
  * Deactivate a user. Users are never hard-deleted: deactivation
@@ -652,7 +916,7 @@ class PlaidClient {
652
916
  * @param {string} id - The resource ID
653
917
  */
654
918
  delete: (id, auditMessage) =>
655
- this._request('DELETE', `/api/v1/users/${id}`, { auditMessage }),
919
+ this._request("DELETE", `/api/v1/users/${id}`, { auditMessage }),
656
920
  /**
657
921
  * Reactivate a deactivated user, restoring their ability to log in.
658
922
  * Project memberships, vocab maintainerships, and API tokens removed
@@ -660,19 +924,27 @@ class PlaidClient {
660
924
  * @param {string} id - The resource ID
661
925
  */
662
926
  activate: (id, auditMessage) =>
663
- this._request('POST', `/api/v1/users/${id}/activate`, { auditMessage }),
927
+ this._request("POST", `/api/v1/users/${id}/activate`, { auditMessage }),
664
928
  /**
665
- * Modify a user. Admins may change the username, password, and admin
666
- * status of any user. All other users may only modify their own username
667
- * or password.
668
- * @param {string} id - The resource ID
929
+ * Modify a user. Admins may change the display name, password, and admin
930
+ * status of any user. All other users may only modify their own display
931
+ * name or password.
932
+ *
933
+ * A user's id is their email address and is fixed for the life of the
934
+ * account — it is what they log in with, so nothing can change it.
935
+ * @param {string} id - The resource ID (the user's email address)
669
936
  * @param {string} [password] - New password
670
- * @param {string} [username] - New username
937
+ * @param {string} [displayName] - New display name
671
938
  * @param {boolean} [isAdmin] - New admin status
672
939
  */
673
- update: (id, password, username, isAdmin, auditMessage) =>
674
- this._request('PATCH', `/api/v1/users/${id}`, { auditMessage,
675
- body: bodyOf({ password, username, 'is-admin': isAdmin }),
940
+ update: (id, password, displayName, isAdmin, auditMessage) =>
941
+ this._request("PATCH", `/api/v1/users/${id}`, {
942
+ auditMessage,
943
+ body: bodyOf({
944
+ password,
945
+ "display-name": displayName,
946
+ "is-admin": isAdmin,
947
+ }),
676
948
  }),
677
949
  /**
678
950
  * Build a URL for a user's profile picture, suitable for use directly as
@@ -692,7 +964,7 @@ class PlaidClient {
692
964
  avatarUrl: (id, avatarHash) => {
693
965
  if (avatarHash === null) return null;
694
966
  const params = new URLSearchParams({ token: this.token });
695
- if (avatarHash) params.set('v', avatarHash);
967
+ if (avatarHash) params.set("v", avatarHash);
696
968
  return `${this.baseUrl}/api/v1/users/${id}/avatar?${params}`;
697
969
  },
698
970
  /**
@@ -701,7 +973,7 @@ class PlaidClient {
701
973
  * @param {string} id - The user ID
702
974
  */
703
975
  getAvatar: (id) =>
704
- this._request('GET', `/api/v1/users/${id}/avatar`, {
976
+ this._request("GET", `/api/v1/users/${id}/avatar`, {
705
977
  noBatch: true,
706
978
  binaryResponse: true,
707
979
  }),
@@ -715,9 +987,12 @@ class PlaidClient {
715
987
  */
716
988
  setAvatar: (id, file, auditMessage) => {
717
989
  const fd = new FormData();
718
- fd.append('file', file);
719
- return this._request('PUT', `/api/v1/users/${id}/avatar`, { auditMessage,
720
- body: fd, formData: true, noBatch: true,
990
+ fd.append("file", file);
991
+ return this._request("PUT", `/api/v1/users/${id}/avatar`, {
992
+ auditMessage,
993
+ body: fd,
994
+ formData: true,
995
+ noBatch: true,
721
996
  });
722
997
  },
723
998
  /**
@@ -725,11 +1000,72 @@ class PlaidClient {
725
1000
  * @param {string} id - The user ID
726
1001
  */
727
1002
  deleteAvatar: (id, auditMessage) =>
728
- this._request('DELETE', `/api/v1/users/${id}/avatar`, { auditMessage,
1003
+ this._request("DELETE", `/api/v1/users/${id}/avatar`, {
1004
+ auditMessage,
729
1005
  noBatch: true,
730
1006
  }),
731
1007
  };
732
1008
 
1009
+ this.userData = {
1010
+ /**
1011
+ * List a user's private data entries ({key, updatedAt}, plus value when
1012
+ * includeValues). Owner or admin only.
1013
+ * @param {string} userId
1014
+ * @param {object} [opts]
1015
+ * @param {string} [opts.prefix] - Only keys starting with this prefix
1016
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
1017
+ */
1018
+ list: (userId, { prefix, includeValues } = {}) =>
1019
+ this._request("GET", `/api/v1/users/${userId}/data`, {
1020
+ queryParams: { prefix, "include-values": includeValues },
1021
+ }),
1022
+ /**
1023
+ * Read one private data entry ({key, updatedAt, value}); 404 if absent.
1024
+ * @param {string} userId
1025
+ * @param {string} key
1026
+ */
1027
+ get: (userId, key) =>
1028
+ this._request(
1029
+ "GET",
1030
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1031
+ ),
1032
+ /**
1033
+ * Create or replace one private data entry. `value` is any JSON (up to
1034
+ * 1 MB), stored as sent. Not audited, not batchable.
1035
+ *
1036
+ * The server stores it verbatim, but this client recases object keys on
1037
+ * the way out and back like any other body (`myKey` <-> `my-key`), so a
1038
+ * value whose keys are camelCase round-trips unchanged while one keyed
1039
+ * by arbitrary strings does not. Put such a map under a `metadata` key,
1040
+ * which both clients pass through untouched.
1041
+ * @param {string} userId
1042
+ * @param {string} key
1043
+ * @param {*} value
1044
+ */
1045
+ put: (userId, key, value) =>
1046
+ this._request(
1047
+ "PUT",
1048
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1049
+ {
1050
+ body: value,
1051
+ noBatch: true,
1052
+ },
1053
+ ),
1054
+ /**
1055
+ * Delete one private data entry; 404 if absent.
1056
+ * @param {string} userId
1057
+ * @param {string} key
1058
+ */
1059
+ delete: (userId, key) =>
1060
+ this._request(
1061
+ "DELETE",
1062
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1063
+ {
1064
+ noBatch: true,
1065
+ },
1066
+ ),
1067
+ };
1068
+
733
1069
  this.apiTokens = {
734
1070
  /**
735
1071
  * List a user's named API tokens. Never includes the signed token
@@ -738,8 +1074,7 @@ class PlaidClient {
738
1074
  * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
739
1075
  * @param {string} userId - The user ID who owns the tokens
740
1076
  */
741
- list: (userId) =>
742
- listAll(this, `/api/v1/users/${userId}/tokens`),
1077
+ list: (userId) => listAll(this, `/api/v1/users/${userId}/tokens`),
743
1078
  /**
744
1079
  * Fetch a single page of a user's named API tokens.
745
1080
  * @param {string} userId - The user ID who owns the tokens
@@ -770,7 +1105,8 @@ class PlaidClient {
770
1105
  * @returns {Promise<{id: string, name: string, token: string}>}
771
1106
  */
772
1107
  create: (userId, name, auditMessage) =>
773
- this._request('POST', `/api/v1/users/${userId}/tokens`, { auditMessage,
1108
+ this._request("POST", `/api/v1/users/${userId}/tokens`, {
1109
+ auditMessage,
774
1110
  body: bodyOf({ name }),
775
1111
  }),
776
1112
  /**
@@ -779,7 +1115,9 @@ class PlaidClient {
779
1115
  * @param {string} tokenId - The token ID to revoke
780
1116
  */
781
1117
  revoke: (userId, tokenId, auditMessage) =>
782
- this._request('DELETE', `/api/v1/users/${userId}/tokens/${tokenId}`, { auditMessage }),
1118
+ this._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
1119
+ auditMessage,
1120
+ }),
783
1121
  };
784
1122
 
785
1123
  this.invites = {
@@ -792,29 +1130,41 @@ class PlaidClient {
792
1130
  * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
793
1131
  * @param {object} [opts]
794
1132
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1133
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
795
1134
  */
796
- list: ({ projectId } = {}) =>
797
- listAll(this, '/api/v1/invites', { query: { 'project-id': projectId } }),
1135
+ list: ({ projectId, all } = {}) =>
1136
+ listAll(this, "/api/v1/invites", {
1137
+ query: { "project-id": projectId, all },
1138
+ }),
798
1139
  /**
799
1140
  * Fetch a single page of invites.
800
1141
  * @param {object} [opts]
801
1142
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1143
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
802
1144
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
803
1145
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
804
1146
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
805
1147
  */
806
- listPage: ({ projectId, limit, cursor } = {}) =>
807
- listPage(this, '/api/v1/invites', { limit, cursor, query: { 'project-id': projectId } }),
1148
+ listPage: ({ projectId, all, limit, cursor } = {}) =>
1149
+ listPage(this, "/api/v1/invites", {
1150
+ limit,
1151
+ cursor,
1152
+ query: { "project-id": projectId, all },
1153
+ }),
808
1154
  /**
809
1155
  * Async-iterate invites page by page; yields each page's entries array.
810
1156
  * @param {object} [opts]
811
1157
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1158
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
812
1159
  * @param {number} [opts.pageSize] - Per-request page size
813
1160
  * 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.
814
1161
  * @returns {AsyncGenerator<Array>}
815
1162
  */
816
- iterPages: ({ projectId, pageSize } = {}) =>
817
- iterPages(this, '/api/v1/invites', { pageSize, query: { 'project-id': projectId } }),
1163
+ iterPages: ({ projectId, all, pageSize } = {}) =>
1164
+ iterPages(this, "/api/v1/invites", {
1165
+ pageSize,
1166
+ query: { "project-id": projectId, all },
1167
+ }),
818
1168
  /**
819
1169
  * Mint an invite. The returned `code` is shown ONLY here — it is never
820
1170
  * stored and cannot be recovered, so build and hand off the link now.
@@ -822,23 +1172,46 @@ class PlaidClient {
822
1172
  *
823
1173
  * Admins may mint anything. A project maintainer may mint role grants on
824
1174
  * projects they maintain, and nothing else: no admin grant, no grantless
825
- * invite, no password resets.
826
- * @param {object} opts
827
- * @param {string} [opts.projectId] - Project the redeemer joins (with projectRole)
828
- * @param {string} [opts.projectRole] - "reader" | "writer" | "maintainer"
1175
+ * invite, no password resets — so for a non-admin, projectId/projectRole
1176
+ * are required in practice (403 without them).
1177
+ *
1178
+ * EVERY option is optional; `create()` with no arguments mints a
1179
+ * single-use signup link granting nothing but an account. Two pairing
1180
+ * rules the server enforces with a 400: projectId and projectRole must be
1181
+ * given TOGETHER, and targetUserId may not be combined with projectId,
1182
+ * grantAdmin, or a maxUses above 1.
1183
+ * @param {object} [opts]
1184
+ * @param {string} [opts.projectId] - Project the redeemer joins (requires projectRole)
1185
+ * @param {string} [opts.projectRole] - "reader" | "writer" | "maintainer" (requires projectId)
829
1186
  * @param {boolean} [opts.grantAdmin] - Make the new account a global admin (admin only)
830
- * @param {string} [opts.targetUserId] - Make this a password reset for that user (admin only)
1187
+ * @param {string} [opts.targetUserId] - Password reset for that user instead of a signup; admin only, single-use, grants nothing
831
1188
  * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
832
1189
  * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
833
1190
  * @param {string} [opts.note] - Human label shown in your invite list
834
1191
  * @returns {Promise<{id: string, code: string, kind: string, status: string}>}
835
1192
  */
836
- create: ({ projectId, projectRole, grantAdmin, targetUserId, maxUses, ttlDays, note } = {}, auditMessage) =>
837
- this._request('POST', '/api/v1/invites', { auditMessage,
1193
+ create: (
1194
+ {
1195
+ projectId,
1196
+ projectRole,
1197
+ grantAdmin,
1198
+ targetUserId,
1199
+ maxUses,
1200
+ ttlDays,
1201
+ note,
1202
+ } = {},
1203
+ auditMessage,
1204
+ ) =>
1205
+ this._request("POST", "/api/v1/invites", {
1206
+ auditMessage,
838
1207
  body: bodyOf({
839
- 'project-id': projectId, 'project-role': projectRole,
840
- 'grant-admin': grantAdmin, 'target-user-id': targetUserId,
841
- 'max-uses': maxUses, 'ttl-days': ttlDays, note,
1208
+ "project-id": projectId,
1209
+ "project-role": projectRole,
1210
+ "grant-admin": grantAdmin,
1211
+ "target-user-id": targetUserId,
1212
+ "max-uses": maxUses,
1213
+ "ttl-days": ttlDays,
1214
+ note,
842
1215
  }),
843
1216
  }),
844
1217
  /**
@@ -847,7 +1220,207 @@ class PlaidClient {
847
1220
  * @param {string} id - The invite ID
848
1221
  */
849
1222
  revoke: (id, auditMessage) =>
850
- this._request('DELETE', `/api/v1/invites/${id}`, { auditMessage }),
1223
+ this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
1224
+ };
1225
+
1226
+ this.admin = {
1227
+ /**
1228
+ * Everything about the server in one read: version and JVM uptime,
1229
+ * database size and per-table row counts, media directory usage, backup
1230
+ * configuration and the backups on disk, and the settings an operator
1231
+ * gets asked to confirm. Carries no secrets. Runs a count per table and
1232
+ * walks the media directory, so open it, do not poll it. Admin only.
1233
+ * @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
1234
+ */
1235
+ server: () => this._request("GET", "/api/v1/admin/server"),
1236
+ /**
1237
+ * Take a database backup right now, outside the nightly schedule.
1238
+ * Resolves to the backup block with `ok` reporting whether the snapshot
1239
+ * succeeded. Uses VACUUM INTO, which only reads, so it is safe while
1240
+ * people are working. Admin only.
1241
+ * @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
1242
+ */
1243
+ backup: () => this._request("POST", "/api/v1/admin/backup"),
1244
+ /**
1245
+ * Documents currently held by an editing lock, with who holds each and
1246
+ * when it expires on its own. Admin only.
1247
+ * @returns {Promise<{entries: Array<{documentId: string, userId: string, expiresAt: number}>}>}
1248
+ */
1249
+ locks: () => this._request("GET", "/api/v1/admin/locks"),
1250
+ /**
1251
+ * Drop the lock on a document whoever holds it. Idempotent. For a client
1252
+ * that went away without releasing one. Admin only.
1253
+ * @param {string} documentId - The document ID
1254
+ */
1255
+ releaseLock: (documentId) =>
1256
+ this._request("DELETE", `/api/v1/admin/locks/${documentId}`),
1257
+ /**
1258
+ * Live login and invite rate-limit buckets: the address, the account
1259
+ * where there is one, failures inside the window, the limit, and whether
1260
+ * it is currently blocking. Admin only.
1261
+ * @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
1262
+ */
1263
+ rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
1264
+ /**
1265
+ * Forget recorded rate-limit failures. With `ip`, clears that address,
1266
+ * narrowed to one account with `userId`. With neither, clears every
1267
+ * bucket. Only ever unblocks. Admin only.
1268
+ * @param {object} [opts]
1269
+ * @param {string} [opts.ip] - Clear only this address
1270
+ * @param {string} [opts.userId] - Narrow to one account on that address
1271
+ */
1272
+ clearRateLimits: ({ ip, userId } = {}) =>
1273
+ this._request("DELETE", "/api/v1/admin/rate-limits", {
1274
+ queryParams: { ip, "user-id": userId },
1275
+ }),
1276
+ /**
1277
+ * The tail of the configured log file. Resolves with an `error` string
1278
+ * instead of lines when no log file is configured or it does not exist
1279
+ * yet. Admin only.
1280
+ * @param {object} [opts]
1281
+ * @param {number} [opts.lines] - How many lines (default 200, max 2000)
1282
+ * @returns {Promise<{file: (string|null), lines: string[], error?: string}>}
1283
+ */
1284
+ logs: ({ lines } = {}) =>
1285
+ this._request("GET", "/api/v1/admin/logs", {
1286
+ queryParams: { lines },
1287
+ }),
1288
+ /**
1289
+ * Private user-data entries across every account, each carrying its
1290
+ * `userId`. `/users/:userId/data` has always been owner-or-admin; this
1291
+ * is the same reach across accounts at once. Transparently follows
1292
+ * pagination cursors and returns the full flat array. Admin only.
1293
+ *
1294
+ * Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
1295
+ * GLOB over the whole key (`*` any run, `?` one character) — the way to
1296
+ * ask for a key convention identified by a segment in the middle, e.g.
1297
+ * `igt:assistant:*:meta:*`. Values come only with `includeValues`, and
1298
+ * are recased like any other body (see `userData.put`).
1299
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use userDataPage() for a single page in a batch.
1300
+ * @param {object} [opts]
1301
+ * @param {string} [opts.prefix] - Only keys starting with this
1302
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
1303
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
1304
+ * @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
1305
+ */
1306
+ userData: ({ prefix, pattern, includeValues } = {}) =>
1307
+ listAll(this, "/api/v1/admin/user-data", {
1308
+ query: {
1309
+ prefix,
1310
+ pattern,
1311
+ "include-values": includeValues,
1312
+ },
1313
+ }),
1314
+ /**
1315
+ * One page of private user-data entries across accounts, ordered by
1316
+ * (user, key). Admin only.
1317
+ * @param {object} [opts]
1318
+ * @param {string} [opts.prefix] - Only keys starting with this
1319
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
1320
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
1321
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1322
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1323
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1324
+ */
1325
+ userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
1326
+ listPage(this, "/api/v1/admin/user-data", {
1327
+ limit,
1328
+ cursor,
1329
+ query: {
1330
+ prefix,
1331
+ pattern,
1332
+ "include-values": includeValues,
1333
+ },
1334
+ }),
1335
+ };
1336
+
1337
+ this.audit = {
1338
+ /**
1339
+ * The audit log across every project, oldest first. Same fold, window
1340
+ * and op-type filter as the per-project read, with the entity scope
1341
+ * dropped. Admin only. Transparently follows pagination cursors and
1342
+ * returns the full flat array.
1343
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
1344
+ * @param {object} [opts]
1345
+ * @param {string} [opts.startTime] - Only operations at or after this instant
1346
+ * @param {string} [opts.endTime] - Only operations at or before this instant
1347
+ * @param {string[]|string} [opts.opTypes] - Only these op types (e.g.
1348
+ * `['span-layer/create']`). An entry appears when one of its operations
1349
+ * matches, carrying only the ones that did.
1350
+ */
1351
+ list: ({ startTime, endTime, opTypes } = {}) =>
1352
+ listAll(this, "/api/v1/audit", {
1353
+ query: {
1354
+ "start-time": startTime,
1355
+ "end-time": endTime,
1356
+ "op-types": opTypesParam(opTypes),
1357
+ },
1358
+ }),
1359
+ /**
1360
+ * Fetch a single page of the instance-wide audit log. Admin only.
1361
+ * @param {object} [opts]
1362
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
1363
+ * is what a feed wants. A cursor belongs to the direction that made it.
1364
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1365
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1366
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1367
+ */
1368
+ listPage: ({ startTime, endTime, opTypes, order, limit, cursor } = {}) =>
1369
+ listPage(this, "/api/v1/audit", {
1370
+ limit,
1371
+ cursor,
1372
+ query: {
1373
+ "start-time": startTime,
1374
+ "end-time": endTime,
1375
+ "op-types": opTypesParam(opTypes),
1376
+ order,
1377
+ },
1378
+ }),
1379
+ /**
1380
+ * Async-iterate the instance-wide audit log page by page. Admin only.
1381
+ * 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.
1382
+ * @returns {AsyncGenerator<Array>}
1383
+ */
1384
+ iterPages: ({ startTime, endTime, opTypes, pageSize } = {}) =>
1385
+ iterPages(this, "/api/v1/audit", {
1386
+ pageSize,
1387
+ query: {
1388
+ "start-time": startTime,
1389
+ "end-time": endTime,
1390
+ "op-types": opTypesParam(opTypes),
1391
+ },
1392
+ }),
1393
+ /**
1394
+ * Per-user activity counts. `changes` is the number of logical actions,
1395
+ * folded the way the audit feed folds them, so one "Confirm word
1396
+ * analysis" counts once however many rows it wrote; `operations` is the
1397
+ * unfolded row count. Only users who did something appear — subtract
1398
+ * from the roster you already hold to find the ones who did not.
1399
+ *
1400
+ * With `projectId`, scoped to that project and open to its maintainers.
1401
+ * Without one, instance-wide and admin only.
1402
+ * @param {object} [opts]
1403
+ * @param {string} [opts.projectId] - Scope to one project
1404
+ * @param {string} [opts.startTime] - Only count at or after this instant
1405
+ * @param {string} [opts.endTime] - Only count at or before this instant
1406
+ * @param {boolean} [opts.daily] - Also return `byDay`, a list of
1407
+ * `{date, changes}` per user, oldest first, at the cost of a second
1408
+ * grouped scan
1409
+ * @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
1410
+ */
1411
+ tally: async ({ projectId, startTime, endTime, daily } = {}) => {
1412
+ const path = projectId
1413
+ ? `/api/v1/projects/${projectId}/audit/tally`
1414
+ : "/api/v1/audit/tally";
1415
+ const result = await this._request("GET", path, {
1416
+ queryParams: {
1417
+ "start-time": startTime,
1418
+ "end-time": endTime,
1419
+ daily,
1420
+ },
1421
+ });
1422
+ return result.entries;
1423
+ },
851
1424
  };
852
1425
 
853
1426
  this.tokenLayers = {
@@ -857,7 +1430,8 @@ class PlaidClient {
857
1430
  * @param {string} direction - The direction ("up" or "down")
858
1431
  */
859
1432
  shift: (tokenLayerId, direction, auditMessage) =>
860
- this._request('POST', `/api/v1/token-layers/${tokenLayerId}/shift`, { auditMessage,
1433
+ this._request("POST", `/api/v1/token-layers/${tokenLayerId}/shift`, {
1434
+ auditMessage,
861
1435
  body: bodyOf({ direction }),
862
1436
  }),
863
1437
  /**
@@ -867,9 +1441,21 @@ class PlaidClient {
867
1441
  * @param {string} [overlapMode] - Per-layer, immutable token invariant: "any" (default), "non-overlapping", or "partitioning". On partitioning layers, single token create/update/delete are rejected; use bulkCreate plus split/merge/shift.
868
1442
  * @param {string} [parentTokenLayerId] - Optional immutable parent token layer. Tokens in this layer must nest within a parent-layer token; the parent layer must be in the same text layer and be "non-overlapping" or "partitioning" (an "any" parent is rejected). A nested layer may be "any" or "non-overlapping" but not "partitioning" (partitioning is only for root layers), e.g. words (non-overlapping, parent=sentences) within sentences (partitioning).
869
1443
  */
870
- create: (textLayerId, name, overlapMode, parentTokenLayerId, auditMessage) =>
871
- this._request('POST', '/api/v1/token-layers', { auditMessage,
872
- body: bodyOf({ 'text-layer-id': textLayerId, name, 'overlap-mode': overlapMode, 'parent-token-layer-id': parentTokenLayerId }),
1444
+ create: (
1445
+ textLayerId,
1446
+ name,
1447
+ overlapMode,
1448
+ parentTokenLayerId,
1449
+ auditMessage,
1450
+ ) =>
1451
+ this._request("POST", "/api/v1/token-layers", {
1452
+ auditMessage,
1453
+ body: bodyOf({
1454
+ "text-layer-id": textLayerId,
1455
+ name,
1456
+ "overlap-mode": overlapMode,
1457
+ "parent-token-layer-id": parentTokenLayerId,
1458
+ }),
873
1459
  }),
874
1460
  /**
875
1461
  * Set a configuration value for a layer in an editor namespace.
@@ -878,10 +1464,18 @@ class PlaidClient {
878
1464
  * @param {string} configKey - The config key
879
1465
  * @param {any} configValue - Configuration value to set
880
1466
  */
881
- setConfig: (tokenLayerId, namespace, configKey, configValue, auditMessage) =>
882
- this._request('PUT', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, { auditMessage,
883
- rawBody: configValue, skipResponseTransform: true,
884
- }),
1467
+ setConfig: (
1468
+ tokenLayerId,
1469
+ namespace,
1470
+ configKey,
1471
+ configValue,
1472
+ auditMessage,
1473
+ ) =>
1474
+ this._request(
1475
+ "PUT",
1476
+ `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
1477
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
1478
+ ),
885
1479
  /**
886
1480
  * Remove a configuration value for a layer.
887
1481
  * @param {string} tokenLayerId - The token layer ID
@@ -889,31 +1483,36 @@ class PlaidClient {
889
1483
  * @param {string} configKey - The config key
890
1484
  */
891
1485
  deleteConfig: (tokenLayerId, namespace, configKey, auditMessage) =>
892
- this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, { auditMessage,
893
- skipResponseTransform: true,
894
- }),
1486
+ this._request(
1487
+ "DELETE",
1488
+ `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
1489
+ { auditMessage, skipResponseTransform: true },
1490
+ ),
895
1491
  /**
896
1492
  * Get a token layer by ID.
897
1493
  * @param {string} tokenLayerId - The token layer ID
898
1494
  * @param {string} [asOf] - Temporal query timestamp
899
1495
  */
900
1496
  get: (tokenLayerId, asOf) =>
901
- this._request('GET', `/api/v1/token-layers/${tokenLayerId}`, {
902
- queryParams: { 'as-of': asOf },
1497
+ this._request("GET", `/api/v1/token-layers/${tokenLayerId}`, {
1498
+ queryParams: { "as-of": asOf },
903
1499
  }),
904
1500
  /**
905
1501
  * Delete a token layer.
906
1502
  * @param {string} tokenLayerId - The token layer ID
907
1503
  */
908
1504
  delete: (tokenLayerId, auditMessage) =>
909
- this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}`, { auditMessage }),
1505
+ this._request("DELETE", `/api/v1/token-layers/${tokenLayerId}`, {
1506
+ auditMessage,
1507
+ }),
910
1508
  /**
911
1509
  * Update a token layer's name.
912
1510
  * @param {string} tokenLayerId - The token layer ID
913
1511
  * @param {string} name - The name
914
1512
  */
915
1513
  update: (tokenLayerId, name, auditMessage) =>
916
- this._request('PATCH', `/api/v1/token-layers/${tokenLayerId}`, { auditMessage,
1514
+ this._request("PATCH", `/api/v1/token-layers/${tokenLayerId}`, {
1515
+ auditMessage,
917
1516
  body: bodyOf({ name }),
918
1517
  }),
919
1518
  };
@@ -925,29 +1524,33 @@ class PlaidClient {
925
1524
  * @param {string} [asOf] - Temporal query timestamp
926
1525
  */
927
1526
  checkLock: (documentId, asOf) =>
928
- this._request('GET', `/api/v1/documents/${documentId}/lock`, {
929
- queryParams: { 'as-of': asOf },
1527
+ this._request("GET", `/api/v1/documents/${documentId}/lock`, {
1528
+ queryParams: { "as-of": asOf },
930
1529
  }),
931
1530
  /**
932
1531
  * Acquire or refresh a document lock
933
1532
  * @param {string} documentId - The document ID
934
1533
  */
935
1534
  acquireLock: (documentId, auditMessage) =>
936
- this._request('POST', `/api/v1/documents/${documentId}/lock`, { auditMessage }),
1535
+ this._request("POST", `/api/v1/documents/${documentId}/lock`, {
1536
+ auditMessage,
1537
+ }),
937
1538
  /**
938
1539
  * Release a document lock
939
1540
  * @param {string} documentId - The document ID
940
1541
  */
941
1542
  releaseLock: (documentId, auditMessage) =>
942
- this._request('DELETE', `/api/v1/documents/${documentId}/lock`, { auditMessage }),
1543
+ this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
1544
+ auditMessage,
1545
+ }),
943
1546
  /**
944
- * Get media file for a document
1547
+ * Get the media file for a document. Media is not versioned, so there is
1548
+ * no as-of form: the route refuses the parameter. Prefer the document's
1549
+ * own `mediaUrl`, which carries the file's version for caching.
945
1550
  * @param {string} documentId - The document ID
946
- * @param {string} [asOf] - Temporal query timestamp
947
1551
  */
948
- getMedia: (documentId, asOf) =>
949
- this._request('GET', `/api/v1/documents/${documentId}/media`, {
950
- queryParams: { 'as-of': asOf },
1552
+ getMedia: (documentId) =>
1553
+ this._request("GET", `/api/v1/documents/${documentId}/media`, {
951
1554
  noBatch: true,
952
1555
  binaryResponse: true,
953
1556
  }),
@@ -955,12 +1558,18 @@ class PlaidClient {
955
1558
  * Upload a media file for a document. Uses Apache Tika for content validation.
956
1559
  * @param {string} documentId - The document ID
957
1560
  * @param {File} file - The file to upload
1561
+ * @param {string} [auditMessage] - Custom audit-log message for this write
1562
+ * @param {object} [options] - `{ onProgress }`: called with `{ loaded, total }` (bytes) as the file goes up. In a browser the upload then travels by XMLHttpRequest, and the request timeout only fires when the upload stalls, not on total time.
958
1563
  */
959
- uploadMedia: (documentId, file, auditMessage) => {
1564
+ uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
960
1565
  const fd = new FormData();
961
- fd.append('file', file);
962
- return this._request('PUT', `/api/v1/documents/${documentId}/media`, { auditMessage,
963
- body: fd, formData: true, noBatch: true,
1566
+ fd.append("file", file);
1567
+ return this._request("PUT", `/api/v1/documents/${documentId}/media`, {
1568
+ auditMessage,
1569
+ body: fd,
1570
+ formData: true,
1571
+ noBatch: true,
1572
+ onUploadProgress: onProgress,
964
1573
  });
965
1574
  },
966
1575
  /**
@@ -968,7 +1577,8 @@ class PlaidClient {
968
1577
  * @param {string} documentId - The document ID
969
1578
  */
970
1579
  deleteMedia: (documentId, auditMessage) =>
971
- this._request('DELETE', `/api/v1/documents/${documentId}/media`, { auditMessage,
1580
+ this._request("DELETE", `/api/v1/documents/${documentId}/media`, {
1581
+ auditMessage,
972
1582
  noBatch: true,
973
1583
  }),
974
1584
  /**
@@ -977,15 +1587,18 @@ class PlaidClient {
977
1587
  * @param {any} body - The request body
978
1588
  */
979
1589
  setMetadata: (documentId, body, auditMessage) =>
980
- this._request('PUT', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
981
- rawBody: body, skipResponseTransform: true,
1590
+ this._request("PUT", `/api/v1/documents/${documentId}/metadata`, {
1591
+ auditMessage,
1592
+ rawBody: body,
1593
+ skipResponseTransform: true,
982
1594
  }),
983
1595
  /**
984
1596
  * Remove all metadata from a document.
985
1597
  * @param {string} documentId - The document ID
986
1598
  */
987
1599
  deleteMetadata: (documentId, auditMessage) =>
988
- this._request('DELETE', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
1600
+ this._request("DELETE", `/api/v1/documents/${documentId}/metadata`, {
1601
+ auditMessage,
989
1602
  skipResponseTransform: true,
990
1603
  }),
991
1604
  /**
@@ -994,8 +1607,10 @@ class PlaidClient {
994
1607
  * @param {any} body - The metadata patch
995
1608
  */
996
1609
  patchMetadata: (documentId, body, auditMessage) =>
997
- this._request('PATCH', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
998
- rawBody: body, skipResponseTransform: true,
1610
+ this._request("PATCH", `/api/v1/documents/${documentId}/metadata`, {
1611
+ auditMessage,
1612
+ rawBody: body,
1613
+ skipResponseTransform: true,
999
1614
  }),
1000
1615
  /**
1001
1616
  * Get audit log for a document. Transparently follows pagination cursors
@@ -1005,34 +1620,106 @@ class PlaidClient {
1005
1620
  * @param {string} [startTime] - Start of time range
1006
1621
  * @param {string} [endTime] - End of time range
1007
1622
  * @param {string} [asOf] - Temporal query timestamp
1623
+ * @param {string[]|string} [opTypes] - Only return operations of these
1624
+ * types, spelled as in an entry's `op/type` (e.g.
1625
+ * `['span-layer/create', 'span-layer/delete']`). An entry appears when
1626
+ * one of its operations matches, carrying only the ones that did.
1008
1627
  */
1009
- audit: (documentId, startTime, endTime, asOf) =>
1628
+ audit: (documentId, startTime, endTime, asOf, opTypes) =>
1010
1629
  listAll(this, `/api/v1/documents/${documentId}/audit`, {
1011
- query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
1630
+ query: {
1631
+ "start-time": startTime,
1632
+ "end-time": endTime,
1633
+ "as-of": asOf,
1634
+ "op-types": opTypesParam(opTypes),
1635
+ },
1636
+ }),
1637
+ /**
1638
+ * One page of the same log, newest-first with `order: "desc"`. Use this
1639
+ * rather than audit() wherever the caller wants the recent end of a log
1640
+ * that may be long: audit() walks every page before it resolves.
1641
+ * @param {object} [opts]
1642
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1643
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1644
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1645
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1646
+ */
1647
+ auditPage: (
1648
+ documentId,
1649
+ { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
1650
+ ) =>
1651
+ listPage(this, `/api/v1/documents/${documentId}/audit`, {
1652
+ limit,
1653
+ cursor,
1654
+ query: {
1655
+ "start-time": startTime,
1656
+ "end-time": endTime,
1657
+ "as-of": asOf,
1658
+ "op-types": opTypesParam(opTypes),
1659
+ order,
1660
+ },
1661
+ }),
1662
+ /**
1663
+ * Restore a document to its state at an earlier time, as one
1664
+ * operation: what was deleted since then comes back under its original
1665
+ * id, what was added since is removed, and what changed is set back,
1666
+ * across every layer. A layer deleted since then, or a vocabulary entry
1667
+ * that no longer exists, is skipped and reported under `skipped`.
1668
+ * Resolves to a summary of the changes. Maintainers only.
1669
+ * @param {string} documentId - The document ID
1670
+ * @param {string} asOf - The moment to go back to (ISO-8601 instant),
1671
+ * typically a history entry's `endTime`
1672
+ * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
1673
+ * is written and the summary says what would change
1674
+ * @param {string} [auditMessage] - Custom audit message for this operation
1675
+ */
1676
+ restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
1677
+ this._request("POST", `/api/v1/documents/${documentId}/restore`, {
1678
+ queryParams: {
1679
+ "as-of": asOf,
1680
+ "dry-run": dryRun ? "true" : undefined,
1681
+ },
1682
+ auditMessage,
1012
1683
  }),
1013
1684
  /**
1014
1685
  * Get a document. Set `includeBody` to true to include all data.
1686
+ *
1687
+ * `layers` narrows a body read to the layers you name (ids of any kind:
1688
+ * text, token, span, or relation). A layer comes back when it is named
1689
+ * or is an ancestor of a named layer, and carries its own
1690
+ * texts/tokens/spans/relations/vocabs only when it is itself named — so
1691
+ * name the text layer too if you also want the text body. An id that is
1692
+ * not a layer of this document's project is an error, not a quietly
1693
+ * smaller response. Requires `includeBody`.
1015
1694
  * @param {string} documentId - The document ID
1016
1695
  * @param {boolean} [includeBody] - Include document body data
1017
1696
  * @param {string} [asOf] - Temporal query timestamp
1697
+ * @param {string[]|string} [layers] - Layer ids to restrict a body read to
1018
1698
  */
1019
- get: (documentId, includeBody, asOf) =>
1020
- this._request('GET', `/api/v1/documents/${documentId}`, {
1021
- queryParams: { 'include-body': includeBody, 'as-of': asOf },
1699
+ get: (documentId, includeBody, asOf, layers) =>
1700
+ this._request("GET", `/api/v1/documents/${documentId}`, {
1701
+ queryParams: {
1702
+ "include-body": includeBody,
1703
+ "as-of": asOf,
1704
+ layers: layersParam(layers),
1705
+ },
1022
1706
  }),
1023
1707
  /**
1024
1708
  * Delete a document and all data contained.
1025
1709
  * @param {string} documentId - The document ID
1026
1710
  */
1027
1711
  delete: (documentId, auditMessage) =>
1028
- this._request('DELETE', `/api/v1/documents/${documentId}`, { auditMessage }),
1712
+ this._request("DELETE", `/api/v1/documents/${documentId}`, {
1713
+ auditMessage,
1714
+ }),
1029
1715
  /**
1030
1716
  * Update a document's name.
1031
1717
  * @param {string} documentId - The document ID
1032
1718
  * @param {string} name - The name
1033
1719
  */
1034
1720
  update: (documentId, name, auditMessage) =>
1035
- this._request('PATCH', `/api/v1/documents/${documentId}`, { auditMessage,
1721
+ this._request("PATCH", `/api/v1/documents/${documentId}`, {
1722
+ auditMessage,
1036
1723
  body: bodyOf({ name }),
1037
1724
  }),
1038
1725
  /**
@@ -1042,8 +1729,28 @@ class PlaidClient {
1042
1729
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1043
1730
  */
1044
1731
  create: (projectId, name, metadata, auditMessage) =>
1045
- this._request('POST', '/api/v1/documents', { auditMessage,
1046
- body: bodyOf({ 'project-id': projectId, name, metadata }),
1732
+ this._request("POST", "/api/v1/documents", {
1733
+ auditMessage,
1734
+ body: bodyOf({ "project-id": projectId, name, metadata }),
1735
+ }),
1736
+ /**
1737
+ * Copy a document and everything in it into a new document of the same
1738
+ * project, as one operation. The copy shares the source's layers and
1739
+ * the vocabulary entries its links name, and holds the source's texts,
1740
+ * tokens, spans, relations and vocab links under fresh ids, with their
1741
+ * metadata. Comments do not travel. The media file does, unless
1742
+ * `includeMedia` is false. Resolves to `{ id }`, plus `mediaError` when
1743
+ * the source had media the copy could not take with it.
1744
+ * @param {string} documentId - The document to copy
1745
+ * @param {string} name - The new document's name
1746
+ * @param {object} [options] - `{ includeMedia }`: false leaves the media
1747
+ * file behind
1748
+ * @param {string} [auditMessage] - Custom audit message for this operation
1749
+ */
1750
+ copy: (documentId, name, { includeMedia } = {}, auditMessage) =>
1751
+ this._request("POST", `/api/v1/documents/${documentId}/copy`, {
1752
+ auditMessage,
1753
+ body: bodyOf({ name, "include-media": includeMedia }),
1047
1754
  }),
1048
1755
  };
1049
1756
 
@@ -1054,28 +1761,36 @@ class PlaidClient {
1054
1761
  * @param {string} userId - The user ID
1055
1762
  */
1056
1763
  addWriter: (id, userId, auditMessage) =>
1057
- this._request('POST', `/api/v1/projects/${id}/writers/${userId}`, { auditMessage }),
1764
+ this._request("POST", `/api/v1/projects/${id}/writers/${userId}`, {
1765
+ auditMessage,
1766
+ }),
1058
1767
  /**
1059
1768
  * Remove a user's writer privileges for this project.
1060
1769
  * @param {string} id - The resource ID
1061
1770
  * @param {string} userId - The user ID
1062
1771
  */
1063
1772
  removeWriter: (id, userId, auditMessage) =>
1064
- this._request('DELETE', `/api/v1/projects/${id}/writers/${userId}`, { auditMessage }),
1773
+ this._request("DELETE", `/api/v1/projects/${id}/writers/${userId}`, {
1774
+ auditMessage,
1775
+ }),
1065
1776
  /**
1066
1777
  * Set a user's access level to read-only for this project.
1067
1778
  * @param {string} id - The resource ID
1068
1779
  * @param {string} userId - The user ID
1069
1780
  */
1070
1781
  addReader: (id, userId, auditMessage) =>
1071
- this._request('POST', `/api/v1/projects/${id}/readers/${userId}`, { auditMessage }),
1782
+ this._request("POST", `/api/v1/projects/${id}/readers/${userId}`, {
1783
+ auditMessage,
1784
+ }),
1072
1785
  /**
1073
1786
  * Remove a user's reader privileges for this project.
1074
1787
  * @param {string} id - The resource ID
1075
1788
  * @param {string} userId - The user ID
1076
1789
  */
1077
1790
  removeReader: (id, userId, auditMessage) =>
1078
- this._request('DELETE', `/api/v1/projects/${id}/readers/${userId}`, { auditMessage }),
1791
+ this._request("DELETE", `/api/v1/projects/${id}/readers/${userId}`, {
1792
+ auditMessage,
1793
+ }),
1079
1794
  /**
1080
1795
  * Set a configuration value for a project in an editor namespace.
1081
1796
  * @param {string} id - The resource ID
@@ -1084,9 +1799,11 @@ class PlaidClient {
1084
1799
  * @param {any} configValue - Configuration value to set
1085
1800
  */
1086
1801
  setConfig: (id, namespace, configKey, configValue, auditMessage) =>
1087
- this._request('PUT', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, { auditMessage,
1088
- rawBody: configValue, skipResponseTransform: true,
1089
- }),
1802
+ this._request(
1803
+ "PUT",
1804
+ `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
1805
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
1806
+ ),
1090
1807
  /**
1091
1808
  * Remove a configuration value for a project.
1092
1809
  * @param {string} id - The resource ID
@@ -1094,23 +1811,31 @@ class PlaidClient {
1094
1811
  * @param {string} configKey - The config key
1095
1812
  */
1096
1813
  deleteConfig: (id, namespace, configKey, auditMessage) =>
1097
- this._request('DELETE', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, { auditMessage,
1098
- skipResponseTransform: true,
1099
- }),
1814
+ this._request(
1815
+ "DELETE",
1816
+ `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
1817
+ { auditMessage, skipResponseTransform: true },
1818
+ ),
1100
1819
  /**
1101
1820
  * Assign a user as a maintainer for this project.
1102
1821
  * @param {string} id - The resource ID
1103
1822
  * @param {string} userId - The user ID
1104
1823
  */
1105
1824
  addMaintainer: (id, userId, auditMessage) =>
1106
- this._request('POST', `/api/v1/projects/${id}/maintainers/${userId}`, { auditMessage }),
1825
+ this._request("POST", `/api/v1/projects/${id}/maintainers/${userId}`, {
1826
+ auditMessage,
1827
+ }),
1107
1828
  /**
1108
1829
  * Remove a user's maintainer privileges for this project.
1109
1830
  * @param {string} id - The resource ID
1110
1831
  * @param {string} userId - The user ID
1111
1832
  */
1112
1833
  removeMaintainer: (id, userId, auditMessage) =>
1113
- this._request('DELETE', `/api/v1/projects/${id}/maintainers/${userId}`, { auditMessage }),
1834
+ this._request(
1835
+ "DELETE",
1836
+ `/api/v1/projects/${id}/maintainers/${userId}`,
1837
+ { auditMessage },
1838
+ ),
1114
1839
  /**
1115
1840
  * Get audit log for a project. Transparently follows pagination cursors
1116
1841
  * and returns the full flat array.
@@ -1119,10 +1844,58 @@ class PlaidClient {
1119
1844
  * @param {string} [startTime] - Start of time range
1120
1845
  * @param {string} [endTime] - End of time range
1121
1846
  * @param {string} [asOf] - Temporal query timestamp
1847
+ * @param {string[]|string} [opTypes] - Only return operations of these
1848
+ * types, spelled as in an entry's `op/type` (e.g.
1849
+ * `['span-layer/create', 'span-layer/delete']`). An entry appears when
1850
+ * one of its operations matches, carrying only the ones that did.
1122
1851
  */
1123
- audit: (projectId, startTime, endTime, asOf) =>
1852
+ audit: (projectId, startTime, endTime, asOf, opTypes) =>
1124
1853
  listAll(this, `/api/v1/projects/${projectId}/audit`, {
1125
- query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
1854
+ query: {
1855
+ "start-time": startTime,
1856
+ "end-time": endTime,
1857
+ "as-of": asOf,
1858
+ "op-types": opTypesParam(opTypes),
1859
+ },
1860
+ }),
1861
+ /**
1862
+ * One page of the same log, newest-first with `order: "desc"`. Use this
1863
+ * rather than audit() wherever the caller wants the recent end of a log
1864
+ * that may be long: audit() walks every page before it resolves.
1865
+ * @param {object} [opts]
1866
+ * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1867
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1868
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
1869
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1870
+ */
1871
+ auditPage: (
1872
+ projectId,
1873
+ { startTime, endTime, asOf, opTypes, order, limit, cursor } = {},
1874
+ ) =>
1875
+ listPage(this, `/api/v1/projects/${projectId}/audit`, {
1876
+ limit,
1877
+ cursor,
1878
+ query: {
1879
+ "start-time": startTime,
1880
+ "end-time": endTime,
1881
+ "as-of": asOf,
1882
+ "op-types": opTypesParam(opTypes),
1883
+ order,
1884
+ },
1885
+ }),
1886
+ /**
1887
+ * When the calling user last wrote to each document in a project, as a
1888
+ * `{documentId: timestamp}` map. Documents they have never written to
1889
+ * are absent. One request covers a whole document list.
1890
+ *
1891
+ * The response is NOT key-transformed: its keys are document ids, and
1892
+ * camelCasing would mangle the hyphens in a UUID.
1893
+ * @param {string} projectId - The project ID
1894
+ * @returns {Promise<Object<string, string>>}
1895
+ */
1896
+ myLastEdits: (projectId) =>
1897
+ this._request("GET", `/api/v1/projects/${projectId}/audit/last-edits`, {
1898
+ skipResponseTransform: true,
1126
1899
  }),
1127
1900
  /**
1128
1901
  * Link a vocabulary to a project.
@@ -1130,14 +1903,18 @@ class PlaidClient {
1130
1903
  * @param {string} vocabId - The vocab layer ID
1131
1904
  */
1132
1905
  linkVocab: (id, vocabId, auditMessage) =>
1133
- this._request('POST', `/api/v1/projects/${id}/vocabs/${vocabId}`, { auditMessage }),
1906
+ this._request("POST", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
1907
+ auditMessage,
1908
+ }),
1134
1909
  /**
1135
1910
  * Unlink a vocabulary from a project.
1136
1911
  * @param {string} id - The resource ID
1137
1912
  * @param {string} vocabId - The vocab layer ID
1138
1913
  */
1139
1914
  unlinkVocab: (id, vocabId, auditMessage) =>
1140
- this._request('DELETE', `/api/v1/projects/${id}/vocabs/${vocabId}`, { auditMessage }),
1915
+ this._request("DELETE", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
1916
+ auditMessage,
1917
+ }),
1141
1918
  /**
1142
1919
  * Get a project by ID. To fetch the project's documents, use
1143
1920
  * listDocuments(id) — the include-documents flag has been removed.
@@ -1145,8 +1922,8 @@ class PlaidClient {
1145
1922
  * @param {string} [asOf] - Temporal query timestamp
1146
1923
  */
1147
1924
  get: (id, asOf) =>
1148
- this._request('GET', `/api/v1/projects/${id}`, {
1149
- queryParams: { 'as-of': asOf },
1925
+ this._request("GET", `/api/v1/projects/${id}`, {
1926
+ queryParams: { "as-of": asOf },
1150
1927
  }),
1151
1928
  /**
1152
1929
  * List all documents in a project. Transparently follows pagination
@@ -1157,8 +1934,7 @@ class PlaidClient {
1157
1934
  * server rejects `?as-of=` on the documents-list route with a 400.
1158
1935
  * @param {string} id - The project ID
1159
1936
  */
1160
- listDocuments: (id) =>
1161
- listAll(this, `/api/v1/projects/${id}/documents`),
1937
+ listDocuments: (id) => listAll(this, `/api/v1/projects/${id}/documents`),
1162
1938
  /**
1163
1939
  * Fetch a single page of a project's documents.
1164
1940
  *
@@ -1198,14 +1974,18 @@ class PlaidClient {
1198
1974
  * @param {number} [options.timeout=0] - Per-request timeout in ms (0/null disables)
1199
1975
  */
1200
1976
  delete: (id, auditMessage, { timeout = 0 } = {}) =>
1201
- this._request('DELETE', `/api/v1/projects/${id}`, { auditMessage, timeout }),
1977
+ this._request("DELETE", `/api/v1/projects/${id}`, {
1978
+ auditMessage,
1979
+ timeout,
1980
+ }),
1202
1981
  /**
1203
1982
  * Update a project's name.
1204
1983
  * @param {string} id - The resource ID
1205
1984
  * @param {string} name - The name
1206
1985
  */
1207
1986
  update: (id, name, auditMessage) =>
1208
- this._request('PATCH', `/api/v1/projects/${id}`, { auditMessage,
1987
+ this._request("PATCH", `/api/v1/projects/${id}`, {
1988
+ auditMessage,
1209
1989
  body: bodyOf({ name }),
1210
1990
  }),
1211
1991
  /**
@@ -1215,7 +1995,7 @@ class PlaidClient {
1215
1995
  * @param {string} [asOf] - Temporal query timestamp
1216
1996
  */
1217
1997
  list: (asOf) =>
1218
- listAll(this, '/api/v1/projects', { query: { 'as-of': asOf } }),
1998
+ listAll(this, "/api/v1/projects", { query: { "as-of": asOf } }),
1219
1999
  /**
1220
2000
  * Fetch a single page of projects.
1221
2001
  * @param {object} [opts]
@@ -1225,7 +2005,11 @@ class PlaidClient {
1225
2005
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1226
2006
  */
1227
2007
  listPage: ({ limit, cursor, asOf } = {}) =>
1228
- listPage(this, '/api/v1/projects', { limit, cursor, query: { 'as-of': asOf } }),
2008
+ listPage(this, "/api/v1/projects", {
2009
+ limit,
2010
+ cursor,
2011
+ query: { "as-of": asOf },
2012
+ }),
1229
2013
  /**
1230
2014
  * Async-iterate projects page by page; yields each page's entries array.
1231
2015
  * @param {object} [opts]
@@ -1235,13 +2019,17 @@ class PlaidClient {
1235
2019
  * @returns {AsyncGenerator<Array>}
1236
2020
  */
1237
2021
  iterPages: ({ pageSize, asOf } = {}) =>
1238
- iterPages(this, '/api/v1/projects', { pageSize, query: { 'as-of': asOf } }),
2022
+ iterPages(this, "/api/v1/projects", {
2023
+ pageSize,
2024
+ query: { "as-of": asOf },
2025
+ }),
1239
2026
  /**
1240
2027
  * Create a new project. Note: this also registers the user as a maintainer.
1241
2028
  * @param {string} name - The name
1242
2029
  */
1243
2030
  create: (name, auditMessage) =>
1244
- this._request('POST', '/api/v1/projects', { auditMessage,
2031
+ this._request("POST", "/api/v1/projects", {
2032
+ auditMessage,
1245
2033
  body: bodyOf({ name }),
1246
2034
  }),
1247
2035
  };
@@ -1254,10 +2042,18 @@ class PlaidClient {
1254
2042
  * @param {string} configKey - The config key
1255
2043
  * @param {any} configValue - Configuration value to set
1256
2044
  */
1257
- setConfig: (textLayerId, namespace, configKey, configValue, auditMessage) =>
1258
- this._request('PUT', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1259
- rawBody: configValue, skipResponseTransform: true,
1260
- }),
2045
+ setConfig: (
2046
+ textLayerId,
2047
+ namespace,
2048
+ configKey,
2049
+ configValue,
2050
+ auditMessage,
2051
+ ) =>
2052
+ this._request(
2053
+ "PUT",
2054
+ `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
2055
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
2056
+ ),
1261
2057
  /**
1262
2058
  * Remove a configuration value for a layer.
1263
2059
  * @param {string} textLayerId - The text layer ID
@@ -1265,31 +2061,36 @@ class PlaidClient {
1265
2061
  * @param {string} configKey - The config key
1266
2062
  */
1267
2063
  deleteConfig: (textLayerId, namespace, configKey, auditMessage) =>
1268
- this._request('DELETE', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1269
- skipResponseTransform: true,
1270
- }),
2064
+ this._request(
2065
+ "DELETE",
2066
+ `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
2067
+ { auditMessage, skipResponseTransform: true },
2068
+ ),
1271
2069
  /**
1272
2070
  * Get a text layer by ID.
1273
2071
  * @param {string} textLayerId - The text layer ID
1274
2072
  * @param {string} [asOf] - Temporal query timestamp
1275
2073
  */
1276
2074
  get: (textLayerId, asOf) =>
1277
- this._request('GET', `/api/v1/text-layers/${textLayerId}`, {
1278
- queryParams: { 'as-of': asOf },
2075
+ this._request("GET", `/api/v1/text-layers/${textLayerId}`, {
2076
+ queryParams: { "as-of": asOf },
1279
2077
  }),
1280
2078
  /**
1281
2079
  * Delete a text layer.
1282
2080
  * @param {string} textLayerId - The text layer ID
1283
2081
  */
1284
2082
  delete: (textLayerId, auditMessage) =>
1285
- this._request('DELETE', `/api/v1/text-layers/${textLayerId}`, { auditMessage }),
2083
+ this._request("DELETE", `/api/v1/text-layers/${textLayerId}`, {
2084
+ auditMessage,
2085
+ }),
1286
2086
  /**
1287
2087
  * Update a text layer's name.
1288
2088
  * @param {string} textLayerId - The text layer ID
1289
2089
  * @param {string} name - The name
1290
2090
  */
1291
2091
  update: (textLayerId, name, auditMessage) =>
1292
- this._request('PATCH', `/api/v1/text-layers/${textLayerId}`, { auditMessage,
2092
+ this._request("PATCH", `/api/v1/text-layers/${textLayerId}`, {
2093
+ auditMessage,
1293
2094
  body: bodyOf({ name }),
1294
2095
  }),
1295
2096
  /**
@@ -1298,7 +2099,8 @@ class PlaidClient {
1298
2099
  * @param {string} direction - The direction ("up" or "down")
1299
2100
  */
1300
2101
  shift: (textLayerId, direction, auditMessage) =>
1301
- this._request('POST', `/api/v1/text-layers/${textLayerId}/shift`, { auditMessage,
2102
+ this._request("POST", `/api/v1/text-layers/${textLayerId}/shift`, {
2103
+ auditMessage,
1302
2104
  body: bodyOf({ direction }),
1303
2105
  }),
1304
2106
  /**
@@ -1307,8 +2109,9 @@ class PlaidClient {
1307
2109
  * @param {string} name - The name
1308
2110
  */
1309
2111
  create: (projectId, name, auditMessage) =>
1310
- this._request('POST', '/api/v1/text-layers', { auditMessage,
1311
- body: bodyOf({ 'project-id': projectId, name }),
2112
+ this._request("POST", "/api/v1/text-layers", {
2113
+ auditMessage,
2114
+ body: bodyOf({ "project-id": projectId, name }),
1312
2115
  }),
1313
2116
  };
1314
2117
 
@@ -1319,15 +2122,18 @@ class PlaidClient {
1319
2122
  * @param {any} body - The request body
1320
2123
  */
1321
2124
  setMetadata: (id, body, auditMessage) =>
1322
- this._request('PUT', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1323
- rawBody: body, skipResponseTransform: true,
2125
+ this._request("PUT", `/api/v1/vocab-items/${id}/metadata`, {
2126
+ auditMessage,
2127
+ rawBody: body,
2128
+ skipResponseTransform: true,
1324
2129
  }),
1325
2130
  /**
1326
2131
  * Remove all metadata from a vocab item.
1327
2132
  * @param {string} id - The resource ID
1328
2133
  */
1329
2134
  deleteMetadata: (id, auditMessage) =>
1330
- this._request('DELETE', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
2135
+ this._request("DELETE", `/api/v1/vocab-items/${id}/metadata`, {
2136
+ auditMessage,
1331
2137
  skipResponseTransform: true,
1332
2138
  }),
1333
2139
  /**
@@ -1336,8 +2142,10 @@ class PlaidClient {
1336
2142
  * @param {any} body - The metadata patch
1337
2143
  */
1338
2144
  patchMetadata: (id, body, auditMessage) =>
1339
- this._request('PATCH', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1340
- rawBody: body, skipResponseTransform: true,
2145
+ this._request("PATCH", `/api/v1/vocab-items/${id}/metadata`, {
2146
+ auditMessage,
2147
+ rawBody: body,
2148
+ skipResponseTransform: true,
1341
2149
  }),
1342
2150
  /**
1343
2151
  * Create a new vocab item
@@ -1346,8 +2154,9 @@ class PlaidClient {
1346
2154
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1347
2155
  */
1348
2156
  create: (vocabLayerId, form, metadata, auditMessage) =>
1349
- this._request('POST', '/api/v1/vocab-items', { auditMessage,
1350
- body: bodyOf({ 'vocab-layer-id': vocabLayerId, form, metadata }),
2157
+ this._request("POST", "/api/v1/vocab-items", {
2158
+ auditMessage,
2159
+ body: bodyOf({ "vocab-layer-id": vocabLayerId, form, metadata }),
1351
2160
  }),
1352
2161
  /**
1353
2162
  * Create multiple vocab items in a single operation. Entries may target
@@ -1355,36 +2164,43 @@ class PlaidClient {
1355
2164
  * @param {Array<{vocabLayerId: string, form: string, metadata?: any}>} body - The vocab items to create
1356
2165
  */
1357
2166
  bulkCreate: (body, auditMessage) =>
1358
- this._request('POST', '/api/v1/vocab-items/bulk', { auditMessage, body }),
2167
+ this._request("POST", "/api/v1/vocab-items/bulk", {
2168
+ auditMessage,
2169
+ body,
2170
+ }),
1359
2171
  /**
1360
2172
  * Delete multiple vocab items in a single operation. Each item's
1361
2173
  * descendant vocab links are deleted too. Provide an array of IDs.
1362
2174
  * @param {string[]} body - The vocab item IDs to delete
1363
2175
  */
1364
2176
  bulkDelete: (body, auditMessage) =>
1365
- this._request('DELETE', '/api/v1/vocab-items/bulk', { auditMessage, body }),
2177
+ this._request("DELETE", "/api/v1/vocab-items/bulk", {
2178
+ auditMessage,
2179
+ body,
2180
+ }),
1366
2181
  /**
1367
2182
  * Get a vocab item by ID
1368
2183
  * @param {string} id - The resource ID
1369
2184
  * @param {string} [asOf] - Temporal query timestamp
1370
2185
  */
1371
2186
  get: (id, asOf) =>
1372
- this._request('GET', `/api/v1/vocab-items/${id}`, {
1373
- queryParams: { 'as-of': asOf },
2187
+ this._request("GET", `/api/v1/vocab-items/${id}`, {
2188
+ queryParams: { "as-of": asOf },
1374
2189
  }),
1375
2190
  /**
1376
2191
  * Delete a vocab item
1377
2192
  * @param {string} id - The resource ID
1378
2193
  */
1379
2194
  delete: (id, auditMessage) =>
1380
- this._request('DELETE', `/api/v1/vocab-items/${id}`, { auditMessage }),
2195
+ this._request("DELETE", `/api/v1/vocab-items/${id}`, { auditMessage }),
1381
2196
  /**
1382
2197
  * Update a vocab item's form
1383
2198
  * @param {string} id - The resource ID
1384
2199
  * @param {string} form - The vocab item form
1385
2200
  */
1386
2201
  update: (id, form, auditMessage) =>
1387
- this._request('PATCH', `/api/v1/vocab-items/${id}`, { auditMessage,
2202
+ this._request("PATCH", `/api/v1/vocab-items/${id}`, {
2203
+ auditMessage,
1388
2204
  body: bodyOf({ form }),
1389
2205
  }),
1390
2206
  };
@@ -1396,17 +2212,20 @@ class PlaidClient {
1396
2212
  * @param {string} direction - The direction ("up" or "down")
1397
2213
  */
1398
2214
  shift: (relationLayerId, direction, auditMessage) =>
1399
- this._request('POST', `/api/v1/relation-layers/${relationLayerId}/shift`, { auditMessage,
1400
- body: bodyOf({ direction }),
1401
- }),
2215
+ this._request(
2216
+ "POST",
2217
+ `/api/v1/relation-layers/${relationLayerId}/shift`,
2218
+ { auditMessage, body: bodyOf({ direction }) },
2219
+ ),
1402
2220
  /**
1403
2221
  * Create a new relation layer.
1404
2222
  * @param {string} spanLayerId - The span layer ID
1405
2223
  * @param {string} name - The name
1406
2224
  */
1407
2225
  create: (spanLayerId, name, auditMessage) =>
1408
- this._request('POST', '/api/v1/relation-layers', { auditMessage,
1409
- body: bodyOf({ 'span-layer-id': spanLayerId, name }),
2226
+ this._request("POST", "/api/v1/relation-layers", {
2227
+ auditMessage,
2228
+ body: bodyOf({ "span-layer-id": spanLayerId, name }),
1410
2229
  }),
1411
2230
  /**
1412
2231
  * Set a configuration value for a layer in an editor namespace.
@@ -1415,10 +2234,18 @@ class PlaidClient {
1415
2234
  * @param {string} configKey - The config key
1416
2235
  * @param {any} configValue - Configuration value to set
1417
2236
  */
1418
- setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage) =>
1419
- this._request('PUT', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1420
- rawBody: configValue, skipResponseTransform: true,
1421
- }),
2237
+ setConfig: (
2238
+ relationLayerId,
2239
+ namespace,
2240
+ configKey,
2241
+ configValue,
2242
+ auditMessage,
2243
+ ) =>
2244
+ this._request(
2245
+ "PUT",
2246
+ `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
2247
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
2248
+ ),
1422
2249
  /**
1423
2250
  * Remove a configuration value for a layer.
1424
2251
  * @param {string} relationLayerId - The relation layer ID
@@ -1426,31 +2253,36 @@ class PlaidClient {
1426
2253
  * @param {string} configKey - The config key
1427
2254
  */
1428
2255
  deleteConfig: (relationLayerId, namespace, configKey, auditMessage) =>
1429
- this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1430
- skipResponseTransform: true,
1431
- }),
2256
+ this._request(
2257
+ "DELETE",
2258
+ `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
2259
+ { auditMessage, skipResponseTransform: true },
2260
+ ),
1432
2261
  /**
1433
2262
  * Get a relation layer by ID.
1434
2263
  * @param {string} relationLayerId - The relation layer ID
1435
2264
  * @param {string} [asOf] - Temporal query timestamp
1436
2265
  */
1437
2266
  get: (relationLayerId, asOf) =>
1438
- this._request('GET', `/api/v1/relation-layers/${relationLayerId}`, {
1439
- queryParams: { 'as-of': asOf },
2267
+ this._request("GET", `/api/v1/relation-layers/${relationLayerId}`, {
2268
+ queryParams: { "as-of": asOf },
1440
2269
  }),
1441
2270
  /**
1442
2271
  * Delete a relation layer.
1443
2272
  * @param {string} relationLayerId - The relation layer ID
1444
2273
  */
1445
2274
  delete: (relationLayerId, auditMessage) =>
1446
- this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}`, { auditMessage }),
2275
+ this._request("DELETE", `/api/v1/relation-layers/${relationLayerId}`, {
2276
+ auditMessage,
2277
+ }),
1447
2278
  /**
1448
2279
  * Update a relation layer's name.
1449
2280
  * @param {string} relationLayerId - The relation layer ID
1450
2281
  * @param {string} name - The name
1451
2282
  */
1452
2283
  update: (relationLayerId, name, auditMessage) =>
1453
- this._request('PATCH', `/api/v1/relation-layers/${relationLayerId}`, { auditMessage,
2284
+ this._request("PATCH", `/api/v1/relation-layers/${relationLayerId}`, {
2285
+ auditMessage,
1454
2286
  body: bodyOf({ name }),
1455
2287
  }),
1456
2288
  };
@@ -1474,9 +2306,25 @@ class PlaidClient {
1474
2306
  * @param {number} [precedence] - Ordering precedence
1475
2307
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1476
2308
  */
1477
- create: (tokenLayerId, text, begin, end, precedence, metadata, auditMessage) =>
1478
- this._request('POST', '/api/v1/tokens', { auditMessage,
1479
- body: bodyOf({ 'token-layer-id': tokenLayerId, text, begin, end, precedence, metadata }),
2309
+ create: (
2310
+ tokenLayerId,
2311
+ text,
2312
+ begin,
2313
+ end,
2314
+ precedence,
2315
+ metadata,
2316
+ auditMessage,
2317
+ ) =>
2318
+ this._request("POST", "/api/v1/tokens", {
2319
+ auditMessage,
2320
+ body: bodyOf({
2321
+ "token-layer-id": tokenLayerId,
2322
+ text,
2323
+ begin,
2324
+ end,
2325
+ precedence,
2326
+ metadata,
2327
+ }),
1480
2328
  }),
1481
2329
  /**
1482
2330
  * Get a token.
@@ -1484,8 +2332,8 @@ class PlaidClient {
1484
2332
  * @param {string} [asOf] - Temporal query timestamp
1485
2333
  */
1486
2334
  get: (tokenId, asOf) =>
1487
- this._request('GET', `/api/v1/tokens/${tokenId}`, {
1488
- queryParams: { 'as-of': asOf },
2335
+ this._request("GET", `/api/v1/tokens/${tokenId}`, {
2336
+ queryParams: { "as-of": asOf },
1489
2337
  }),
1490
2338
  /**
1491
2339
  * Delete a token and remove it from any spans. If this causes a span to
@@ -1493,7 +2341,7 @@ class PlaidClient {
1493
2341
  * @param {string} tokenId - The token ID
1494
2342
  */
1495
2343
  delete: (tokenId, auditMessage) =>
1496
- this._request('DELETE', `/api/v1/tokens/${tokenId}`, { auditMessage }),
2344
+ this._request("DELETE", `/api/v1/tokens/${tokenId}`, { auditMessage }),
1497
2345
  /**
1498
2346
  * Update a token.
1499
2347
  * @param {string} tokenId - The token ID
@@ -1505,7 +2353,8 @@ class PlaidClient {
1505
2353
  * drops undefined, so the three cases map correctly to the server.
1506
2354
  */
1507
2355
  update: (tokenId, begin, end, precedence, auditMessage) =>
1508
- this._request('PATCH', `/api/v1/tokens/${tokenId}`, { auditMessage,
2356
+ this._request("PATCH", `/api/v1/tokens/${tokenId}`, {
2357
+ auditMessage,
1509
2358
  body: bodyOf({ begin, end, precedence }),
1510
2359
  }),
1511
2360
  /**
@@ -1514,13 +2363,13 @@ class PlaidClient {
1514
2363
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
1515
2364
  */
1516
2365
  bulkCreate: (body, auditMessage) =>
1517
- this._request('POST', '/api/v1/tokens/bulk', { auditMessage, body }),
2366
+ this._request("POST", "/api/v1/tokens/bulk", { auditMessage, body }),
1518
2367
  /**
1519
2368
  * Delete multiple tokens in a single operation. Provide an array of IDs.
1520
2369
  * @param {Array} body - The request body
1521
2370
  */
1522
2371
  bulkDelete: (body, auditMessage) =>
1523
- this._request('DELETE', '/api/v1/tokens/bulk', { auditMessage, body }),
2372
+ this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
1524
2373
  /**
1525
2374
  * Split a token at a Unicode code-point offset. The original token becomes the
1526
2375
  * left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
@@ -1528,7 +2377,8 @@ class PlaidClient {
1528
2377
  * @param {number} position - Code-point offset to split at (strictly between begin and end)
1529
2378
  */
1530
2379
  split: (tokenId, position, auditMessage) =>
1531
- this._request('POST', `/api/v1/tokens/${tokenId}/split`, { auditMessage,
2380
+ this._request("POST", `/api/v1/tokens/${tokenId}/split`, {
2381
+ auditMessage,
1532
2382
  body: bodyOf({ position }),
1533
2383
  }),
1534
2384
  /**
@@ -1540,8 +2390,9 @@ class PlaidClient {
1540
2390
  * @param {string} otherTokenId - The other token to merge in
1541
2391
  */
1542
2392
  merge: (tokenId, otherTokenId, auditMessage) =>
1543
- this._request('POST', `/api/v1/tokens/${tokenId}/merge`, { auditMessage,
1544
- body: bodyOf({ 'other-token-id': otherTokenId }),
2393
+ this._request("POST", `/api/v1/tokens/${tokenId}/merge`, {
2394
+ auditMessage,
2395
+ body: bodyOf({ "other-token-id": otherTokenId }),
1545
2396
  }),
1546
2397
  /**
1547
2398
  * Shift a token's boundary. On partitioning layers the adjacent token is
@@ -1552,7 +2403,8 @@ class PlaidClient {
1552
2403
  * @param {number} [end] - New end offset, exclusive (Unicode code points)
1553
2404
  */
1554
2405
  shift: (tokenId, begin, end, auditMessage) =>
1555
- this._request('POST', `/api/v1/tokens/${tokenId}/shift`, { auditMessage,
2406
+ this._request("POST", `/api/v1/tokens/${tokenId}/shift`, {
2407
+ auditMessage,
1556
2408
  body: bodyOf({ begin, end }),
1557
2409
  }),
1558
2410
  /**
@@ -1561,15 +2413,18 @@ class PlaidClient {
1561
2413
  * @param {any} body - The request body
1562
2414
  */
1563
2415
  setMetadata: (tokenId, body, auditMessage) =>
1564
- this._request('PUT', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1565
- rawBody: body, skipResponseTransform: true,
2416
+ this._request("PUT", `/api/v1/tokens/${tokenId}/metadata`, {
2417
+ auditMessage,
2418
+ rawBody: body,
2419
+ skipResponseTransform: true,
1566
2420
  }),
1567
2421
  /**
1568
2422
  * Remove all metadata from a token.
1569
2423
  * @param {string} tokenId - The token ID
1570
2424
  */
1571
2425
  deleteMetadata: (tokenId, auditMessage) =>
1572
- this._request('DELETE', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
2426
+ this._request("DELETE", `/api/v1/tokens/${tokenId}/metadata`, {
2427
+ auditMessage,
1573
2428
  skipResponseTransform: true,
1574
2429
  }),
1575
2430
  /**
@@ -1578,8 +2433,10 @@ class PlaidClient {
1578
2433
  * @param {any} body - The metadata patch
1579
2434
  */
1580
2435
  patchMetadata: (tokenId, body, auditMessage) =>
1581
- this._request('PATCH', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1582
- rawBody: body, skipResponseTransform: true,
2436
+ this._request("PATCH", `/api/v1/tokens/${tokenId}/metadata`, {
2437
+ auditMessage,
2438
+ rawBody: body,
2439
+ skipResponseTransform: true,
1583
2440
  }),
1584
2441
  };
1585
2442
 
@@ -1595,14 +2452,22 @@ class PlaidClient {
1595
2452
  createSSEConnection(this, projectId, onEvent, path),
1596
2453
 
1597
2454
  /**
1598
- * Send a message to project listeners
2455
+ * Send a message to project listeners.
2456
+ *
2457
+ * `data` may be any JSON value and is sent VERBATIM (`rawBody`): a
2458
+ * message payload is opaque application data, like `metadata` and
2459
+ * `config`, so its keys must not be re-cased on the way out. Without
2460
+ * this a key such as `case-marker` would reach listeners as
2461
+ * `caseMarker` in JS and `case_marker` in Python. `listen` restores it
2462
+ * verbatim on the way in.
1599
2463
  * @param {string} projectId - The UUID of the project to send to
1600
2464
  * @param {any} data - The message data to send
1601
2465
  * @returns {Promise<any>} Response from the send operation
1602
2466
  */
1603
2467
  sendMessage: (projectId, data, auditMessage) =>
1604
- this._request('POST', `/api/v1/projects/${projectId}/message`, { auditMessage,
1605
- body: { body: data },
2468
+ this._request("POST", `/api/v1/projects/${projectId}/message`, {
2469
+ auditMessage,
2470
+ rawBody: { body: data },
1606
2471
  }),
1607
2472
 
1608
2473
  /**
@@ -1612,8 +2477,7 @@ class PlaidClient {
1612
2477
  * @param {string} projectId - The UUID of the project to query
1613
2478
  * @returns {Promise<Array>} Array of discovered service information
1614
2479
  */
1615
- discoverServices: (projectId) =>
1616
- discoverServices(this, projectId),
2480
+ discoverServices: (projectId) => discoverServices(this, projectId),
1617
2481
 
1618
2482
  /**
1619
2483
  * Forget a previously-seen (offline) service. Maintainer-only; 409 if
@@ -1627,26 +2491,100 @@ class PlaidClient {
1627
2491
 
1628
2492
  /**
1629
2493
  * Register as a service and handle incoming work requests.
2494
+ *
2495
+ * The registration reopens its channel whenever it drops, so a server
2496
+ * restart needs no service restart.
1630
2497
  * @param {string} projectId - The UUID of the project to serve
1631
2498
  * @param {Object} serviceInfo - Service information {serviceId, serviceName, description}
1632
2499
  * @param {function} onServiceRequest - Callback (data, responseHelper)
1633
2500
  * @param {Object} [extras] - Optional additional service metadata
1634
- * @returns {Object} Service registration object with .stop() method
2501
+ * @param {function} [onStatus] - Optional callback (event, projectId, detail) for
2502
+ * connection transitions: 'registered', 'reconnected', 'disconnected'
2503
+ * @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
1635
2504
  */
1636
- serve: (projectId, serviceInfo, onServiceRequest, extras) =>
1637
- serve(this, projectId, serviceInfo, onServiceRequest, extras),
2505
+ serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
2506
+ serve(this, projectId, serviceInfo, onServiceRequest, extras, onStatus),
1638
2507
 
1639
2508
  /**
1640
2509
  * Request a service to perform work and await its result.
2510
+ *
2511
+ * The request outlives this call: after a timeout, an abort, or a
2512
+ * dropped connection the service goes on, and `attachServiceRequest`
2513
+ * collects the result given the request id, which `opts.onAccepted`
2514
+ * receives as soon as the server has taken the request. Pass
2515
+ * `opts.requestId` (a UUID you mint) to know the id before submitting;
2516
+ * submitting an id that names a request you already made rejoins it
2517
+ * instead of starting another.
1641
2518
  * @param {string} projectId - The UUID of the project
1642
2519
  * @param {string} serviceId - The ID of the service to request
1643
2520
  * @param {any} data - The request data
1644
- * @param {number} [timeout] - Timeout in milliseconds (default: 10000)
2521
+ * @param {number} [timeout=10000] - Timeout in ms
1645
2522
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
1646
- * @returns {Promise<any>} Service response
2523
+ * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
2524
+ * @param {Object} [opts] - {requestId, onAccepted}
2525
+ * @returns {Promise<any>} The service's result
2526
+ */
2527
+ requestService: (
2528
+ projectId,
2529
+ serviceId,
2530
+ data,
2531
+ timeout,
2532
+ onProgress,
2533
+ signal,
2534
+ opts,
2535
+ ) =>
2536
+ requestService(
2537
+ this,
2538
+ projectId,
2539
+ serviceId,
2540
+ data,
2541
+ timeout,
2542
+ onProgress,
2543
+ signal,
2544
+ opts,
2545
+ ),
2546
+
2547
+ /**
2548
+ * Rejoin a service request made earlier and await its result: the
2549
+ * latest progress is replayed, then the result comes, or at once if the
2550
+ * request already finished. Only the user who submitted it (or an
2551
+ * admin). Rejects with an error whose `status` is 404 when the request
2552
+ * is unknown or expired (a finished request's result is kept for a
2553
+ * while, not forever).
2554
+ * @param {string} projectId - The UUID of the project
2555
+ * @param {string} requestId - The request id
2556
+ * @param {number} [timeout=10000] - Timeout in ms
2557
+ * @param {function} [onProgress] - Called with each progress payload
2558
+ * @param {AbortSignal} [signal] - Abort to stop waiting
2559
+ * @returns {Promise<any>} The service's result
2560
+ */
2561
+ attachServiceRequest: (
2562
+ projectId,
2563
+ requestId,
2564
+ timeout,
2565
+ onProgress,
2566
+ signal,
2567
+ ) =>
2568
+ attachServiceRequest(
2569
+ this,
2570
+ projectId,
2571
+ requestId,
2572
+ timeout,
2573
+ onProgress,
2574
+ signal,
2575
+ ),
2576
+
2577
+ /**
2578
+ * Ask the service to stop a request made earlier. The request still
2579
+ * ends with whatever the service then reports, on the stream of whoever
2580
+ * is awaiting it. Rejects with 404 if unknown or expired, 409 once
2581
+ * finished.
2582
+ * @param {string} projectId - The UUID of the project
2583
+ * @param {string} requestId - The request id
2584
+ * @returns {Promise<void>}
1647
2585
  */
1648
- requestService: (projectId, serviceId, data, timeout, onProgress) =>
1649
- requestService(this, projectId, serviceId, data, timeout, onProgress),
2586
+ cancelServiceRequest: (projectId, requestId) =>
2587
+ cancelServiceRequest(this, projectId, requestId),
1650
2588
  };
1651
2589
 
1652
2590
  /**
@@ -1684,8 +2622,18 @@ class PlaidClient {
1684
2622
  * For 'count': {return: 'count', count}. Entity cells are full entity objects
1685
2623
  * (same shape as the GET endpoints).
1686
2624
  */
2625
+ // bypassBatch: a query is a read. `client.batched()` flips one flag on the
2626
+ // whole client, so a query issued by unrelated code while a batch is open
2627
+ // (the IGT island fetching precedent while a document reconciles) used to
2628
+ // be queued into it: the caller got `{batched: true}` instead of results,
2629
+ // and the server ran the query against the batch's tx Connection, which
2630
+ // 500s and rolls back every write in that batch.
1687
2631
  this.query = (body, auditMessage) =>
1688
- this._request('POST', '/api/v1/query', { auditMessage, body });
2632
+ this._request("POST", "/api/v1/query", {
2633
+ auditMessage,
2634
+ body,
2635
+ bypassBatch: true,
2636
+ });
1689
2637
 
1690
2638
  // Logical-operation groups (audit-log grouping). There is no create: a
1691
2639
  // group row is made lazily by the first write carrying `?group-id=`
@@ -1695,15 +2643,193 @@ class PlaidClient {
1695
2643
  * Get a logical-operation group (its label + creator).
1696
2644
  * @param {string} id - The group id
1697
2645
  */
1698
- get: (id) =>
1699
- this._request('GET', `/api/v1/operation-groups/${id}`),
2646
+ get: (id) => this._request("GET", `/api/v1/operation-groups/${id}`),
1700
2647
  /**
1701
2648
  * Relabel a logical-operation group after the fact. Owner or admin only.
1702
2649
  * @param {string} id - The group id
1703
2650
  * @param {string|null} message - The new label
1704
2651
  */
1705
2652
  update: (id, message) =>
1706
- this._request('PATCH', `/api/v1/operation-groups/${id}`, { body: { message } }),
2653
+ this._request("PATCH", `/api/v1/operation-groups/${id}`, {
2654
+ body: { message },
2655
+ }),
2656
+ };
2657
+
2658
+ this.comments = {
2659
+ /**
2660
+ * Post a comment on an entity. Requires write access to the entity's
2661
+ * project, or to the vocabulary for a vocab-item; the author is the
2662
+ * authenticated caller. Comments are not audited and do not bump the
2663
+ * document version.
2664
+ *
2665
+ * A comment outlives its anchor: deleting the entity does not delete
2666
+ * the comment. `anchorLabel` is the caption shown once that happens
2667
+ * ("Gloss of ktab, sentence 4"), so pass what the comment is about.
2668
+ * @param {'document'|'text'|'token'|'span'|'relation'|'vocab-item'} entityType - What kind of thing is being commented on
2669
+ * @param {string} entityId - The commented entity's id
2670
+ * @param {string} body - The comment text (1..10000 characters)
2671
+ * @param {object} [opts]
2672
+ * @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
2673
+ */
2674
+ create: (entityType, entityId, body, { anchorLabel } = {}) =>
2675
+ this._request("POST", "/api/v1/comments", {
2676
+ body: bodyOf({
2677
+ "entity-type": entityType,
2678
+ "entity-id": entityId,
2679
+ body,
2680
+ "anchor-label": anchorLabel,
2681
+ }),
2682
+ }),
2683
+ /**
2684
+ * Read one comment.
2685
+ * @param {string} id - The comment id
2686
+ */
2687
+ get: (id) => this._request("GET", `/api/v1/comments/${id}`),
2688
+ /**
2689
+ * Edit a comment's body. Only the comment's AUTHOR may do this - not
2690
+ * maintainers, not admins. Sets `edited` on the comment.
2691
+ * @param {string} id - The comment id
2692
+ * @param {string} body - The replacement text
2693
+ */
2694
+ update: (id, body) =>
2695
+ this._request("PATCH", `/api/v1/comments/${id}`, { body: { body } }),
2696
+ /**
2697
+ * Delete a comment. The author may delete their own; a project
2698
+ * maintainer (or admin) may delete any.
2699
+ * @param {string} id - The comment id
2700
+ */
2701
+ delete: (id) => this._request("DELETE", `/api/v1/comments/${id}`),
2702
+ /**
2703
+ * List comments in a project, oldest first. Transparently follows
2704
+ * pagination cursors and returns the full flat array.
2705
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching - use listPage() for a single page in a batch.
2706
+ * @param {string} projectId - The project to read
2707
+ * @param {object} [filters] - Narrow the scope
2708
+ * @param {string} [filters.documentId] - Every comment anywhere in one document
2709
+ * @param {string} [filters.entityType] - With entityId, one entity's thread
2710
+ * @param {string} [filters.entityId] - With entityType, one entity's thread
2711
+ */
2712
+ list: (projectId, { documentId, entityType, entityId } = {}) =>
2713
+ listAll(this, `/api/v1/projects/${projectId}/comments`, {
2714
+ query: {
2715
+ "document-id": documentId,
2716
+ "entity-type": entityType,
2717
+ "entity-id": entityId,
2718
+ },
2719
+ }),
2720
+ /**
2721
+ * Fetch a single page of a project's comments.
2722
+ * @param {string} projectId - The project to read
2723
+ * @param {object} [opts]
2724
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2725
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
2726
+ * @param {string} [opts.documentId] - Every comment anywhere in one document
2727
+ * @param {string} [opts.entityType] - With entityId, one entity's thread
2728
+ * @param {string} [opts.entityId] - With entityType, one entity's thread
2729
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2730
+ */
2731
+ listPage: (
2732
+ projectId,
2733
+ { limit, cursor, documentId, entityType, entityId } = {},
2734
+ ) =>
2735
+ listPage(this, `/api/v1/projects/${projectId}/comments`, {
2736
+ limit,
2737
+ cursor,
2738
+ query: {
2739
+ "document-id": documentId,
2740
+ "entity-type": entityType,
2741
+ "entity-id": entityId,
2742
+ },
2743
+ }),
2744
+ /**
2745
+ * Async-iterate a project's comments page by page; yields each page's entries array.
2746
+ * 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.
2747
+ * @param {string} projectId - The project to read
2748
+ * @param {object} [opts]
2749
+ * @param {number} [opts.pageSize] - Per-request page size
2750
+ * @param {string} [opts.documentId] - Every comment anywhere in one document
2751
+ * @param {string} [opts.entityType] - With entityId, one entity's thread
2752
+ * @param {string} [opts.entityId] - With entityType, one entity's thread
2753
+ * @returns {AsyncGenerator<Array>}
2754
+ */
2755
+ iterPages: (
2756
+ projectId,
2757
+ { pageSize, documentId, entityType, entityId } = {},
2758
+ ) =>
2759
+ iterPages(this, `/api/v1/projects/${projectId}/comments`, {
2760
+ pageSize,
2761
+ query: {
2762
+ "document-id": documentId,
2763
+ "entity-type": entityType,
2764
+ "entity-id": entityId,
2765
+ },
2766
+ }),
2767
+ /**
2768
+ * Comment counts per entity, as an `{entityId: n}` map, over the same
2769
+ * scope and filters as `list`. One cheap request paints a comment
2770
+ * indicator on every annotated item in a document without paging
2771
+ * through the bodies.
2772
+ *
2773
+ * The response is NOT key-transformed: its keys are entity ids, and
2774
+ * camelCasing would mangle the hyphens in a UUID.
2775
+ * @param {string} projectId - The project to read
2776
+ * @param {object} [filters] - Same filters as list()
2777
+ * @returns {Promise<Object<string, number>>}
2778
+ */
2779
+ counts: (projectId, { documentId, entityType, entityId } = {}) =>
2780
+ this._request("GET", `/api/v1/projects/${projectId}/comments/counts`, {
2781
+ queryParams: {
2782
+ "document-id": documentId,
2783
+ "entity-type": entityType,
2784
+ "entity-id": entityId,
2785
+ },
2786
+ skipResponseTransform: true,
2787
+ }),
2788
+ /**
2789
+ * List the comments on a vocabulary's entries, oldest first. Requires
2790
+ * read access to the vocabulary. Transparently follows pagination
2791
+ * cursors and returns the full flat array.
2792
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching - use listInVocabPage() for a single page in a batch.
2793
+ * @param {string} vocabId - The vocab layer to read
2794
+ * @param {object} [filters]
2795
+ * @param {string} [filters.entityId] - One entry's thread
2796
+ */
2797
+ listInVocab: (vocabId, { entityId } = {}) =>
2798
+ listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
2799
+ query: { "entity-id": entityId },
2800
+ }),
2801
+ /**
2802
+ * Fetch a single page of a vocabulary's comments.
2803
+ * @param {string} vocabId - The vocab layer to read
2804
+ * @param {object} [opts]
2805
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2806
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
2807
+ * @param {string} [opts.entityId] - One entry's thread
2808
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2809
+ */
2810
+ listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
2811
+ listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
2812
+ limit,
2813
+ cursor,
2814
+ query: { "entity-id": entityId },
2815
+ }),
2816
+ /**
2817
+ * Comment counts per entry of a vocabulary, as an `{entityId: n}` map.
2818
+ * The response is NOT key-transformed: its keys are entry ids.
2819
+ * @param {string} vocabId - The vocab layer to read
2820
+ * @param {object} [filters]
2821
+ * @param {string} [filters.entityId] - One entry only
2822
+ * @returns {Promise<Object<string, number>>}
2823
+ */
2824
+ countsInVocab: (vocabId, { entityId } = {}) =>
2825
+ this._request(
2826
+ "GET",
2827
+ `/api/v1/vocab-layers/${vocabId}/comments/counts`,
2828
+ {
2829
+ queryParams: { "entity-id": entityId },
2830
+ skipResponseTransform: true,
2831
+ },
2832
+ ),
1707
2833
  };
1708
2834
  }
1709
2835
 
@@ -1811,7 +2937,9 @@ class PlaidClient {
1811
2937
  async withOperation(message, fn) {
1812
2938
  this.beginOperation(message);
1813
2939
  const group = this.operationGroup;
1814
- const setMessage = (msg) => { if (group.depth === 1) group.refined = msg; };
2940
+ const setMessage = (msg) => {
2941
+ if (group.depth === 1) group.refined = msg;
2942
+ };
1815
2943
  try {
1816
2944
  return await fn(setMessage);
1817
2945
  } finally {
@@ -1835,7 +2963,7 @@ class PlaidClient {
1835
2963
  */
1836
2964
  async submitBatch() {
1837
2965
  if (!this.isBatching) {
1838
- throw new Error('No active batch. Call beginBatch() first.');
2966
+ throw new Error("No active batch. Call beginBatch() first.");
1839
2967
  }
1840
2968
 
1841
2969
  if (this.batchOperations.length === 0) {
@@ -1844,55 +2972,86 @@ class PlaidClient {
1844
2972
  }
1845
2973
 
1846
2974
  try {
1847
- let url = `${this.baseUrl}/api/v1/batch`;
1848
- const body = this.batchOperations.map(op => ({
2975
+ const url = `${this.baseUrl}/api/v1/batch`;
2976
+ const ops = this.batchOperations.map((op) => ({
1849
2977
  path: op.path,
1850
2978
  method: op.method.toUpperCase(),
1851
2979
  ...(op.body && { body: op.body }),
1852
2980
  }));
2981
+ // The server caps a batch at MAX_BATCH_OPS so one transaction cannot
2982
+ // hold the write lock without bound. A larger batch goes as consecutive
2983
+ // requests, results concatenated in queue order: it could not have been
2984
+ // one transaction anyway, and a repair or bulk edit over a big document
2985
+ // must not fail on its size alone.
2986
+ const results = [];
2987
+ for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
2988
+ results.push(
2989
+ ...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))),
2990
+ );
2991
+ }
2992
+ return results;
2993
+ } finally {
2994
+ this.isBatching = false;
2995
+ this.batchOperations = [];
2996
+ }
2997
+ }
1853
2998
 
1854
- const fetchOptions = {
1855
- method: 'POST',
1856
- headers: {
1857
- 'Authorization': `Bearer ${this.token}`,
1858
- 'Content-Type': 'application/json',
1859
- },
1860
- body: JSON.stringify(body),
1861
- };
1862
- const signal = timeoutSignal(this.timeout);
1863
- if (signal) fetchOptions.signal = signal;
2999
+ /** POST one batch request (at most MAX_BATCH_OPS operations); resolves to its transformed results. */
3000
+ async _postBatch(url, body) {
3001
+ const fetchOptions = {
3002
+ method: "POST",
3003
+ headers: {
3004
+ Authorization: `Bearer ${this.token}`,
3005
+ "Content-Type": "application/json",
3006
+ },
3007
+ body: JSON.stringify(body),
3008
+ };
1864
3009
 
1865
- try {
1866
- const response = await fetch(url, fetchOptions);
1867
- if (!response.ok) {
1868
- throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
3010
+ try {
3011
+ // Retry a 503: the batch is atomic, so a refused one wrote nothing and
3012
+ // repeating it is safe. A fresh timeout signal per attempt — an
3013
+ // AbortSignal.timeout stays aborted once it fires.
3014
+ const response = await retryWhileBusy(async () => {
3015
+ const signal = timeoutSignal(this.batchTimeout);
3016
+ const res = await fetch(
3017
+ url,
3018
+ signal ? { ...fetchOptions, signal } : fetchOptions,
3019
+ );
3020
+ if (!res.ok) {
3021
+ throw makeHttpError(res, await parseErrorBody(res), url, "POST");
1869
3022
  }
3023
+ return res;
3024
+ });
1870
3025
 
1871
- const results = await response.json();
1872
-
1873
- // Extract document versions from each batch response
1874
- for (const result of results) {
1875
- if (result.headers && result.headers['X-Document-Versions']) {
1876
- try {
1877
- const versionsMap = JSON.parse(result.headers['X-Document-Versions']);
1878
- if (typeof versionsMap === 'object' && versionsMap !== null) {
1879
- // Clone once per response, then merge — not once per entry.
1880
- this.documentVersions = { ...this.documentVersions, ...versionsMap };
1881
- }
1882
- } catch (e) {
1883
- console.warn('Failed to parse document versions header from batch response:', e);
3026
+ const results = await response.json();
3027
+
3028
+ // Extract document versions from each batch response
3029
+ for (const result of results) {
3030
+ if (result.headers && result.headers["X-Document-Versions"]) {
3031
+ try {
3032
+ const versionsMap = JSON.parse(
3033
+ result.headers["X-Document-Versions"],
3034
+ );
3035
+ if (typeof versionsMap === "object" && versionsMap !== null) {
3036
+ // Clone once per response, then merge — not once per entry.
3037
+ this.documentVersions = {
3038
+ ...this.documentVersions,
3039
+ ...versionsMap,
3040
+ };
1884
3041
  }
3042
+ } catch (e) {
3043
+ console.warn(
3044
+ "Failed to parse document versions header from batch response:",
3045
+ e,
3046
+ );
1885
3047
  }
1886
3048
  }
1887
-
1888
- return results.map(result => transformResponse(result));
1889
- } catch (error) {
1890
- if (error.status) throw error;
1891
- throw makeNetworkError(error, url, 'POST');
1892
3049
  }
1893
- } finally {
1894
- this.isBatching = false;
1895
- this.batchOperations = [];
3050
+
3051
+ return results.map((result) => transformResponse(result));
3052
+ } catch (error) {
3053
+ if (error.status) throw error;
3054
+ throw makeNetworkError(error, url, "POST");
1896
3055
  }
1897
3056
  }
1898
3057
 
@@ -1951,6 +3110,27 @@ class PlaidClient {
1951
3110
  * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
1952
3111
  * @returns {Promise<PlaidClient>} - Authenticated client instance
1953
3112
  */
3113
+ /**
3114
+ * Liveness, version and database size, with NO authentication and no client
3115
+ * instance — for a launcher or status page checking whether a server is up
3116
+ * before anyone logs in.
3117
+ * @param {string} baseUrl - The API base URL
3118
+ * @param {object} [options]
3119
+ */
3120
+ static async health(baseUrl, options = {}) {
3121
+ return anonymousGet(baseUrl, "/health", options);
3122
+ }
3123
+
3124
+ /**
3125
+ * The limits this server enforces, with NO authentication and no client
3126
+ * instance. See the instance method for the shape.
3127
+ * @param {string} baseUrl - The API base URL
3128
+ * @param {object} [options]
3129
+ */
3130
+ static async info(baseUrl, options = {}) {
3131
+ return anonymousGet(baseUrl, "/api/v1/info", options);
3132
+ }
3133
+
1954
3134
  /**
1955
3135
  * Build the link to hand someone for an invite code. The server never sees
1956
3136
  * an app URL, so the app that minted the invite is the one that names it.
@@ -1961,7 +3141,7 @@ class PlaidClient {
1961
3141
  * @returns {string}
1962
3142
  */
1963
3143
  static inviteUrl(appUrl, code) {
1964
- return `${appUrl.replace(/#.*$/, '').replace(/\/$/, '')}/#/invite/${encodeURIComponent(code)}`;
3144
+ return `${appUrl.replace(/#.*$/, "").replace(/\/$/, "")}/#/invite/${encodeURIComponent(code)}`;
1965
3145
  }
1966
3146
 
1967
3147
  /**
@@ -1975,63 +3155,85 @@ class PlaidClient {
1975
3155
  * @param {string} baseUrl - The API base URL
1976
3156
  * @param {string} code - The invite code
1977
3157
  * @param {object} [options]
1978
- * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, username?: string, grantAdmin: boolean}>}
3158
+ * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
1979
3159
  */
1980
3160
  static async lookupInvite(baseUrl, code, options = {}) {
1981
- return anonymousPost(baseUrl, '/api/v1/invite-codes/lookup', { code }, options);
3161
+ return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
1982
3162
  }
1983
3163
 
1984
3164
  /**
1985
3165
  * Redeem an invite code, with NO authentication.
1986
3166
  *
1987
- * For a signup invite, pass `username` and `password` to create the account;
1988
- * the invite's grants are applied in the same transaction. For a password
1989
- * reset link, pass `password` only. Resolves to a logged-in client, exactly
1990
- * like login() — the redeemer just chose these credentials, so there is no
1991
- * reason to send them to a login form to retype them.
3167
+ * For a signup invite, pass `email` and `password` to create the account
3168
+ * (and optionally `displayName`); the invite's grants are applied in the
3169
+ * same transaction. For a password reset link, pass `password` only.
3170
+ * Resolves to a logged-in client, exactly like login() — the redeemer just
3171
+ * chose these credentials, so there is no reason to send them to a login
3172
+ * form to retype them.
1992
3173
  * @param {string} baseUrl - The API base URL
1993
3174
  * @param {string} code - The invite code
1994
3175
  * @param {object} credentials
1995
- * @param {string} [credentials.username] - Desired username (signup only)
3176
+ * @param {string} [credentials.email] - The new account's email address,
3177
+ * which becomes its id and login (signup only)
3178
+ * @param {string} [credentials.displayName] - How the new user is shown in
3179
+ * the UI; defaults to the local part of the email (signup only)
1996
3180
  * @param {string} credentials.password - Desired password (min 8 characters)
1997
3181
  * @param {object} [options]
1998
3182
  * @returns {Promise<{client: PlaidClient, userId: string, kind: string}>}
1999
3183
  */
2000
- static async redeemInvite(baseUrl, code, { username, password } = {}, options = {}) {
3184
+ static async redeemInvite(
3185
+ baseUrl,
3186
+ code,
3187
+ { email, password, displayName } = {},
3188
+ options = {},
3189
+ ) {
2001
3190
  const data = await anonymousPost(
2002
- baseUrl, '/api/v1/invite-codes/redeem',
2003
- bodyOf({ code, username, password }), options,
3191
+ baseUrl,
3192
+ "/api/v1/invites/redeem",
3193
+ bodyOf({ code, email, password, "display-name": displayName }),
3194
+ options,
2004
3195
  );
2005
3196
  return {
2006
- client: new PlaidClient(baseUrl.replace(/\/$/, ''), data.token || '', options),
3197
+ client: new PlaidClient(
3198
+ baseUrl.replace(/\/$/, ""),
3199
+ data.token || "",
3200
+ options,
3201
+ ),
2007
3202
  userId: data.userId,
2008
3203
  kind: data.kind,
2009
3204
  };
2010
3205
  }
2011
3206
 
2012
3207
  static async login(baseUrl, userId, password, options = {}) {
2013
- baseUrl = baseUrl.replace(/\/$/, '');
3208
+ baseUrl = baseUrl.replace(/\/$/, "");
2014
3209
  const url = `${baseUrl}/api/v1/login`;
2015
3210
  try {
2016
3211
  const fetchOptions = {
2017
- method: 'POST',
2018
- headers: { 'Content-Type': 'application/json' },
2019
- body: JSON.stringify({ 'user-id': userId, password }),
3212
+ method: "POST",
3213
+ headers: { "Content-Type": "application/json" },
3214
+ body: JSON.stringify({ "user-id": userId, password }),
2020
3215
  };
2021
- const signal = timeoutSignal(options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS);
3216
+ const signal = timeoutSignal(
3217
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
3218
+ );
2022
3219
  if (signal) fetchOptions.signal = signal;
2023
3220
 
2024
3221
  const response = await fetch(url, fetchOptions);
2025
3222
  if (!response.ok) {
2026
- throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
3223
+ throw makeHttpError(
3224
+ response,
3225
+ await parseErrorBody(response),
3226
+ url,
3227
+ "POST",
3228
+ );
2027
3229
  }
2028
3230
 
2029
3231
  const data = await response.json();
2030
- const token = data.token || '';
3232
+ const token = data.token || "";
2031
3233
  return new PlaidClient(baseUrl, token, options);
2032
3234
  } catch (error) {
2033
3235
  if (error.status) throw error;
2034
- throw makeNetworkError(error, url, 'POST');
3236
+ throw makeNetworkError(error, url, "POST");
2035
3237
  }
2036
3238
  }
2037
3239
  }
@@ -2041,8 +3243,22 @@ export { PlaidClient };
2041
3243
 
2042
3244
  // Unicode code-point helpers for text offsets (token begin/end are code-point
2043
3245
  // indices). See ./codepoint.js.
2044
- export { cpLength, cpSlice, cpSlicer, utf16ToCp, cpToUtf16, cpIndexOf } from './codepoint.js';
2045
- export { PLAID_NAMESPACE, ROLE_KEY, ROLES, readRole, findByRole } from './roles.js';
3246
+ export {
3247
+ cpLength,
3248
+ cpSlice,
3249
+ cpSlicer,
3250
+ utf16ToCp,
3251
+ cpToUtf16,
3252
+ cpIndexOf,
3253
+ } from "./codepoint.js";
3254
+ export {
3255
+ PLAID_NAMESPACE,
3256
+ ROLE_KEY,
3257
+ PRESERVE_ON_SPLIT_KEY,
3258
+ ROLES,
3259
+ readRole,
3260
+ findByRole,
3261
+ } from "./roles.js";
2046
3262
  // Service self-description helpers: filter discovered services by task, read a
2047
3263
  // service's parameter schema/summary, and build/coerce form values. See
2048
3264
  // ./serviceSchema.js.
@@ -2054,20 +3270,40 @@ export {
2054
3270
  getServiceSummary,
2055
3271
  buildDefaultValues,
2056
3272
  coerceParamValues,
2057
- } from './serviceSchema.js';
2058
- // Provenance: the cross-app convention for machine-provided vs human-labeled
2059
- // information (flat prov/provSource/provConfirmed metadata; absence = human),
2060
- // plus the machine-writer contract. See ./provenance.js and the manual,
2061
- // "Provenance".
3273
+ } from "./serviceSchema.js";
3274
+ // Cooperative cancellation for a service AUTHORED in JS: `progress()` is a
3275
+ // checkpoint that throws ServiceCancelled once the requester stops the
3276
+ // request, and `critical()` holds that off around writes. See ./services.js
3277
+ // and the manual, "Stopping a request".
3278
+ export { ServiceCancelled, createCancelScope } from "./services.js";
3279
+ // Provenance: the cross-app convention for who made an annotation (flat
3280
+ // prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
3281
+ // a machine; 'contributed' = a person whose work is reviewed), plus the
3282
+ // machine-writer contract. See ./provenance.js and the manual, "Provenance".
2062
3283
  export {
2063
3284
  PROV,
2064
3285
  PROV_STATES,
2065
3286
  PROV_CONFIRMED,
3287
+ PROVENANCE_KEYS,
2066
3288
  stampInferred,
2067
3289
  confirmedInferred,
3290
+ stampContributed,
2068
3291
  provState,
3292
+ provOrigin,
2069
3293
  isMachine,
2070
3294
  isProtected,
3295
+ needsReview,
2071
3296
  verifyOnEdit,
3297
+ contributeOnEdit,
3298
+ mergeMetadata,
2072
3299
  serviceSource,
2073
- } from './provenance.js';
3300
+ userSource,
3301
+ REVIEW_KEY,
3302
+ PROJECT_ROLES,
3303
+ readReview,
3304
+ projectRole,
3305
+ isReviewed,
3306
+ withReviewedUser,
3307
+ writerPolicy,
3308
+ } from "./provenance.js";
3309
+ export { MAX_BATCH_OPS };