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

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,41 @@ 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
+ * The server's cap on operations per batch request (plaid.rest-api.v1.batch).
90
+ * A batch queued past it is submitted as consecutive requests.
91
+ */
92
+ const MAX_BATCH_OPS = 1000;
93
+
54
94
  class PlaidClient {
55
95
  /**
56
96
  * Create a new PlaidClient instance
@@ -58,11 +98,24 @@ class PlaidClient {
58
98
  * @param {string} token - The authentication token
59
99
  * @param {object} [options] - Client options
60
100
  * @param {number} [options.timeout=30000] - Per-request timeout in ms (0 or null disables it)
101
+ * @param {number} [options.batchTimeout=180000] - Timeout for batch submissions in ms (0 or null disables it)
61
102
  */
62
103
  constructor(baseUrl, token, options = {}) {
63
- this.baseUrl = baseUrl.replace(/\/$/, '');
104
+ this.baseUrl = baseUrl.replace(/\/$/, "");
64
105
  this.token = token;
65
- this.timeout = options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
106
+ this.timeout =
107
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
108
+ // Batch submissions get a longer budget than a single request. Aborting a
109
+ // batch does NOT stop the server: it keeps running the transaction and
110
+ // holding the single SQLite write lock, so a client that gives up early
111
+ // just adds a retry on top of a write that is still in progress. Falls
112
+ // back to an explicit `timeout` when one was given.
113
+ this.batchTimeout =
114
+ options.batchTimeout !== undefined
115
+ ? options.batchTimeout
116
+ : options.timeout !== undefined
117
+ ? options.timeout
118
+ : DEFAULT_BATCH_TIMEOUT_MS;
66
119
  this.isBatching = false;
67
120
  this.batchOperations = [];
68
121
  this.documentVersions = {};
@@ -88,8 +141,9 @@ class PlaidClient {
88
141
  * @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
89
142
  */
90
143
  create: (vocabItem, tokens, metadata, auditMessage) =>
91
- this._request('POST', '/api/v1/vocab-links', { auditMessage,
92
- body: bodyOf({ 'vocab-item': vocabItem, tokens, metadata }),
144
+ this._request("POST", "/api/v1/vocab-links", {
145
+ auditMessage,
146
+ body: bodyOf({ "vocab-item": vocabItem, tokens, metadata }),
93
147
  }),
94
148
  /**
95
149
  * Create multiple vocab links in a single operation. Entries may
@@ -99,28 +153,37 @@ class PlaidClient {
99
153
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
100
154
  */
101
155
  bulkCreate: (body, auditMessage) =>
102
- this._request('POST', '/api/v1/vocab-links/bulk', { auditMessage, body }),
156
+ this._request("POST", "/api/v1/vocab-links/bulk", {
157
+ auditMessage,
158
+ body,
159
+ }),
103
160
  /**
104
161
  * Delete multiple vocab links in a single operation. Provide an array of IDs.
105
162
  * @param {string[]} body - The vocab link IDs to delete
106
163
  */
107
164
  bulkDelete: (body, auditMessage) =>
108
- this._request('DELETE', '/api/v1/vocab-links/bulk', { auditMessage, body }),
165
+ this._request("DELETE", "/api/v1/vocab-links/bulk", {
166
+ auditMessage,
167
+ body,
168
+ }),
109
169
  /**
110
170
  * 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
171
  * @param {string} id - The resource ID
112
172
  * @param {any} body - The request body
113
173
  */
114
174
  setMetadata: (id, body, auditMessage) =>
115
- this._request('PUT', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
116
- rawBody: body, skipResponseTransform: true,
175
+ this._request("PUT", `/api/v1/vocab-links/${id}/metadata`, {
176
+ auditMessage,
177
+ rawBody: body,
178
+ skipResponseTransform: true,
117
179
  }),
118
180
  /**
119
181
  * Remove all metadata from a vocab link.
120
182
  * @param {string} id - The resource ID
121
183
  */
122
184
  deleteMetadata: (id, auditMessage) =>
123
- this._request('DELETE', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
185
+ this._request("DELETE", `/api/v1/vocab-links/${id}/metadata`, {
186
+ auditMessage,
124
187
  skipResponseTransform: true,
125
188
  }),
126
189
  /**
@@ -129,8 +192,10 @@ class PlaidClient {
129
192
  * @param {any} body - The metadata patch
130
193
  */
131
194
  patchMetadata: (id, body, auditMessage) =>
132
- this._request('PATCH', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
133
- rawBody: body, skipResponseTransform: true,
195
+ this._request("PATCH", `/api/v1/vocab-links/${id}/metadata`, {
196
+ auditMessage,
197
+ rawBody: body,
198
+ skipResponseTransform: true,
134
199
  }),
135
200
  /**
136
201
  * Get a vocab link by ID
@@ -138,15 +203,15 @@ class PlaidClient {
138
203
  * @param {string} [asOf] - Temporal query timestamp
139
204
  */
140
205
  get: (id, asOf) =>
141
- this._request('GET', `/api/v1/vocab-links/${id}`, {
142
- queryParams: { 'as-of': asOf },
206
+ this._request("GET", `/api/v1/vocab-links/${id}`, {
207
+ queryParams: { "as-of": asOf },
143
208
  }),
144
209
  /**
145
210
  * Delete a vocab link
146
211
  * @param {string} id - The resource ID
147
212
  */
148
213
  delete: (id, auditMessage) =>
149
- this._request('DELETE', `/api/v1/vocab-links/${id}`, { auditMessage }),
214
+ this._request("DELETE", `/api/v1/vocab-links/${id}`, { auditMessage }),
150
215
  };
151
216
 
152
217
  this.vocabLayers = {
@@ -157,22 +222,23 @@ class PlaidClient {
157
222
  * @param {string} [asOf] - Temporal query timestamp
158
223
  */
159
224
  get: (id, includeItems, asOf) =>
160
- this._request('GET', `/api/v1/vocab-layers/${id}`, {
161
- queryParams: { 'include-items': includeItems, 'as-of': asOf },
225
+ this._request("GET", `/api/v1/vocab-layers/${id}`, {
226
+ queryParams: { "include-items": includeItems, "as-of": asOf },
162
227
  }),
163
228
  /**
164
229
  * Delete a vocab layer.
165
230
  * @param {string} id - The resource ID
166
231
  */
167
232
  delete: (id, auditMessage) =>
168
- this._request('DELETE', `/api/v1/vocab-layers/${id}`, { auditMessage }),
233
+ this._request("DELETE", `/api/v1/vocab-layers/${id}`, { auditMessage }),
169
234
  /**
170
235
  * Update a vocab layer's name.
171
236
  * @param {string} id - The resource ID
172
237
  * @param {string} name - The name
173
238
  */
174
239
  update: (id, name, auditMessage) =>
175
- this._request('PATCH', `/api/v1/vocab-layers/${id}`, { auditMessage,
240
+ this._request("PATCH", `/api/v1/vocab-layers/${id}`, {
241
+ auditMessage,
176
242
  body: bodyOf({ name }),
177
243
  }),
178
244
  /**
@@ -183,9 +249,11 @@ class PlaidClient {
183
249
  * @param {any} configValue - Configuration value to set
184
250
  */
185
251
  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
- }),
252
+ this._request(
253
+ "PUT",
254
+ `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
255
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
256
+ ),
189
257
  /**
190
258
  * Remove a configuration value for a layer.
191
259
  * @param {string} id - The resource ID
@@ -193,9 +261,11 @@ class PlaidClient {
193
261
  * @param {string} configKey - The config key
194
262
  */
195
263
  deleteConfig: (id, namespace, configKey, auditMessage) =>
196
- this._request('DELETE', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, { auditMessage,
197
- skipResponseTransform: true,
198
- }),
264
+ this._request(
265
+ "DELETE",
266
+ `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
267
+ { auditMessage, skipResponseTransform: true },
268
+ ),
199
269
  /**
200
270
  * List all vocab layers accessible to user. Transparently follows
201
271
  * pagination cursors and returns the full flat array.
@@ -203,7 +273,7 @@ class PlaidClient {
203
273
  * @param {string} [asOf] - Temporal query timestamp
204
274
  */
205
275
  list: (asOf) =>
206
- listAll(this, '/api/v1/vocab-layers', { query: { 'as-of': asOf } }),
276
+ listAll(this, "/api/v1/vocab-layers", { query: { "as-of": asOf } }),
207
277
  /**
208
278
  * Fetch a single page of vocab layers.
209
279
  * @param {object} [opts]
@@ -213,7 +283,11 @@ class PlaidClient {
213
283
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
214
284
  */
215
285
  listPage: ({ limit, cursor, asOf } = {}) =>
216
- listPage(this, '/api/v1/vocab-layers', { limit, cursor, query: { 'as-of': asOf } }),
286
+ listPage(this, "/api/v1/vocab-layers", {
287
+ limit,
288
+ cursor,
289
+ query: { "as-of": asOf },
290
+ }),
217
291
  /**
218
292
  * Async-iterate vocab layers page by page; yields each page's entries array.
219
293
  * @param {object} [opts]
@@ -223,13 +297,17 @@ class PlaidClient {
223
297
  * @returns {AsyncGenerator<Array>}
224
298
  */
225
299
  iterPages: ({ pageSize, asOf } = {}) =>
226
- iterPages(this, '/api/v1/vocab-layers', { pageSize, query: { 'as-of': asOf } }),
300
+ iterPages(this, "/api/v1/vocab-layers", {
301
+ pageSize,
302
+ query: { "as-of": asOf },
303
+ }),
227
304
  /**
228
305
  * Create a new vocab layer. Note: this also registers the user as a maintainer.
229
306
  * @param {string} name - The name
230
307
  */
231
308
  create: (name, auditMessage) =>
232
- this._request('POST', '/api/v1/vocab-layers', { auditMessage,
309
+ this._request("POST", "/api/v1/vocab-layers", {
310
+ auditMessage,
233
311
  body: bodyOf({ name }),
234
312
  }),
235
313
  /**
@@ -238,14 +316,22 @@ class PlaidClient {
238
316
  * @param {string} userId - The user ID
239
317
  */
240
318
  addMaintainer: (id, userId, auditMessage) =>
241
- this._request('POST', `/api/v1/vocab-layers/${id}/maintainers/${userId}`, { auditMessage }),
319
+ this._request(
320
+ "POST",
321
+ `/api/v1/vocab-layers/${id}/maintainers/${userId}`,
322
+ { auditMessage },
323
+ ),
242
324
  /**
243
325
  * Remove a user's maintainer privileges for this vocab layer.
244
326
  * @param {string} id - The resource ID
245
327
  * @param {string} userId - The user ID
246
328
  */
247
329
  removeMaintainer: (id, userId, auditMessage) =>
248
- this._request('DELETE', `/api/v1/vocab-layers/${id}/maintainers/${userId}`, { auditMessage }),
330
+ this._request(
331
+ "DELETE",
332
+ `/api/v1/vocab-layers/${id}/maintainers/${userId}`,
333
+ { auditMessage },
334
+ ),
249
335
  };
250
336
 
251
337
  this.relations = {
@@ -255,15 +341,18 @@ class PlaidClient {
255
341
  * @param {any} body - The request body
256
342
  */
257
343
  setMetadata: (relationId, body, auditMessage) =>
258
- this._request('PUT', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
259
- rawBody: body, skipResponseTransform: true,
344
+ this._request("PUT", `/api/v1/relations/${relationId}/metadata`, {
345
+ auditMessage,
346
+ rawBody: body,
347
+ skipResponseTransform: true,
260
348
  }),
261
349
  /**
262
350
  * Remove all metadata from a relation.
263
351
  * @param {string} relationId - The relation ID
264
352
  */
265
353
  deleteMetadata: (relationId, auditMessage) =>
266
- this._request('DELETE', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
354
+ this._request("DELETE", `/api/v1/relations/${relationId}/metadata`, {
355
+ auditMessage,
267
356
  skipResponseTransform: true,
268
357
  }),
269
358
  /**
@@ -272,8 +361,10 @@ class PlaidClient {
272
361
  * @param {any} body - The metadata patch
273
362
  */
274
363
  patchMetadata: (relationId, body, auditMessage) =>
275
- this._request('PATCH', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
276
- rawBody: body, skipResponseTransform: true,
364
+ this._request("PATCH", `/api/v1/relations/${relationId}/metadata`, {
365
+ auditMessage,
366
+ rawBody: body,
367
+ skipResponseTransform: true,
277
368
  }),
278
369
  /**
279
370
  * Update the target span of a relation.
@@ -281,8 +372,9 @@ class PlaidClient {
281
372
  * @param {string} spanId - The span ID
282
373
  */
283
374
  setTarget: (relationId, spanId, auditMessage) =>
284
- this._request('PUT', `/api/v1/relations/${relationId}/target`, { auditMessage,
285
- body: bodyOf({ 'span-id': spanId }),
375
+ this._request("PUT", `/api/v1/relations/${relationId}/target`, {
376
+ auditMessage,
377
+ body: bodyOf({ "span-id": spanId }),
286
378
  }),
287
379
  /**
288
380
  * Get a relation by ID.
@@ -290,22 +382,25 @@ class PlaidClient {
290
382
  * @param {string} [asOf] - Temporal query timestamp
291
383
  */
292
384
  get: (relationId, asOf) =>
293
- this._request('GET', `/api/v1/relations/${relationId}`, {
294
- queryParams: { 'as-of': asOf },
385
+ this._request("GET", `/api/v1/relations/${relationId}`, {
386
+ queryParams: { "as-of": asOf },
295
387
  }),
296
388
  /**
297
389
  * Delete a relation.
298
390
  * @param {string} relationId - The relation ID
299
391
  */
300
392
  delete: (relationId, auditMessage) =>
301
- this._request('DELETE', `/api/v1/relations/${relationId}`, { auditMessage }),
393
+ this._request("DELETE", `/api/v1/relations/${relationId}`, {
394
+ auditMessage,
395
+ }),
302
396
  /**
303
397
  * Update a relation's value.
304
398
  * @param {string} relationId - The relation ID
305
399
  * @param {any} value - The value
306
400
  */
307
401
  update: (relationId, value, auditMessage) =>
308
- this._request('PATCH', `/api/v1/relations/${relationId}`, { auditMessage,
402
+ this._request("PATCH", `/api/v1/relations/${relationId}`, {
403
+ auditMessage,
309
404
  body: bodyOf({ value }),
310
405
  }),
311
406
  /**
@@ -314,8 +409,9 @@ class PlaidClient {
314
409
  * @param {string} spanId - The span ID
315
410
  */
316
411
  setSource: (relationId, spanId, auditMessage) =>
317
- this._request('PUT', `/api/v1/relations/${relationId}/source`, { auditMessage,
318
- body: bodyOf({ 'span-id': spanId }),
412
+ this._request("PUT", `/api/v1/relations/${relationId}/source`, {
413
+ auditMessage,
414
+ body: bodyOf({ "span-id": spanId }),
319
415
  }),
320
416
  /**
321
417
  * Create a new relation. A relation is a directed edge between two spans
@@ -328,8 +424,15 @@ class PlaidClient {
328
424
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
329
425
  */
330
426
  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 }),
427
+ this._request("POST", "/api/v1/relations", {
428
+ auditMessage,
429
+ body: bodyOf({
430
+ "layer-id": layerId,
431
+ "source-id": sourceId,
432
+ "target-id": targetId,
433
+ value,
434
+ metadata,
435
+ }),
333
436
  }),
334
437
  /**
335
438
  * Create multiple relations in a single operation.
@@ -337,13 +440,16 @@ class PlaidClient {
337
440
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
338
441
  */
339
442
  bulkCreate: (body, auditMessage) =>
340
- this._request('POST', '/api/v1/relations/bulk', { auditMessage, body }),
443
+ this._request("POST", "/api/v1/relations/bulk", { auditMessage, body }),
341
444
  /**
342
445
  * Delete multiple relations in a single operation. Provide an array of IDs.
343
446
  * @param {Array} body - The request body
344
447
  */
345
448
  bulkDelete: (body, auditMessage) =>
346
- this._request('DELETE', '/api/v1/relations/bulk', { auditMessage, body }),
449
+ this._request("DELETE", "/api/v1/relations/bulk", {
450
+ auditMessage,
451
+ body,
452
+ }),
347
453
  };
348
454
 
349
455
  this.spanLayers = {
@@ -354,10 +460,18 @@ class PlaidClient {
354
460
  * @param {string} configKey - The config key
355
461
  * @param {any} configValue - Configuration value to set
356
462
  */
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
- }),
463
+ setConfig: (
464
+ spanLayerId,
465
+ namespace,
466
+ configKey,
467
+ configValue,
468
+ auditMessage,
469
+ ) =>
470
+ this._request(
471
+ "PUT",
472
+ `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
473
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
474
+ ),
361
475
  /**
362
476
  * Remove a configuration value for a layer.
363
477
  * @param {string} spanLayerId - The span layer ID
@@ -365,31 +479,36 @@ class PlaidClient {
365
479
  * @param {string} configKey - The config key
366
480
  */
367
481
  deleteConfig: (spanLayerId, namespace, configKey, auditMessage) =>
368
- this._request('DELETE', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, { auditMessage,
369
- skipResponseTransform: true,
370
- }),
482
+ this._request(
483
+ "DELETE",
484
+ `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
485
+ { auditMessage, skipResponseTransform: true },
486
+ ),
371
487
  /**
372
488
  * Get a span layer by ID.
373
489
  * @param {string} spanLayerId - The span layer ID
374
490
  * @param {string} [asOf] - Temporal query timestamp
375
491
  */
376
492
  get: (spanLayerId, asOf) =>
377
- this._request('GET', `/api/v1/span-layers/${spanLayerId}`, {
378
- queryParams: { 'as-of': asOf },
493
+ this._request("GET", `/api/v1/span-layers/${spanLayerId}`, {
494
+ queryParams: { "as-of": asOf },
379
495
  }),
380
496
  /**
381
497
  * Delete a span layer.
382
498
  * @param {string} spanLayerId - The span layer ID
383
499
  */
384
500
  delete: (spanLayerId, auditMessage) =>
385
- this._request('DELETE', `/api/v1/span-layers/${spanLayerId}`, { auditMessage }),
501
+ this._request("DELETE", `/api/v1/span-layers/${spanLayerId}`, {
502
+ auditMessage,
503
+ }),
386
504
  /**
387
505
  * Update a span layer's name.
388
506
  * @param {string} spanLayerId - The span layer ID
389
507
  * @param {string} name - The name
390
508
  */
391
509
  update: (spanLayerId, name, auditMessage) =>
392
- this._request('PATCH', `/api/v1/span-layers/${spanLayerId}`, { auditMessage,
510
+ this._request("PATCH", `/api/v1/span-layers/${spanLayerId}`, {
511
+ auditMessage,
393
512
  body: bodyOf({ name }),
394
513
  }),
395
514
  /**
@@ -398,8 +517,9 @@ class PlaidClient {
398
517
  * @param {string} name - The name
399
518
  */
400
519
  create: (tokenLayerId, name, auditMessage) =>
401
- this._request('POST', '/api/v1/span-layers', { auditMessage,
402
- body: bodyOf({ 'token-layer-id': tokenLayerId, name }),
520
+ this._request("POST", "/api/v1/span-layers", {
521
+ auditMessage,
522
+ body: bodyOf({ "token-layer-id": tokenLayerId, name }),
403
523
  }),
404
524
  /**
405
525
  * Shift a span layer's display order.
@@ -407,7 +527,8 @@ class PlaidClient {
407
527
  * @param {string} direction - The direction ("up" or "down")
408
528
  */
409
529
  shift: (spanLayerId, direction, auditMessage) =>
410
- this._request('POST', `/api/v1/span-layers/${spanLayerId}/shift`, { auditMessage,
530
+ this._request("POST", `/api/v1/span-layers/${spanLayerId}/shift`, {
531
+ auditMessage,
411
532
  body: bodyOf({ direction }),
412
533
  }),
413
534
  };
@@ -419,7 +540,8 @@ class PlaidClient {
419
540
  * @param {Array} tokens - The tokens
420
541
  */
421
542
  setTokens: (spanId, tokens, auditMessage) =>
422
- this._request('PUT', `/api/v1/spans/${spanId}/tokens`, { auditMessage,
543
+ this._request("PUT", `/api/v1/spans/${spanId}/tokens`, {
544
+ auditMessage,
423
545
  body: bodyOf({ tokens }),
424
546
  }),
425
547
  /**
@@ -431,8 +553,14 @@ class PlaidClient {
431
553
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
432
554
  */
433
555
  create: (spanLayerId, tokens, value, metadata, auditMessage) =>
434
- this._request('POST', '/api/v1/spans', { auditMessage,
435
- body: bodyOf({ 'span-layer-id': spanLayerId, tokens, value, metadata }),
556
+ this._request("POST", "/api/v1/spans", {
557
+ auditMessage,
558
+ body: bodyOf({
559
+ "span-layer-id": spanLayerId,
560
+ tokens,
561
+ value,
562
+ metadata,
563
+ }),
436
564
  }),
437
565
  /**
438
566
  * Get a span by ID.
@@ -440,22 +568,23 @@ class PlaidClient {
440
568
  * @param {string} [asOf] - Temporal query timestamp
441
569
  */
442
570
  get: (spanId, asOf) =>
443
- this._request('GET', `/api/v1/spans/${spanId}`, {
444
- queryParams: { 'as-of': asOf },
571
+ this._request("GET", `/api/v1/spans/${spanId}`, {
572
+ queryParams: { "as-of": asOf },
445
573
  }),
446
574
  /**
447
575
  * Delete a span.
448
576
  * @param {string} spanId - The span ID
449
577
  */
450
578
  delete: (spanId, auditMessage) =>
451
- this._request('DELETE', `/api/v1/spans/${spanId}`, { auditMessage }),
579
+ this._request("DELETE", `/api/v1/spans/${spanId}`, { auditMessage }),
452
580
  /**
453
581
  * Update a span's value.
454
582
  * @param {string} spanId - The span ID
455
583
  * @param {any} value - The value
456
584
  */
457
585
  update: (spanId, value, auditMessage) =>
458
- this._request('PATCH', `/api/v1/spans/${spanId}`, { auditMessage,
586
+ this._request("PATCH", `/api/v1/spans/${spanId}`, {
587
+ auditMessage,
459
588
  body: bodyOf({ value }),
460
589
  }),
461
590
  /**
@@ -464,28 +593,31 @@ class PlaidClient {
464
593
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
465
594
  */
466
595
  bulkCreate: (body, auditMessage) =>
467
- this._request('POST', '/api/v1/spans/bulk', { auditMessage, body }),
596
+ this._request("POST", "/api/v1/spans/bulk", { auditMessage, body }),
468
597
  /**
469
598
  * Delete multiple spans in a single operation. Provide an array of IDs.
470
599
  * @param {Array} body - The request body
471
600
  */
472
601
  bulkDelete: (body, auditMessage) =>
473
- this._request('DELETE', '/api/v1/spans/bulk', { auditMessage, body }),
602
+ this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
474
603
  /**
475
604
  * Replace all metadata for a span.
476
605
  * @param {string} spanId - The span ID
477
606
  * @param {any} body - The request body
478
607
  */
479
608
  setMetadata: (spanId, body, auditMessage) =>
480
- this._request('PUT', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
481
- rawBody: body, skipResponseTransform: true,
609
+ this._request("PUT", `/api/v1/spans/${spanId}/metadata`, {
610
+ auditMessage,
611
+ rawBody: body,
612
+ skipResponseTransform: true,
482
613
  }),
483
614
  /**
484
615
  * Remove all metadata from a span.
485
616
  * @param {string} spanId - The span ID
486
617
  */
487
618
  deleteMetadata: (spanId, auditMessage) =>
488
- this._request('DELETE', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
619
+ this._request("DELETE", `/api/v1/spans/${spanId}/metadata`, {
620
+ auditMessage,
489
621
  skipResponseTransform: true,
490
622
  }),
491
623
  /**
@@ -494,8 +626,10 @@ class PlaidClient {
494
626
  * @param {any} body - The metadata patch
495
627
  */
496
628
  patchMetadata: (spanId, body, auditMessage) =>
497
- this._request('PATCH', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
498
- rawBody: body, skipResponseTransform: true,
629
+ this._request("PATCH", `/api/v1/spans/${spanId}/metadata`, {
630
+ auditMessage,
631
+ rawBody: body,
632
+ skipResponseTransform: true,
499
633
  }),
500
634
  };
501
635
 
@@ -506,8 +640,10 @@ class PlaidClient {
506
640
  * @param {Array} body - The request body
507
641
  */
508
642
  submit: (body, auditMessage) =>
509
- this._request('POST', '/api/v1/batch', { auditMessage,
510
- body, noBatch: true,
643
+ this._request("POST", "/api/v1/batch", {
644
+ auditMessage,
645
+ body,
646
+ noBatch: true,
511
647
  }),
512
648
  };
513
649
 
@@ -518,15 +654,18 @@ class PlaidClient {
518
654
  * @param {any} body - The request body
519
655
  */
520
656
  setMetadata: (textId, body, auditMessage) =>
521
- this._request('PUT', `/api/v1/texts/${textId}/metadata`, { auditMessage,
522
- rawBody: body, skipResponseTransform: true,
657
+ this._request("PUT", `/api/v1/texts/${textId}/metadata`, {
658
+ auditMessage,
659
+ rawBody: body,
660
+ skipResponseTransform: true,
523
661
  }),
524
662
  /**
525
663
  * Remove all metadata from a text.
526
664
  * @param {string} textId - The text ID
527
665
  */
528
666
  deleteMetadata: (textId, auditMessage) =>
529
- this._request('DELETE', `/api/v1/texts/${textId}/metadata`, { auditMessage,
667
+ this._request("DELETE", `/api/v1/texts/${textId}/metadata`, {
668
+ auditMessage,
530
669
  skipResponseTransform: true,
531
670
  }),
532
671
  /**
@@ -535,8 +674,10 @@ class PlaidClient {
535
674
  * @param {any} body - The metadata patch
536
675
  */
537
676
  patchMetadata: (textId, body, auditMessage) =>
538
- this._request('PATCH', `/api/v1/texts/${textId}/metadata`, { auditMessage,
539
- rawBody: body, skipResponseTransform: true,
677
+ this._request("PATCH", `/api/v1/texts/${textId}/metadata`, {
678
+ auditMessage,
679
+ rawBody: body,
680
+ skipResponseTransform: true,
540
681
  }),
541
682
  /**
542
683
  * Create a new text in a document's text layer. A text is a container for
@@ -547,8 +688,14 @@ class PlaidClient {
547
688
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
548
689
  */
549
690
  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 }),
691
+ this._request("POST", "/api/v1/texts", {
692
+ auditMessage,
693
+ body: bodyOf({
694
+ "text-layer-id": textLayerId,
695
+ "document-id": documentId,
696
+ body,
697
+ metadata,
698
+ }),
552
699
  }),
553
700
  /**
554
701
  * Get a text.
@@ -556,24 +703,30 @@ class PlaidClient {
556
703
  * @param {string} [asOf] - Temporal query timestamp
557
704
  */
558
705
  get: (textId, asOf) =>
559
- this._request('GET', `/api/v1/texts/${textId}`, {
560
- queryParams: { 'as-of': asOf },
706
+ this._request("GET", `/api/v1/texts/${textId}`, {
707
+ queryParams: { "as-of": asOf },
561
708
  }),
562
709
  /**
563
710
  * Delete a text and all dependent data.
564
711
  * @param {string} textId - The text ID
565
712
  */
566
713
  delete: (textId, auditMessage) =>
567
- this._request('DELETE', `/api/v1/texts/${textId}`, { auditMessage }),
714
+ this._request("DELETE", `/api/v1/texts/${textId}`, { auditMessage }),
568
715
  /**
569
716
  * Update a text's body. A diff is computed and token indices are updated
570
717
  * so that tokens remain intact. Alternatively, `body` can be a list of
571
- * edit directives.
718
+ * edit directives (code-point indices, applied in order):
719
+ * {type: 'delete', index, value: count}
720
+ * {type: 'insert', index, value: string}
721
+ * {type: 'replace', index, length, value: string} — unlike
722
+ * delete+insert, a token covering the whole range is resized to keep
723
+ * it (respell a word in place without losing its annotations).
572
724
  * @param {string} textId - The text ID
573
725
  * @param {any} body - The request body
574
726
  */
575
727
  update: (textId, body, auditMessage) =>
576
- this._request('PATCH', `/api/v1/texts/${textId}`, { auditMessage,
728
+ this._request("PATCH", `/api/v1/texts/${textId}`, {
729
+ auditMessage,
577
730
  body: bodyOf({ body }),
578
731
  }),
579
732
  };
@@ -584,42 +737,58 @@ class PlaidClient {
584
737
  * returns the full flat array. Admin-or-maintainer only.
585
738
  * 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
739
  * @param {object} [opts]
587
- * @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
740
+ * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
588
741
  * @param {string} [opts.asOf] - Temporal query timestamp
589
742
  */
590
743
  list: ({ q, asOf } = {}) =>
591
- listAll(this, '/api/v1/users', { query: { q, 'as-of': asOf } }),
744
+ listAll(this, "/api/v1/users", { query: { q, "as-of": asOf } }),
592
745
  /**
593
746
  * Fetch a single page of users (optionally filtered by `q`).
594
747
  * @param {object} [opts]
595
- * @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
748
+ * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
596
749
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
597
750
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
598
751
  * @param {string} [opts.asOf] - Temporal query timestamp
599
752
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
600
753
  */
601
754
  listPage: ({ q, limit, cursor, asOf } = {}) =>
602
- listPage(this, '/api/v1/users', { limit, cursor, query: { q, 'as-of': asOf } }),
755
+ listPage(this, "/api/v1/users", {
756
+ limit,
757
+ cursor,
758
+ query: { q, "as-of": asOf },
759
+ }),
603
760
  /**
604
761
  * Async-iterate users page by page; yields each page's entries array.
605
762
  * @param {object} [opts]
606
- * @param {string} [opts.q] - Filter to usernames containing this text (case-insensitive)
763
+ * @param {string} [opts.q] - Filter to users whose display name or email contains this text (case-insensitive)
607
764
  * @param {number} [opts.pageSize] - Per-request page size
608
765
  * @param {string} [opts.asOf] - Temporal query timestamp
609
766
  * 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
767
  * @returns {AsyncGenerator<Array>}
611
768
  */
612
769
  iterPages: ({ q, pageSize, asOf } = {}) =>
613
- iterPages(this, '/api/v1/users', { pageSize, query: { q, 'as-of': asOf } }),
770
+ iterPages(this, "/api/v1/users", {
771
+ pageSize,
772
+ query: { q, "as-of": asOf },
773
+ }),
614
774
  /**
615
- * Create a new user
616
- * @param {string} username - The username
775
+ * Create a new user.
776
+ * @param {string} email - The account's email address. It becomes the
777
+ * user's id and is what they log in with; it can never be changed.
617
778
  * @param {string} password - The password
618
779
  * @param {boolean} isAdmin - Whether the user is an admin
780
+ * @param {string} [displayName] - How the user is shown in the UI.
781
+ * Defaults to the local part of the email.
619
782
  */
620
- create: (username, password, isAdmin, auditMessage) =>
621
- this._request('POST', '/api/v1/users', { auditMessage,
622
- body: bodyOf({ username, password, 'is-admin': isAdmin }),
783
+ create: (email, password, isAdmin, displayName, auditMessage) =>
784
+ this._request("POST", "/api/v1/users", {
785
+ auditMessage,
786
+ body: bodyOf({
787
+ email,
788
+ password,
789
+ "is-admin": isAdmin,
790
+ "display-name": displayName,
791
+ }),
623
792
  }),
624
793
  /**
625
794
  * Get audit log for a user's actions. Transparently follows pagination
@@ -629,10 +798,19 @@ class PlaidClient {
629
798
  * @param {string} [startTime] - Start of time range
630
799
  * @param {string} [endTime] - End of time range
631
800
  * @param {string} [asOf] - Temporal query timestamp
801
+ * @param {string[]|string} [opTypes] - Only return operations of these
802
+ * types, spelled as in an entry's `op/type` (e.g.
803
+ * `['span-layer/create', 'span-layer/delete']`). An entry appears when
804
+ * one of its operations matches, carrying only the ones that did.
632
805
  */
633
- audit: (userId, startTime, endTime, asOf) =>
806
+ audit: (userId, startTime, endTime, asOf, opTypes) =>
634
807
  listAll(this, `/api/v1/users/${userId}/audit`, {
635
- query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
808
+ query: {
809
+ "start-time": startTime,
810
+ "end-time": endTime,
811
+ "as-of": asOf,
812
+ "op-types": opTypesParam(opTypes),
813
+ },
636
814
  }),
637
815
  /**
638
816
  * Get a user by ID
@@ -640,8 +818,8 @@ class PlaidClient {
640
818
  * @param {string} [asOf] - Temporal query timestamp
641
819
  */
642
820
  get: (id, asOf) =>
643
- this._request('GET', `/api/v1/users/${id}`, {
644
- queryParams: { 'as-of': asOf },
821
+ this._request("GET", `/api/v1/users/${id}`, {
822
+ queryParams: { "as-of": asOf },
645
823
  }),
646
824
  /**
647
825
  * Deactivate a user. Users are never hard-deleted: deactivation
@@ -652,7 +830,7 @@ class PlaidClient {
652
830
  * @param {string} id - The resource ID
653
831
  */
654
832
  delete: (id, auditMessage) =>
655
- this._request('DELETE', `/api/v1/users/${id}`, { auditMessage }),
833
+ this._request("DELETE", `/api/v1/users/${id}`, { auditMessage }),
656
834
  /**
657
835
  * Reactivate a deactivated user, restoring their ability to log in.
658
836
  * Project memberships, vocab maintainerships, and API tokens removed
@@ -660,19 +838,27 @@ class PlaidClient {
660
838
  * @param {string} id - The resource ID
661
839
  */
662
840
  activate: (id, auditMessage) =>
663
- this._request('POST', `/api/v1/users/${id}/activate`, { auditMessage }),
841
+ this._request("POST", `/api/v1/users/${id}/activate`, { auditMessage }),
664
842
  /**
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
843
+ * Modify a user. Admins may change the display name, password, and admin
844
+ * status of any user. All other users may only modify their own display
845
+ * name or password.
846
+ *
847
+ * A user's id is their email address and is fixed for the life of the
848
+ * account — it is what they log in with, so nothing can change it.
849
+ * @param {string} id - The resource ID (the user's email address)
669
850
  * @param {string} [password] - New password
670
- * @param {string} [username] - New username
851
+ * @param {string} [displayName] - New display name
671
852
  * @param {boolean} [isAdmin] - New admin status
672
853
  */
673
- update: (id, password, username, isAdmin, auditMessage) =>
674
- this._request('PATCH', `/api/v1/users/${id}`, { auditMessage,
675
- body: bodyOf({ password, username, 'is-admin': isAdmin }),
854
+ update: (id, password, displayName, isAdmin, auditMessage) =>
855
+ this._request("PATCH", `/api/v1/users/${id}`, {
856
+ auditMessage,
857
+ body: bodyOf({
858
+ password,
859
+ "display-name": displayName,
860
+ "is-admin": isAdmin,
861
+ }),
676
862
  }),
677
863
  /**
678
864
  * Build a URL for a user's profile picture, suitable for use directly as
@@ -692,7 +878,7 @@ class PlaidClient {
692
878
  avatarUrl: (id, avatarHash) => {
693
879
  if (avatarHash === null) return null;
694
880
  const params = new URLSearchParams({ token: this.token });
695
- if (avatarHash) params.set('v', avatarHash);
881
+ if (avatarHash) params.set("v", avatarHash);
696
882
  return `${this.baseUrl}/api/v1/users/${id}/avatar?${params}`;
697
883
  },
698
884
  /**
@@ -701,7 +887,7 @@ class PlaidClient {
701
887
  * @param {string} id - The user ID
702
888
  */
703
889
  getAvatar: (id) =>
704
- this._request('GET', `/api/v1/users/${id}/avatar`, {
890
+ this._request("GET", `/api/v1/users/${id}/avatar`, {
705
891
  noBatch: true,
706
892
  binaryResponse: true,
707
893
  }),
@@ -715,9 +901,12 @@ class PlaidClient {
715
901
  */
716
902
  setAvatar: (id, file, auditMessage) => {
717
903
  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,
904
+ fd.append("file", file);
905
+ return this._request("PUT", `/api/v1/users/${id}/avatar`, {
906
+ auditMessage,
907
+ body: fd,
908
+ formData: true,
909
+ noBatch: true,
721
910
  });
722
911
  },
723
912
  /**
@@ -725,11 +914,72 @@ class PlaidClient {
725
914
  * @param {string} id - The user ID
726
915
  */
727
916
  deleteAvatar: (id, auditMessage) =>
728
- this._request('DELETE', `/api/v1/users/${id}/avatar`, { auditMessage,
917
+ this._request("DELETE", `/api/v1/users/${id}/avatar`, {
918
+ auditMessage,
729
919
  noBatch: true,
730
920
  }),
731
921
  };
732
922
 
923
+ this.userData = {
924
+ /**
925
+ * List a user's private data entries ({key, updatedAt}, plus value when
926
+ * includeValues). Owner or admin only.
927
+ * @param {string} userId
928
+ * @param {object} [opts]
929
+ * @param {string} [opts.prefix] - Only keys starting with this prefix
930
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
931
+ */
932
+ list: (userId, { prefix, includeValues } = {}) =>
933
+ this._request("GET", `/api/v1/users/${userId}/data`, {
934
+ queryParams: { prefix, "include-values": includeValues },
935
+ }),
936
+ /**
937
+ * Read one private data entry ({key, updatedAt, value}); 404 if absent.
938
+ * @param {string} userId
939
+ * @param {string} key
940
+ */
941
+ get: (userId, key) =>
942
+ this._request(
943
+ "GET",
944
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
945
+ ),
946
+ /**
947
+ * Create or replace one private data entry. `value` is any JSON (up to
948
+ * 1 MB), stored as sent. Not audited, not batchable.
949
+ *
950
+ * The server stores it verbatim, but this client recases object keys on
951
+ * the way out and back like any other body (`myKey` <-> `my-key`), so a
952
+ * value whose keys are camelCase round-trips unchanged while one keyed
953
+ * by arbitrary strings does not. Put such a map under a `metadata` key,
954
+ * which both clients pass through untouched.
955
+ * @param {string} userId
956
+ * @param {string} key
957
+ * @param {*} value
958
+ */
959
+ put: (userId, key, value) =>
960
+ this._request(
961
+ "PUT",
962
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
963
+ {
964
+ body: value,
965
+ noBatch: true,
966
+ },
967
+ ),
968
+ /**
969
+ * Delete one private data entry; 404 if absent.
970
+ * @param {string} userId
971
+ * @param {string} key
972
+ */
973
+ delete: (userId, key) =>
974
+ this._request(
975
+ "DELETE",
976
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
977
+ {
978
+ noBatch: true,
979
+ },
980
+ ),
981
+ };
982
+
733
983
  this.apiTokens = {
734
984
  /**
735
985
  * List a user's named API tokens. Never includes the signed token
@@ -738,8 +988,7 @@ class PlaidClient {
738
988
  * 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
989
  * @param {string} userId - The user ID who owns the tokens
740
990
  */
741
- list: (userId) =>
742
- listAll(this, `/api/v1/users/${userId}/tokens`),
991
+ list: (userId) => listAll(this, `/api/v1/users/${userId}/tokens`),
743
992
  /**
744
993
  * Fetch a single page of a user's named API tokens.
745
994
  * @param {string} userId - The user ID who owns the tokens
@@ -770,7 +1019,8 @@ class PlaidClient {
770
1019
  * @returns {Promise<{id: string, name: string, token: string}>}
771
1020
  */
772
1021
  create: (userId, name, auditMessage) =>
773
- this._request('POST', `/api/v1/users/${userId}/tokens`, { auditMessage,
1022
+ this._request("POST", `/api/v1/users/${userId}/tokens`, {
1023
+ auditMessage,
774
1024
  body: bodyOf({ name }),
775
1025
  }),
776
1026
  /**
@@ -779,7 +1029,9 @@ class PlaidClient {
779
1029
  * @param {string} tokenId - The token ID to revoke
780
1030
  */
781
1031
  revoke: (userId, tokenId, auditMessage) =>
782
- this._request('DELETE', `/api/v1/users/${userId}/tokens/${tokenId}`, { auditMessage }),
1032
+ this._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
1033
+ auditMessage,
1034
+ }),
783
1035
  };
784
1036
 
785
1037
  this.invites = {
@@ -794,7 +1046,9 @@ class PlaidClient {
794
1046
  * @param {string} [opts.projectId] - List this project's invites instead of your own
795
1047
  */
796
1048
  list: ({ projectId } = {}) =>
797
- listAll(this, '/api/v1/invites', { query: { 'project-id': projectId } }),
1049
+ listAll(this, "/api/v1/invites", {
1050
+ query: { "project-id": projectId },
1051
+ }),
798
1052
  /**
799
1053
  * Fetch a single page of invites.
800
1054
  * @param {object} [opts]
@@ -804,7 +1058,11 @@ class PlaidClient {
804
1058
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
805
1059
  */
806
1060
  listPage: ({ projectId, limit, cursor } = {}) =>
807
- listPage(this, '/api/v1/invites', { limit, cursor, query: { 'project-id': projectId } }),
1061
+ listPage(this, "/api/v1/invites", {
1062
+ limit,
1063
+ cursor,
1064
+ query: { "project-id": projectId },
1065
+ }),
808
1066
  /**
809
1067
  * Async-iterate invites page by page; yields each page's entries array.
810
1068
  * @param {object} [opts]
@@ -814,7 +1072,10 @@ class PlaidClient {
814
1072
  * @returns {AsyncGenerator<Array>}
815
1073
  */
816
1074
  iterPages: ({ projectId, pageSize } = {}) =>
817
- iterPages(this, '/api/v1/invites', { pageSize, query: { 'project-id': projectId } }),
1075
+ iterPages(this, "/api/v1/invites", {
1076
+ pageSize,
1077
+ query: { "project-id": projectId },
1078
+ }),
818
1079
  /**
819
1080
  * Mint an invite. The returned `code` is shown ONLY here — it is never
820
1081
  * stored and cannot be recovered, so build and hand off the link now.
@@ -822,23 +1083,46 @@ class PlaidClient {
822
1083
  *
823
1084
  * Admins may mint anything. A project maintainer may mint role grants on
824
1085
  * 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"
1086
+ * invite, no password resets — so for a non-admin, projectId/projectRole
1087
+ * are required in practice (403 without them).
1088
+ *
1089
+ * EVERY option is optional; `create()` with no arguments mints a
1090
+ * single-use signup link granting nothing but an account. Two pairing
1091
+ * rules the server enforces with a 400: projectId and projectRole must be
1092
+ * given TOGETHER, and targetUserId may not be combined with projectId,
1093
+ * grantAdmin, or a maxUses above 1.
1094
+ * @param {object} [opts]
1095
+ * @param {string} [opts.projectId] - Project the redeemer joins (requires projectRole)
1096
+ * @param {string} [opts.projectRole] - "reader" | "writer" | "maintainer" (requires projectId)
829
1097
  * @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)
1098
+ * @param {string} [opts.targetUserId] - Password reset for that user instead of a signup; admin only, single-use, grants nothing
831
1099
  * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
832
1100
  * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
833
1101
  * @param {string} [opts.note] - Human label shown in your invite list
834
1102
  * @returns {Promise<{id: string, code: string, kind: string, status: string}>}
835
1103
  */
836
- create: ({ projectId, projectRole, grantAdmin, targetUserId, maxUses, ttlDays, note } = {}, auditMessage) =>
837
- this._request('POST', '/api/v1/invites', { auditMessage,
1104
+ create: (
1105
+ {
1106
+ projectId,
1107
+ projectRole,
1108
+ grantAdmin,
1109
+ targetUserId,
1110
+ maxUses,
1111
+ ttlDays,
1112
+ note,
1113
+ } = {},
1114
+ auditMessage,
1115
+ ) =>
1116
+ this._request("POST", "/api/v1/invites", {
1117
+ auditMessage,
838
1118
  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,
1119
+ "project-id": projectId,
1120
+ "project-role": projectRole,
1121
+ "grant-admin": grantAdmin,
1122
+ "target-user-id": targetUserId,
1123
+ "max-uses": maxUses,
1124
+ "ttl-days": ttlDays,
1125
+ note,
842
1126
  }),
843
1127
  }),
844
1128
  /**
@@ -847,7 +1131,7 @@ class PlaidClient {
847
1131
  * @param {string} id - The invite ID
848
1132
  */
849
1133
  revoke: (id, auditMessage) =>
850
- this._request('DELETE', `/api/v1/invites/${id}`, { auditMessage }),
1134
+ this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
851
1135
  };
852
1136
 
853
1137
  this.tokenLayers = {
@@ -857,7 +1141,8 @@ class PlaidClient {
857
1141
  * @param {string} direction - The direction ("up" or "down")
858
1142
  */
859
1143
  shift: (tokenLayerId, direction, auditMessage) =>
860
- this._request('POST', `/api/v1/token-layers/${tokenLayerId}/shift`, { auditMessage,
1144
+ this._request("POST", `/api/v1/token-layers/${tokenLayerId}/shift`, {
1145
+ auditMessage,
861
1146
  body: bodyOf({ direction }),
862
1147
  }),
863
1148
  /**
@@ -867,9 +1152,21 @@ class PlaidClient {
867
1152
  * @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
1153
  * @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
1154
  */
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 }),
1155
+ create: (
1156
+ textLayerId,
1157
+ name,
1158
+ overlapMode,
1159
+ parentTokenLayerId,
1160
+ auditMessage,
1161
+ ) =>
1162
+ this._request("POST", "/api/v1/token-layers", {
1163
+ auditMessage,
1164
+ body: bodyOf({
1165
+ "text-layer-id": textLayerId,
1166
+ name,
1167
+ "overlap-mode": overlapMode,
1168
+ "parent-token-layer-id": parentTokenLayerId,
1169
+ }),
873
1170
  }),
874
1171
  /**
875
1172
  * Set a configuration value for a layer in an editor namespace.
@@ -878,10 +1175,18 @@ class PlaidClient {
878
1175
  * @param {string} configKey - The config key
879
1176
  * @param {any} configValue - Configuration value to set
880
1177
  */
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
- }),
1178
+ setConfig: (
1179
+ tokenLayerId,
1180
+ namespace,
1181
+ configKey,
1182
+ configValue,
1183
+ auditMessage,
1184
+ ) =>
1185
+ this._request(
1186
+ "PUT",
1187
+ `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
1188
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
1189
+ ),
885
1190
  /**
886
1191
  * Remove a configuration value for a layer.
887
1192
  * @param {string} tokenLayerId - The token layer ID
@@ -889,31 +1194,36 @@ class PlaidClient {
889
1194
  * @param {string} configKey - The config key
890
1195
  */
891
1196
  deleteConfig: (tokenLayerId, namespace, configKey, auditMessage) =>
892
- this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, { auditMessage,
893
- skipResponseTransform: true,
894
- }),
1197
+ this._request(
1198
+ "DELETE",
1199
+ `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
1200
+ { auditMessage, skipResponseTransform: true },
1201
+ ),
895
1202
  /**
896
1203
  * Get a token layer by ID.
897
1204
  * @param {string} tokenLayerId - The token layer ID
898
1205
  * @param {string} [asOf] - Temporal query timestamp
899
1206
  */
900
1207
  get: (tokenLayerId, asOf) =>
901
- this._request('GET', `/api/v1/token-layers/${tokenLayerId}`, {
902
- queryParams: { 'as-of': asOf },
1208
+ this._request("GET", `/api/v1/token-layers/${tokenLayerId}`, {
1209
+ queryParams: { "as-of": asOf },
903
1210
  }),
904
1211
  /**
905
1212
  * Delete a token layer.
906
1213
  * @param {string} tokenLayerId - The token layer ID
907
1214
  */
908
1215
  delete: (tokenLayerId, auditMessage) =>
909
- this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}`, { auditMessage }),
1216
+ this._request("DELETE", `/api/v1/token-layers/${tokenLayerId}`, {
1217
+ auditMessage,
1218
+ }),
910
1219
  /**
911
1220
  * Update a token layer's name.
912
1221
  * @param {string} tokenLayerId - The token layer ID
913
1222
  * @param {string} name - The name
914
1223
  */
915
1224
  update: (tokenLayerId, name, auditMessage) =>
916
- this._request('PATCH', `/api/v1/token-layers/${tokenLayerId}`, { auditMessage,
1225
+ this._request("PATCH", `/api/v1/token-layers/${tokenLayerId}`, {
1226
+ auditMessage,
917
1227
  body: bodyOf({ name }),
918
1228
  }),
919
1229
  };
@@ -925,29 +1235,33 @@ class PlaidClient {
925
1235
  * @param {string} [asOf] - Temporal query timestamp
926
1236
  */
927
1237
  checkLock: (documentId, asOf) =>
928
- this._request('GET', `/api/v1/documents/${documentId}/lock`, {
929
- queryParams: { 'as-of': asOf },
1238
+ this._request("GET", `/api/v1/documents/${documentId}/lock`, {
1239
+ queryParams: { "as-of": asOf },
930
1240
  }),
931
1241
  /**
932
1242
  * Acquire or refresh a document lock
933
1243
  * @param {string} documentId - The document ID
934
1244
  */
935
1245
  acquireLock: (documentId, auditMessage) =>
936
- this._request('POST', `/api/v1/documents/${documentId}/lock`, { auditMessage }),
1246
+ this._request("POST", `/api/v1/documents/${documentId}/lock`, {
1247
+ auditMessage,
1248
+ }),
937
1249
  /**
938
1250
  * Release a document lock
939
1251
  * @param {string} documentId - The document ID
940
1252
  */
941
1253
  releaseLock: (documentId, auditMessage) =>
942
- this._request('DELETE', `/api/v1/documents/${documentId}/lock`, { auditMessage }),
1254
+ this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
1255
+ auditMessage,
1256
+ }),
943
1257
  /**
944
- * Get media file for a document
1258
+ * Get the media file for a document. Media is not versioned, so there is
1259
+ * no as-of form: the route refuses the parameter. Prefer the document's
1260
+ * own `mediaUrl`, which carries the file's version for caching.
945
1261
  * @param {string} documentId - The document ID
946
- * @param {string} [asOf] - Temporal query timestamp
947
1262
  */
948
- getMedia: (documentId, asOf) =>
949
- this._request('GET', `/api/v1/documents/${documentId}/media`, {
950
- queryParams: { 'as-of': asOf },
1263
+ getMedia: (documentId) =>
1264
+ this._request("GET", `/api/v1/documents/${documentId}/media`, {
951
1265
  noBatch: true,
952
1266
  binaryResponse: true,
953
1267
  }),
@@ -955,12 +1269,18 @@ class PlaidClient {
955
1269
  * Upload a media file for a document. Uses Apache Tika for content validation.
956
1270
  * @param {string} documentId - The document ID
957
1271
  * @param {File} file - The file to upload
1272
+ * @param {string} [auditMessage] - Custom audit-log message for this write
1273
+ * @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
1274
  */
959
- uploadMedia: (documentId, file, auditMessage) => {
1275
+ uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
960
1276
  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,
1277
+ fd.append("file", file);
1278
+ return this._request("PUT", `/api/v1/documents/${documentId}/media`, {
1279
+ auditMessage,
1280
+ body: fd,
1281
+ formData: true,
1282
+ noBatch: true,
1283
+ onUploadProgress: onProgress,
964
1284
  });
965
1285
  },
966
1286
  /**
@@ -968,7 +1288,8 @@ class PlaidClient {
968
1288
  * @param {string} documentId - The document ID
969
1289
  */
970
1290
  deleteMedia: (documentId, auditMessage) =>
971
- this._request('DELETE', `/api/v1/documents/${documentId}/media`, { auditMessage,
1291
+ this._request("DELETE", `/api/v1/documents/${documentId}/media`, {
1292
+ auditMessage,
972
1293
  noBatch: true,
973
1294
  }),
974
1295
  /**
@@ -977,15 +1298,18 @@ class PlaidClient {
977
1298
  * @param {any} body - The request body
978
1299
  */
979
1300
  setMetadata: (documentId, body, auditMessage) =>
980
- this._request('PUT', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
981
- rawBody: body, skipResponseTransform: true,
1301
+ this._request("PUT", `/api/v1/documents/${documentId}/metadata`, {
1302
+ auditMessage,
1303
+ rawBody: body,
1304
+ skipResponseTransform: true,
982
1305
  }),
983
1306
  /**
984
1307
  * Remove all metadata from a document.
985
1308
  * @param {string} documentId - The document ID
986
1309
  */
987
1310
  deleteMetadata: (documentId, auditMessage) =>
988
- this._request('DELETE', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
1311
+ this._request("DELETE", `/api/v1/documents/${documentId}/metadata`, {
1312
+ auditMessage,
989
1313
  skipResponseTransform: true,
990
1314
  }),
991
1315
  /**
@@ -994,8 +1318,10 @@ class PlaidClient {
994
1318
  * @param {any} body - The metadata patch
995
1319
  */
996
1320
  patchMetadata: (documentId, body, auditMessage) =>
997
- this._request('PATCH', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
998
- rawBody: body, skipResponseTransform: true,
1321
+ this._request("PATCH", `/api/v1/documents/${documentId}/metadata`, {
1322
+ auditMessage,
1323
+ rawBody: body,
1324
+ skipResponseTransform: true,
999
1325
  }),
1000
1326
  /**
1001
1327
  * Get audit log for a document. Transparently follows pagination cursors
@@ -1005,34 +1331,78 @@ class PlaidClient {
1005
1331
  * @param {string} [startTime] - Start of time range
1006
1332
  * @param {string} [endTime] - End of time range
1007
1333
  * @param {string} [asOf] - Temporal query timestamp
1334
+ * @param {string[]|string} [opTypes] - Only return operations of these
1335
+ * types, spelled as in an entry's `op/type` (e.g.
1336
+ * `['span-layer/create', 'span-layer/delete']`). An entry appears when
1337
+ * one of its operations matches, carrying only the ones that did.
1008
1338
  */
1009
- audit: (documentId, startTime, endTime, asOf) =>
1339
+ audit: (documentId, startTime, endTime, asOf, opTypes) =>
1010
1340
  listAll(this, `/api/v1/documents/${documentId}/audit`, {
1011
- query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
1341
+ query: {
1342
+ "start-time": startTime,
1343
+ "end-time": endTime,
1344
+ "as-of": asOf,
1345
+ "op-types": opTypesParam(opTypes),
1346
+ },
1347
+ }),
1348
+ /**
1349
+ * Restore a document to its state at an earlier time, as one
1350
+ * operation: what was deleted since then comes back under its original
1351
+ * id, what was added since is removed, and what changed is set back,
1352
+ * across every layer. A layer deleted since then, or a vocabulary entry
1353
+ * that no longer exists, is skipped and reported under `skipped`.
1354
+ * Resolves to a summary of the changes. Maintainers only.
1355
+ * @param {string} documentId - The document ID
1356
+ * @param {string} asOf - The moment to go back to (ISO-8601 instant),
1357
+ * typically a history entry's `endTime`
1358
+ * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
1359
+ * is written and the summary says what would change
1360
+ * @param {string} [auditMessage] - Custom audit message for this operation
1361
+ */
1362
+ restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
1363
+ this._request("POST", `/api/v1/documents/${documentId}/restore`, {
1364
+ queryParams: { "as-of": asOf, "dry-run": dryRun ? "true" : undefined },
1365
+ auditMessage,
1012
1366
  }),
1013
1367
  /**
1014
1368
  * Get a document. Set `includeBody` to true to include all data.
1369
+ *
1370
+ * `layers` narrows a body read to the layers you name (ids of any kind:
1371
+ * text, token, span, or relation). A layer comes back when it is named
1372
+ * or is an ancestor of a named layer, and carries its own
1373
+ * texts/tokens/spans/relations/vocabs only when it is itself named — so
1374
+ * name the text layer too if you also want the text body. An id that is
1375
+ * not a layer of this document's project is an error, not a quietly
1376
+ * smaller response. Requires `includeBody`.
1015
1377
  * @param {string} documentId - The document ID
1016
1378
  * @param {boolean} [includeBody] - Include document body data
1017
1379
  * @param {string} [asOf] - Temporal query timestamp
1380
+ * @param {string[]|string} [layers] - Layer ids to restrict a body read to
1018
1381
  */
1019
- get: (documentId, includeBody, asOf) =>
1020
- this._request('GET', `/api/v1/documents/${documentId}`, {
1021
- queryParams: { 'include-body': includeBody, 'as-of': asOf },
1382
+ get: (documentId, includeBody, asOf, layers) =>
1383
+ this._request("GET", `/api/v1/documents/${documentId}`, {
1384
+ queryParams: {
1385
+ "include-body": includeBody,
1386
+ "as-of": asOf,
1387
+ layers: layersParam(layers),
1388
+ },
1022
1389
  }),
1023
1390
  /**
1024
1391
  * Delete a document and all data contained.
1025
1392
  * @param {string} documentId - The document ID
1026
1393
  */
1027
1394
  delete: (documentId, auditMessage) =>
1028
- this._request('DELETE', `/api/v1/documents/${documentId}`, { auditMessage }),
1395
+ this._request("DELETE", `/api/v1/documents/${documentId}`, {
1396
+ auditMessage,
1397
+ }),
1029
1398
  /**
1030
1399
  * Update a document's name.
1031
1400
  * @param {string} documentId - The document ID
1032
1401
  * @param {string} name - The name
1033
1402
  */
1034
1403
  update: (documentId, name, auditMessage) =>
1035
- this._request('PATCH', `/api/v1/documents/${documentId}`, { auditMessage,
1404
+ this._request("PATCH", `/api/v1/documents/${documentId}`, {
1405
+ auditMessage,
1036
1406
  body: bodyOf({ name }),
1037
1407
  }),
1038
1408
  /**
@@ -1042,8 +1412,9 @@ class PlaidClient {
1042
1412
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1043
1413
  */
1044
1414
  create: (projectId, name, metadata, auditMessage) =>
1045
- this._request('POST', '/api/v1/documents', { auditMessage,
1046
- body: bodyOf({ 'project-id': projectId, name, metadata }),
1415
+ this._request("POST", "/api/v1/documents", {
1416
+ auditMessage,
1417
+ body: bodyOf({ "project-id": projectId, name, metadata }),
1047
1418
  }),
1048
1419
  };
1049
1420
 
@@ -1054,28 +1425,36 @@ class PlaidClient {
1054
1425
  * @param {string} userId - The user ID
1055
1426
  */
1056
1427
  addWriter: (id, userId, auditMessage) =>
1057
- this._request('POST', `/api/v1/projects/${id}/writers/${userId}`, { auditMessage }),
1428
+ this._request("POST", `/api/v1/projects/${id}/writers/${userId}`, {
1429
+ auditMessage,
1430
+ }),
1058
1431
  /**
1059
1432
  * Remove a user's writer privileges for this project.
1060
1433
  * @param {string} id - The resource ID
1061
1434
  * @param {string} userId - The user ID
1062
1435
  */
1063
1436
  removeWriter: (id, userId, auditMessage) =>
1064
- this._request('DELETE', `/api/v1/projects/${id}/writers/${userId}`, { auditMessage }),
1437
+ this._request("DELETE", `/api/v1/projects/${id}/writers/${userId}`, {
1438
+ auditMessage,
1439
+ }),
1065
1440
  /**
1066
1441
  * Set a user's access level to read-only for this project.
1067
1442
  * @param {string} id - The resource ID
1068
1443
  * @param {string} userId - The user ID
1069
1444
  */
1070
1445
  addReader: (id, userId, auditMessage) =>
1071
- this._request('POST', `/api/v1/projects/${id}/readers/${userId}`, { auditMessage }),
1446
+ this._request("POST", `/api/v1/projects/${id}/readers/${userId}`, {
1447
+ auditMessage,
1448
+ }),
1072
1449
  /**
1073
1450
  * Remove a user's reader privileges for this project.
1074
1451
  * @param {string} id - The resource ID
1075
1452
  * @param {string} userId - The user ID
1076
1453
  */
1077
1454
  removeReader: (id, userId, auditMessage) =>
1078
- this._request('DELETE', `/api/v1/projects/${id}/readers/${userId}`, { auditMessage }),
1455
+ this._request("DELETE", `/api/v1/projects/${id}/readers/${userId}`, {
1456
+ auditMessage,
1457
+ }),
1079
1458
  /**
1080
1459
  * Set a configuration value for a project in an editor namespace.
1081
1460
  * @param {string} id - The resource ID
@@ -1084,9 +1463,11 @@ class PlaidClient {
1084
1463
  * @param {any} configValue - Configuration value to set
1085
1464
  */
1086
1465
  setConfig: (id, namespace, configKey, configValue, auditMessage) =>
1087
- this._request('PUT', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, { auditMessage,
1088
- rawBody: configValue, skipResponseTransform: true,
1089
- }),
1466
+ this._request(
1467
+ "PUT",
1468
+ `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
1469
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
1470
+ ),
1090
1471
  /**
1091
1472
  * Remove a configuration value for a project.
1092
1473
  * @param {string} id - The resource ID
@@ -1094,23 +1475,31 @@ class PlaidClient {
1094
1475
  * @param {string} configKey - The config key
1095
1476
  */
1096
1477
  deleteConfig: (id, namespace, configKey, auditMessage) =>
1097
- this._request('DELETE', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, { auditMessage,
1098
- skipResponseTransform: true,
1099
- }),
1478
+ this._request(
1479
+ "DELETE",
1480
+ `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
1481
+ { auditMessage, skipResponseTransform: true },
1482
+ ),
1100
1483
  /**
1101
1484
  * Assign a user as a maintainer for this project.
1102
1485
  * @param {string} id - The resource ID
1103
1486
  * @param {string} userId - The user ID
1104
1487
  */
1105
1488
  addMaintainer: (id, userId, auditMessage) =>
1106
- this._request('POST', `/api/v1/projects/${id}/maintainers/${userId}`, { auditMessage }),
1489
+ this._request("POST", `/api/v1/projects/${id}/maintainers/${userId}`, {
1490
+ auditMessage,
1491
+ }),
1107
1492
  /**
1108
1493
  * Remove a user's maintainer privileges for this project.
1109
1494
  * @param {string} id - The resource ID
1110
1495
  * @param {string} userId - The user ID
1111
1496
  */
1112
1497
  removeMaintainer: (id, userId, auditMessage) =>
1113
- this._request('DELETE', `/api/v1/projects/${id}/maintainers/${userId}`, { auditMessage }),
1498
+ this._request(
1499
+ "DELETE",
1500
+ `/api/v1/projects/${id}/maintainers/${userId}`,
1501
+ { auditMessage },
1502
+ ),
1114
1503
  /**
1115
1504
  * Get audit log for a project. Transparently follows pagination cursors
1116
1505
  * and returns the full flat array.
@@ -1119,10 +1508,33 @@ class PlaidClient {
1119
1508
  * @param {string} [startTime] - Start of time range
1120
1509
  * @param {string} [endTime] - End of time range
1121
1510
  * @param {string} [asOf] - Temporal query timestamp
1511
+ * @param {string[]|string} [opTypes] - Only return operations of these
1512
+ * types, spelled as in an entry's `op/type` (e.g.
1513
+ * `['span-layer/create', 'span-layer/delete']`). An entry appears when
1514
+ * one of its operations matches, carrying only the ones that did.
1122
1515
  */
1123
- audit: (projectId, startTime, endTime, asOf) =>
1516
+ audit: (projectId, startTime, endTime, asOf, opTypes) =>
1124
1517
  listAll(this, `/api/v1/projects/${projectId}/audit`, {
1125
- query: { 'start-time': startTime, 'end-time': endTime, 'as-of': asOf },
1518
+ query: {
1519
+ "start-time": startTime,
1520
+ "end-time": endTime,
1521
+ "as-of": asOf,
1522
+ "op-types": opTypesParam(opTypes),
1523
+ },
1524
+ }),
1525
+ /**
1526
+ * When the calling user last wrote to each document in a project, as a
1527
+ * `{documentId: timestamp}` map. Documents they have never written to
1528
+ * are absent. One request covers a whole document list.
1529
+ *
1530
+ * The response is NOT key-transformed: its keys are document ids, and
1531
+ * camelCasing would mangle the hyphens in a UUID.
1532
+ * @param {string} projectId - The project ID
1533
+ * @returns {Promise<Object<string, string>>}
1534
+ */
1535
+ myLastEdits: (projectId) =>
1536
+ this._request("GET", `/api/v1/projects/${projectId}/audit/last-edits`, {
1537
+ skipResponseTransform: true,
1126
1538
  }),
1127
1539
  /**
1128
1540
  * Link a vocabulary to a project.
@@ -1130,14 +1542,18 @@ class PlaidClient {
1130
1542
  * @param {string} vocabId - The vocab layer ID
1131
1543
  */
1132
1544
  linkVocab: (id, vocabId, auditMessage) =>
1133
- this._request('POST', `/api/v1/projects/${id}/vocabs/${vocabId}`, { auditMessage }),
1545
+ this._request("POST", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
1546
+ auditMessage,
1547
+ }),
1134
1548
  /**
1135
1549
  * Unlink a vocabulary from a project.
1136
1550
  * @param {string} id - The resource ID
1137
1551
  * @param {string} vocabId - The vocab layer ID
1138
1552
  */
1139
1553
  unlinkVocab: (id, vocabId, auditMessage) =>
1140
- this._request('DELETE', `/api/v1/projects/${id}/vocabs/${vocabId}`, { auditMessage }),
1554
+ this._request("DELETE", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
1555
+ auditMessage,
1556
+ }),
1141
1557
  /**
1142
1558
  * Get a project by ID. To fetch the project's documents, use
1143
1559
  * listDocuments(id) — the include-documents flag has been removed.
@@ -1145,8 +1561,8 @@ class PlaidClient {
1145
1561
  * @param {string} [asOf] - Temporal query timestamp
1146
1562
  */
1147
1563
  get: (id, asOf) =>
1148
- this._request('GET', `/api/v1/projects/${id}`, {
1149
- queryParams: { 'as-of': asOf },
1564
+ this._request("GET", `/api/v1/projects/${id}`, {
1565
+ queryParams: { "as-of": asOf },
1150
1566
  }),
1151
1567
  /**
1152
1568
  * List all documents in a project. Transparently follows pagination
@@ -1157,8 +1573,7 @@ class PlaidClient {
1157
1573
  * server rejects `?as-of=` on the documents-list route with a 400.
1158
1574
  * @param {string} id - The project ID
1159
1575
  */
1160
- listDocuments: (id) =>
1161
- listAll(this, `/api/v1/projects/${id}/documents`),
1576
+ listDocuments: (id) => listAll(this, `/api/v1/projects/${id}/documents`),
1162
1577
  /**
1163
1578
  * Fetch a single page of a project's documents.
1164
1579
  *
@@ -1198,14 +1613,18 @@ class PlaidClient {
1198
1613
  * @param {number} [options.timeout=0] - Per-request timeout in ms (0/null disables)
1199
1614
  */
1200
1615
  delete: (id, auditMessage, { timeout = 0 } = {}) =>
1201
- this._request('DELETE', `/api/v1/projects/${id}`, { auditMessage, timeout }),
1616
+ this._request("DELETE", `/api/v1/projects/${id}`, {
1617
+ auditMessage,
1618
+ timeout,
1619
+ }),
1202
1620
  /**
1203
1621
  * Update a project's name.
1204
1622
  * @param {string} id - The resource ID
1205
1623
  * @param {string} name - The name
1206
1624
  */
1207
1625
  update: (id, name, auditMessage) =>
1208
- this._request('PATCH', `/api/v1/projects/${id}`, { auditMessage,
1626
+ this._request("PATCH", `/api/v1/projects/${id}`, {
1627
+ auditMessage,
1209
1628
  body: bodyOf({ name }),
1210
1629
  }),
1211
1630
  /**
@@ -1215,7 +1634,7 @@ class PlaidClient {
1215
1634
  * @param {string} [asOf] - Temporal query timestamp
1216
1635
  */
1217
1636
  list: (asOf) =>
1218
- listAll(this, '/api/v1/projects', { query: { 'as-of': asOf } }),
1637
+ listAll(this, "/api/v1/projects", { query: { "as-of": asOf } }),
1219
1638
  /**
1220
1639
  * Fetch a single page of projects.
1221
1640
  * @param {object} [opts]
@@ -1225,7 +1644,11 @@ class PlaidClient {
1225
1644
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1226
1645
  */
1227
1646
  listPage: ({ limit, cursor, asOf } = {}) =>
1228
- listPage(this, '/api/v1/projects', { limit, cursor, query: { 'as-of': asOf } }),
1647
+ listPage(this, "/api/v1/projects", {
1648
+ limit,
1649
+ cursor,
1650
+ query: { "as-of": asOf },
1651
+ }),
1229
1652
  /**
1230
1653
  * Async-iterate projects page by page; yields each page's entries array.
1231
1654
  * @param {object} [opts]
@@ -1235,13 +1658,17 @@ class PlaidClient {
1235
1658
  * @returns {AsyncGenerator<Array>}
1236
1659
  */
1237
1660
  iterPages: ({ pageSize, asOf } = {}) =>
1238
- iterPages(this, '/api/v1/projects', { pageSize, query: { 'as-of': asOf } }),
1661
+ iterPages(this, "/api/v1/projects", {
1662
+ pageSize,
1663
+ query: { "as-of": asOf },
1664
+ }),
1239
1665
  /**
1240
1666
  * Create a new project. Note: this also registers the user as a maintainer.
1241
1667
  * @param {string} name - The name
1242
1668
  */
1243
1669
  create: (name, auditMessage) =>
1244
- this._request('POST', '/api/v1/projects', { auditMessage,
1670
+ this._request("POST", "/api/v1/projects", {
1671
+ auditMessage,
1245
1672
  body: bodyOf({ name }),
1246
1673
  }),
1247
1674
  };
@@ -1254,10 +1681,18 @@ class PlaidClient {
1254
1681
  * @param {string} configKey - The config key
1255
1682
  * @param {any} configValue - Configuration value to set
1256
1683
  */
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
- }),
1684
+ setConfig: (
1685
+ textLayerId,
1686
+ namespace,
1687
+ configKey,
1688
+ configValue,
1689
+ auditMessage,
1690
+ ) =>
1691
+ this._request(
1692
+ "PUT",
1693
+ `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
1694
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
1695
+ ),
1261
1696
  /**
1262
1697
  * Remove a configuration value for a layer.
1263
1698
  * @param {string} textLayerId - The text layer ID
@@ -1265,31 +1700,36 @@ class PlaidClient {
1265
1700
  * @param {string} configKey - The config key
1266
1701
  */
1267
1702
  deleteConfig: (textLayerId, namespace, configKey, auditMessage) =>
1268
- this._request('DELETE', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1269
- skipResponseTransform: true,
1270
- }),
1703
+ this._request(
1704
+ "DELETE",
1705
+ `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
1706
+ { auditMessage, skipResponseTransform: true },
1707
+ ),
1271
1708
  /**
1272
1709
  * Get a text layer by ID.
1273
1710
  * @param {string} textLayerId - The text layer ID
1274
1711
  * @param {string} [asOf] - Temporal query timestamp
1275
1712
  */
1276
1713
  get: (textLayerId, asOf) =>
1277
- this._request('GET', `/api/v1/text-layers/${textLayerId}`, {
1278
- queryParams: { 'as-of': asOf },
1714
+ this._request("GET", `/api/v1/text-layers/${textLayerId}`, {
1715
+ queryParams: { "as-of": asOf },
1279
1716
  }),
1280
1717
  /**
1281
1718
  * Delete a text layer.
1282
1719
  * @param {string} textLayerId - The text layer ID
1283
1720
  */
1284
1721
  delete: (textLayerId, auditMessage) =>
1285
- this._request('DELETE', `/api/v1/text-layers/${textLayerId}`, { auditMessage }),
1722
+ this._request("DELETE", `/api/v1/text-layers/${textLayerId}`, {
1723
+ auditMessage,
1724
+ }),
1286
1725
  /**
1287
1726
  * Update a text layer's name.
1288
1727
  * @param {string} textLayerId - The text layer ID
1289
1728
  * @param {string} name - The name
1290
1729
  */
1291
1730
  update: (textLayerId, name, auditMessage) =>
1292
- this._request('PATCH', `/api/v1/text-layers/${textLayerId}`, { auditMessage,
1731
+ this._request("PATCH", `/api/v1/text-layers/${textLayerId}`, {
1732
+ auditMessage,
1293
1733
  body: bodyOf({ name }),
1294
1734
  }),
1295
1735
  /**
@@ -1298,7 +1738,8 @@ class PlaidClient {
1298
1738
  * @param {string} direction - The direction ("up" or "down")
1299
1739
  */
1300
1740
  shift: (textLayerId, direction, auditMessage) =>
1301
- this._request('POST', `/api/v1/text-layers/${textLayerId}/shift`, { auditMessage,
1741
+ this._request("POST", `/api/v1/text-layers/${textLayerId}/shift`, {
1742
+ auditMessage,
1302
1743
  body: bodyOf({ direction }),
1303
1744
  }),
1304
1745
  /**
@@ -1307,8 +1748,9 @@ class PlaidClient {
1307
1748
  * @param {string} name - The name
1308
1749
  */
1309
1750
  create: (projectId, name, auditMessage) =>
1310
- this._request('POST', '/api/v1/text-layers', { auditMessage,
1311
- body: bodyOf({ 'project-id': projectId, name }),
1751
+ this._request("POST", "/api/v1/text-layers", {
1752
+ auditMessage,
1753
+ body: bodyOf({ "project-id": projectId, name }),
1312
1754
  }),
1313
1755
  };
1314
1756
 
@@ -1319,15 +1761,18 @@ class PlaidClient {
1319
1761
  * @param {any} body - The request body
1320
1762
  */
1321
1763
  setMetadata: (id, body, auditMessage) =>
1322
- this._request('PUT', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1323
- rawBody: body, skipResponseTransform: true,
1764
+ this._request("PUT", `/api/v1/vocab-items/${id}/metadata`, {
1765
+ auditMessage,
1766
+ rawBody: body,
1767
+ skipResponseTransform: true,
1324
1768
  }),
1325
1769
  /**
1326
1770
  * Remove all metadata from a vocab item.
1327
1771
  * @param {string} id - The resource ID
1328
1772
  */
1329
1773
  deleteMetadata: (id, auditMessage) =>
1330
- this._request('DELETE', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1774
+ this._request("DELETE", `/api/v1/vocab-items/${id}/metadata`, {
1775
+ auditMessage,
1331
1776
  skipResponseTransform: true,
1332
1777
  }),
1333
1778
  /**
@@ -1336,8 +1781,10 @@ class PlaidClient {
1336
1781
  * @param {any} body - The metadata patch
1337
1782
  */
1338
1783
  patchMetadata: (id, body, auditMessage) =>
1339
- this._request('PATCH', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1340
- rawBody: body, skipResponseTransform: true,
1784
+ this._request("PATCH", `/api/v1/vocab-items/${id}/metadata`, {
1785
+ auditMessage,
1786
+ rawBody: body,
1787
+ skipResponseTransform: true,
1341
1788
  }),
1342
1789
  /**
1343
1790
  * Create a new vocab item
@@ -1346,8 +1793,9 @@ class PlaidClient {
1346
1793
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1347
1794
  */
1348
1795
  create: (vocabLayerId, form, metadata, auditMessage) =>
1349
- this._request('POST', '/api/v1/vocab-items', { auditMessage,
1350
- body: bodyOf({ 'vocab-layer-id': vocabLayerId, form, metadata }),
1796
+ this._request("POST", "/api/v1/vocab-items", {
1797
+ auditMessage,
1798
+ body: bodyOf({ "vocab-layer-id": vocabLayerId, form, metadata }),
1351
1799
  }),
1352
1800
  /**
1353
1801
  * Create multiple vocab items in a single operation. Entries may target
@@ -1355,36 +1803,43 @@ class PlaidClient {
1355
1803
  * @param {Array<{vocabLayerId: string, form: string, metadata?: any}>} body - The vocab items to create
1356
1804
  */
1357
1805
  bulkCreate: (body, auditMessage) =>
1358
- this._request('POST', '/api/v1/vocab-items/bulk', { auditMessage, body }),
1806
+ this._request("POST", "/api/v1/vocab-items/bulk", {
1807
+ auditMessage,
1808
+ body,
1809
+ }),
1359
1810
  /**
1360
1811
  * Delete multiple vocab items in a single operation. Each item's
1361
1812
  * descendant vocab links are deleted too. Provide an array of IDs.
1362
1813
  * @param {string[]} body - The vocab item IDs to delete
1363
1814
  */
1364
1815
  bulkDelete: (body, auditMessage) =>
1365
- this._request('DELETE', '/api/v1/vocab-items/bulk', { auditMessage, body }),
1816
+ this._request("DELETE", "/api/v1/vocab-items/bulk", {
1817
+ auditMessage,
1818
+ body,
1819
+ }),
1366
1820
  /**
1367
1821
  * Get a vocab item by ID
1368
1822
  * @param {string} id - The resource ID
1369
1823
  * @param {string} [asOf] - Temporal query timestamp
1370
1824
  */
1371
1825
  get: (id, asOf) =>
1372
- this._request('GET', `/api/v1/vocab-items/${id}`, {
1373
- queryParams: { 'as-of': asOf },
1826
+ this._request("GET", `/api/v1/vocab-items/${id}`, {
1827
+ queryParams: { "as-of": asOf },
1374
1828
  }),
1375
1829
  /**
1376
1830
  * Delete a vocab item
1377
1831
  * @param {string} id - The resource ID
1378
1832
  */
1379
1833
  delete: (id, auditMessage) =>
1380
- this._request('DELETE', `/api/v1/vocab-items/${id}`, { auditMessage }),
1834
+ this._request("DELETE", `/api/v1/vocab-items/${id}`, { auditMessage }),
1381
1835
  /**
1382
1836
  * Update a vocab item's form
1383
1837
  * @param {string} id - The resource ID
1384
1838
  * @param {string} form - The vocab item form
1385
1839
  */
1386
1840
  update: (id, form, auditMessage) =>
1387
- this._request('PATCH', `/api/v1/vocab-items/${id}`, { auditMessage,
1841
+ this._request("PATCH", `/api/v1/vocab-items/${id}`, {
1842
+ auditMessage,
1388
1843
  body: bodyOf({ form }),
1389
1844
  }),
1390
1845
  };
@@ -1396,17 +1851,20 @@ class PlaidClient {
1396
1851
  * @param {string} direction - The direction ("up" or "down")
1397
1852
  */
1398
1853
  shift: (relationLayerId, direction, auditMessage) =>
1399
- this._request('POST', `/api/v1/relation-layers/${relationLayerId}/shift`, { auditMessage,
1400
- body: bodyOf({ direction }),
1401
- }),
1854
+ this._request(
1855
+ "POST",
1856
+ `/api/v1/relation-layers/${relationLayerId}/shift`,
1857
+ { auditMessage, body: bodyOf({ direction }) },
1858
+ ),
1402
1859
  /**
1403
1860
  * Create a new relation layer.
1404
1861
  * @param {string} spanLayerId - The span layer ID
1405
1862
  * @param {string} name - The name
1406
1863
  */
1407
1864
  create: (spanLayerId, name, auditMessage) =>
1408
- this._request('POST', '/api/v1/relation-layers', { auditMessage,
1409
- body: bodyOf({ 'span-layer-id': spanLayerId, name }),
1865
+ this._request("POST", "/api/v1/relation-layers", {
1866
+ auditMessage,
1867
+ body: bodyOf({ "span-layer-id": spanLayerId, name }),
1410
1868
  }),
1411
1869
  /**
1412
1870
  * Set a configuration value for a layer in an editor namespace.
@@ -1415,10 +1873,18 @@ class PlaidClient {
1415
1873
  * @param {string} configKey - The config key
1416
1874
  * @param {any} configValue - Configuration value to set
1417
1875
  */
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
- }),
1876
+ setConfig: (
1877
+ relationLayerId,
1878
+ namespace,
1879
+ configKey,
1880
+ configValue,
1881
+ auditMessage,
1882
+ ) =>
1883
+ this._request(
1884
+ "PUT",
1885
+ `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
1886
+ { auditMessage, rawBody: configValue, skipResponseTransform: true },
1887
+ ),
1422
1888
  /**
1423
1889
  * Remove a configuration value for a layer.
1424
1890
  * @param {string} relationLayerId - The relation layer ID
@@ -1426,31 +1892,36 @@ class PlaidClient {
1426
1892
  * @param {string} configKey - The config key
1427
1893
  */
1428
1894
  deleteConfig: (relationLayerId, namespace, configKey, auditMessage) =>
1429
- this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1430
- skipResponseTransform: true,
1431
- }),
1895
+ this._request(
1896
+ "DELETE",
1897
+ `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
1898
+ { auditMessage, skipResponseTransform: true },
1899
+ ),
1432
1900
  /**
1433
1901
  * Get a relation layer by ID.
1434
1902
  * @param {string} relationLayerId - The relation layer ID
1435
1903
  * @param {string} [asOf] - Temporal query timestamp
1436
1904
  */
1437
1905
  get: (relationLayerId, asOf) =>
1438
- this._request('GET', `/api/v1/relation-layers/${relationLayerId}`, {
1439
- queryParams: { 'as-of': asOf },
1906
+ this._request("GET", `/api/v1/relation-layers/${relationLayerId}`, {
1907
+ queryParams: { "as-of": asOf },
1440
1908
  }),
1441
1909
  /**
1442
1910
  * Delete a relation layer.
1443
1911
  * @param {string} relationLayerId - The relation layer ID
1444
1912
  */
1445
1913
  delete: (relationLayerId, auditMessage) =>
1446
- this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}`, { auditMessage }),
1914
+ this._request("DELETE", `/api/v1/relation-layers/${relationLayerId}`, {
1915
+ auditMessage,
1916
+ }),
1447
1917
  /**
1448
1918
  * Update a relation layer's name.
1449
1919
  * @param {string} relationLayerId - The relation layer ID
1450
1920
  * @param {string} name - The name
1451
1921
  */
1452
1922
  update: (relationLayerId, name, auditMessage) =>
1453
- this._request('PATCH', `/api/v1/relation-layers/${relationLayerId}`, { auditMessage,
1923
+ this._request("PATCH", `/api/v1/relation-layers/${relationLayerId}`, {
1924
+ auditMessage,
1454
1925
  body: bodyOf({ name }),
1455
1926
  }),
1456
1927
  };
@@ -1474,9 +1945,25 @@ class PlaidClient {
1474
1945
  * @param {number} [precedence] - Ordering precedence
1475
1946
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1476
1947
  */
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 }),
1948
+ create: (
1949
+ tokenLayerId,
1950
+ text,
1951
+ begin,
1952
+ end,
1953
+ precedence,
1954
+ metadata,
1955
+ auditMessage,
1956
+ ) =>
1957
+ this._request("POST", "/api/v1/tokens", {
1958
+ auditMessage,
1959
+ body: bodyOf({
1960
+ "token-layer-id": tokenLayerId,
1961
+ text,
1962
+ begin,
1963
+ end,
1964
+ precedence,
1965
+ metadata,
1966
+ }),
1480
1967
  }),
1481
1968
  /**
1482
1969
  * Get a token.
@@ -1484,8 +1971,8 @@ class PlaidClient {
1484
1971
  * @param {string} [asOf] - Temporal query timestamp
1485
1972
  */
1486
1973
  get: (tokenId, asOf) =>
1487
- this._request('GET', `/api/v1/tokens/${tokenId}`, {
1488
- queryParams: { 'as-of': asOf },
1974
+ this._request("GET", `/api/v1/tokens/${tokenId}`, {
1975
+ queryParams: { "as-of": asOf },
1489
1976
  }),
1490
1977
  /**
1491
1978
  * Delete a token and remove it from any spans. If this causes a span to
@@ -1493,7 +1980,7 @@ class PlaidClient {
1493
1980
  * @param {string} tokenId - The token ID
1494
1981
  */
1495
1982
  delete: (tokenId, auditMessage) =>
1496
- this._request('DELETE', `/api/v1/tokens/${tokenId}`, { auditMessage }),
1983
+ this._request("DELETE", `/api/v1/tokens/${tokenId}`, { auditMessage }),
1497
1984
  /**
1498
1985
  * Update a token.
1499
1986
  * @param {string} tokenId - The token ID
@@ -1505,7 +1992,8 @@ class PlaidClient {
1505
1992
  * drops undefined, so the three cases map correctly to the server.
1506
1993
  */
1507
1994
  update: (tokenId, begin, end, precedence, auditMessage) =>
1508
- this._request('PATCH', `/api/v1/tokens/${tokenId}`, { auditMessage,
1995
+ this._request("PATCH", `/api/v1/tokens/${tokenId}`, {
1996
+ auditMessage,
1509
1997
  body: bodyOf({ begin, end, precedence }),
1510
1998
  }),
1511
1999
  /**
@@ -1514,13 +2002,13 @@ class PlaidClient {
1514
2002
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
1515
2003
  */
1516
2004
  bulkCreate: (body, auditMessage) =>
1517
- this._request('POST', '/api/v1/tokens/bulk', { auditMessage, body }),
2005
+ this._request("POST", "/api/v1/tokens/bulk", { auditMessage, body }),
1518
2006
  /**
1519
2007
  * Delete multiple tokens in a single operation. Provide an array of IDs.
1520
2008
  * @param {Array} body - The request body
1521
2009
  */
1522
2010
  bulkDelete: (body, auditMessage) =>
1523
- this._request('DELETE', '/api/v1/tokens/bulk', { auditMessage, body }),
2011
+ this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
1524
2012
  /**
1525
2013
  * Split a token at a Unicode code-point offset. The original token becomes the
1526
2014
  * left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
@@ -1528,7 +2016,8 @@ class PlaidClient {
1528
2016
  * @param {number} position - Code-point offset to split at (strictly between begin and end)
1529
2017
  */
1530
2018
  split: (tokenId, position, auditMessage) =>
1531
- this._request('POST', `/api/v1/tokens/${tokenId}/split`, { auditMessage,
2019
+ this._request("POST", `/api/v1/tokens/${tokenId}/split`, {
2020
+ auditMessage,
1532
2021
  body: bodyOf({ position }),
1533
2022
  }),
1534
2023
  /**
@@ -1540,8 +2029,9 @@ class PlaidClient {
1540
2029
  * @param {string} otherTokenId - The other token to merge in
1541
2030
  */
1542
2031
  merge: (tokenId, otherTokenId, auditMessage) =>
1543
- this._request('POST', `/api/v1/tokens/${tokenId}/merge`, { auditMessage,
1544
- body: bodyOf({ 'other-token-id': otherTokenId }),
2032
+ this._request("POST", `/api/v1/tokens/${tokenId}/merge`, {
2033
+ auditMessage,
2034
+ body: bodyOf({ "other-token-id": otherTokenId }),
1545
2035
  }),
1546
2036
  /**
1547
2037
  * Shift a token's boundary. On partitioning layers the adjacent token is
@@ -1552,7 +2042,8 @@ class PlaidClient {
1552
2042
  * @param {number} [end] - New end offset, exclusive (Unicode code points)
1553
2043
  */
1554
2044
  shift: (tokenId, begin, end, auditMessage) =>
1555
- this._request('POST', `/api/v1/tokens/${tokenId}/shift`, { auditMessage,
2045
+ this._request("POST", `/api/v1/tokens/${tokenId}/shift`, {
2046
+ auditMessage,
1556
2047
  body: bodyOf({ begin, end }),
1557
2048
  }),
1558
2049
  /**
@@ -1561,15 +2052,18 @@ class PlaidClient {
1561
2052
  * @param {any} body - The request body
1562
2053
  */
1563
2054
  setMetadata: (tokenId, body, auditMessage) =>
1564
- this._request('PUT', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1565
- rawBody: body, skipResponseTransform: true,
2055
+ this._request("PUT", `/api/v1/tokens/${tokenId}/metadata`, {
2056
+ auditMessage,
2057
+ rawBody: body,
2058
+ skipResponseTransform: true,
1566
2059
  }),
1567
2060
  /**
1568
2061
  * Remove all metadata from a token.
1569
2062
  * @param {string} tokenId - The token ID
1570
2063
  */
1571
2064
  deleteMetadata: (tokenId, auditMessage) =>
1572
- this._request('DELETE', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
2065
+ this._request("DELETE", `/api/v1/tokens/${tokenId}/metadata`, {
2066
+ auditMessage,
1573
2067
  skipResponseTransform: true,
1574
2068
  }),
1575
2069
  /**
@@ -1578,8 +2072,10 @@ class PlaidClient {
1578
2072
  * @param {any} body - The metadata patch
1579
2073
  */
1580
2074
  patchMetadata: (tokenId, body, auditMessage) =>
1581
- this._request('PATCH', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1582
- rawBody: body, skipResponseTransform: true,
2075
+ this._request("PATCH", `/api/v1/tokens/${tokenId}/metadata`, {
2076
+ auditMessage,
2077
+ rawBody: body,
2078
+ skipResponseTransform: true,
1583
2079
  }),
1584
2080
  };
1585
2081
 
@@ -1595,14 +2091,22 @@ class PlaidClient {
1595
2091
  createSSEConnection(this, projectId, onEvent, path),
1596
2092
 
1597
2093
  /**
1598
- * Send a message to project listeners
2094
+ * Send a message to project listeners.
2095
+ *
2096
+ * `data` may be any JSON value and is sent VERBATIM (`rawBody`): a
2097
+ * message payload is opaque application data, like `metadata` and
2098
+ * `config`, so its keys must not be re-cased on the way out. Without
2099
+ * this a key such as `case-marker` would reach listeners as
2100
+ * `caseMarker` in JS and `case_marker` in Python. `listen` restores it
2101
+ * verbatim on the way in.
1599
2102
  * @param {string} projectId - The UUID of the project to send to
1600
2103
  * @param {any} data - The message data to send
1601
2104
  * @returns {Promise<any>} Response from the send operation
1602
2105
  */
1603
2106
  sendMessage: (projectId, data, auditMessage) =>
1604
- this._request('POST', `/api/v1/projects/${projectId}/message`, { auditMessage,
1605
- body: { body: data },
2107
+ this._request("POST", `/api/v1/projects/${projectId}/message`, {
2108
+ auditMessage,
2109
+ rawBody: { body: data },
1606
2110
  }),
1607
2111
 
1608
2112
  /**
@@ -1612,8 +2116,7 @@ class PlaidClient {
1612
2116
  * @param {string} projectId - The UUID of the project to query
1613
2117
  * @returns {Promise<Array>} Array of discovered service information
1614
2118
  */
1615
- discoverServices: (projectId) =>
1616
- discoverServices(this, projectId),
2119
+ discoverServices: (projectId) => discoverServices(this, projectId),
1617
2120
 
1618
2121
  /**
1619
2122
  * Forget a previously-seen (offline) service. Maintainer-only; 409 if
@@ -1627,26 +2130,70 @@ class PlaidClient {
1627
2130
 
1628
2131
  /**
1629
2132
  * Register as a service and handle incoming work requests.
2133
+ *
2134
+ * The registration reopens its channel whenever it drops, so a server
2135
+ * restart needs no service restart.
1630
2136
  * @param {string} projectId - The UUID of the project to serve
1631
2137
  * @param {Object} serviceInfo - Service information {serviceId, serviceName, description}
1632
2138
  * @param {function} onServiceRequest - Callback (data, responseHelper)
1633
2139
  * @param {Object} [extras] - Optional additional service metadata
1634
- * @returns {Object} Service registration object with .stop() method
2140
+ * @param {function} [onStatus] - Optional callback (event, projectId, detail) for
2141
+ * connection transitions: 'registered', 'reconnected', 'disconnected'
2142
+ * @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
1635
2143
  */
1636
- serve: (projectId, serviceInfo, onServiceRequest, extras) =>
1637
- serve(this, projectId, serviceInfo, onServiceRequest, extras),
2144
+ serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
2145
+ serve(this, projectId, serviceInfo, onServiceRequest, extras, onStatus),
1638
2146
 
1639
2147
  /**
1640
2148
  * Request a service to perform work and await its result.
2149
+ *
2150
+ * The request outlives this call: after a timeout, an abort, or a
2151
+ * dropped connection the service goes on, and `attachServiceRequest`
2152
+ * collects the result given the request id, which `opts.onAccepted`
2153
+ * receives as soon as the server has taken the request. Pass
2154
+ * `opts.requestId` (a UUID you mint) to know the id before submitting;
2155
+ * submitting an id that names a request you already made rejoins it
2156
+ * instead of starting another.
1641
2157
  * @param {string} projectId - The UUID of the project
1642
2158
  * @param {string} serviceId - The ID of the service to request
1643
2159
  * @param {any} data - The request data
1644
- * @param {number} [timeout] - Timeout in milliseconds (default: 10000)
2160
+ * @param {number} [timeout=10000] - Timeout in ms
1645
2161
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
1646
- * @returns {Promise<any>} Service response
2162
+ * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
2163
+ * @param {Object} [opts] - {requestId, onAccepted}
2164
+ * @returns {Promise<any>} The service's result
2165
+ */
2166
+ requestService: (projectId, serviceId, data, timeout, onProgress, signal, opts) =>
2167
+ requestService(this, projectId, serviceId, data, timeout, onProgress, signal, opts),
2168
+
2169
+ /**
2170
+ * Rejoin a service request made earlier and await its result: the
2171
+ * latest progress is replayed, then the result comes, or at once if the
2172
+ * request already finished. Only the user who submitted it (or an
2173
+ * admin). Rejects with an error whose `status` is 404 when the request
2174
+ * is unknown or expired (a finished request's result is kept for a
2175
+ * while, not forever).
2176
+ * @param {string} projectId - The UUID of the project
2177
+ * @param {string} requestId - The request id
2178
+ * @param {number} [timeout=10000] - Timeout in ms
2179
+ * @param {function} [onProgress] - Called with each progress payload
2180
+ * @param {AbortSignal} [signal] - Abort to stop waiting
2181
+ * @returns {Promise<any>} The service's result
2182
+ */
2183
+ attachServiceRequest: (projectId, requestId, timeout, onProgress, signal) =>
2184
+ attachServiceRequest(this, projectId, requestId, timeout, onProgress, signal),
2185
+
2186
+ /**
2187
+ * Ask the service to stop a request made earlier. The request still
2188
+ * ends with whatever the service then reports, on the stream of whoever
2189
+ * is awaiting it. Rejects with 404 if unknown or expired, 409 once
2190
+ * finished.
2191
+ * @param {string} projectId - The UUID of the project
2192
+ * @param {string} requestId - The request id
2193
+ * @returns {Promise<void>}
1647
2194
  */
1648
- requestService: (projectId, serviceId, data, timeout, onProgress) =>
1649
- requestService(this, projectId, serviceId, data, timeout, onProgress),
2195
+ cancelServiceRequest: (projectId, requestId) =>
2196
+ cancelServiceRequest(this, projectId, requestId),
1650
2197
  };
1651
2198
 
1652
2199
  /**
@@ -1684,8 +2231,14 @@ class PlaidClient {
1684
2231
  * For 'count': {return: 'count', count}. Entity cells are full entity objects
1685
2232
  * (same shape as the GET endpoints).
1686
2233
  */
2234
+ // bypassBatch: a query is a read. `client.batched()` flips one flag on the
2235
+ // whole client, so a query issued by unrelated code while a batch is open
2236
+ // (the IGT island fetching precedent while a document reconciles) used to
2237
+ // be queued into it: the caller got `{batched: true}` instead of results,
2238
+ // and the server ran the query against the batch's tx Connection, which
2239
+ // 500s and rolls back every write in that batch.
1687
2240
  this.query = (body, auditMessage) =>
1688
- this._request('POST', '/api/v1/query', { auditMessage, body });
2241
+ this._request("POST", "/api/v1/query", { auditMessage, body, bypassBatch: true });
1689
2242
 
1690
2243
  // Logical-operation groups (audit-log grouping). There is no create: a
1691
2244
  // group row is made lazily by the first write carrying `?group-id=`
@@ -1695,15 +2248,183 @@ class PlaidClient {
1695
2248
  * Get a logical-operation group (its label + creator).
1696
2249
  * @param {string} id - The group id
1697
2250
  */
1698
- get: (id) =>
1699
- this._request('GET', `/api/v1/operation-groups/${id}`),
2251
+ get: (id) => this._request("GET", `/api/v1/operation-groups/${id}`),
1700
2252
  /**
1701
2253
  * Relabel a logical-operation group after the fact. Owner or admin only.
1702
2254
  * @param {string} id - The group id
1703
2255
  * @param {string|null} message - The new label
1704
2256
  */
1705
2257
  update: (id, message) =>
1706
- this._request('PATCH', `/api/v1/operation-groups/${id}`, { body: { message } }),
2258
+ this._request("PATCH", `/api/v1/operation-groups/${id}`, {
2259
+ body: { message },
2260
+ }),
2261
+ };
2262
+
2263
+ this.comments = {
2264
+ /**
2265
+ * Post a comment on an entity. Requires write access to the entity's
2266
+ * project, or to the vocabulary for a vocab-item; the author is the
2267
+ * authenticated caller. Comments are not audited and do not bump the
2268
+ * document version.
2269
+ *
2270
+ * A comment outlives its anchor: deleting the entity does not delete
2271
+ * the comment. `anchorLabel` is the caption shown once that happens
2272
+ * ("Gloss of ktab, sentence 4"), so pass what the comment is about.
2273
+ * @param {'document'|'text'|'token'|'span'|'relation'|'vocab-item'} entityType - What kind of thing is being commented on
2274
+ * @param {string} entityId - The commented entity's id
2275
+ * @param {string} body - The comment text (1..10000 characters)
2276
+ * @param {object} [opts]
2277
+ * @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
2278
+ */
2279
+ create: (entityType, entityId, body, { anchorLabel } = {}) =>
2280
+ this._request("POST", "/api/v1/comments", {
2281
+ body: bodyOf({
2282
+ "entity-type": entityType,
2283
+ "entity-id": entityId,
2284
+ body,
2285
+ "anchor-label": anchorLabel,
2286
+ }),
2287
+ }),
2288
+ /**
2289
+ * Read one comment.
2290
+ * @param {string} id - The comment id
2291
+ */
2292
+ get: (id) => this._request("GET", `/api/v1/comments/${id}`),
2293
+ /**
2294
+ * Edit a comment's body. Only the comment's AUTHOR may do this - not
2295
+ * maintainers, not admins. Sets `edited` on the comment.
2296
+ * @param {string} id - The comment id
2297
+ * @param {string} body - The replacement text
2298
+ */
2299
+ update: (id, body) =>
2300
+ this._request("PATCH", `/api/v1/comments/${id}`, { body: { body } }),
2301
+ /**
2302
+ * Delete a comment. The author may delete their own; a project
2303
+ * maintainer (or admin) may delete any.
2304
+ * @param {string} id - The comment id
2305
+ */
2306
+ delete: (id) => this._request("DELETE", `/api/v1/comments/${id}`),
2307
+ /**
2308
+ * List comments in a project, oldest first. Transparently follows
2309
+ * pagination cursors and returns the full flat array.
2310
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching - use listPage() for a single page in a batch.
2311
+ * @param {string} projectId - The project to read
2312
+ * @param {object} [filters] - Narrow the scope
2313
+ * @param {string} [filters.documentId] - Every comment anywhere in one document
2314
+ * @param {string} [filters.entityType] - With entityId, one entity's thread
2315
+ * @param {string} [filters.entityId] - With entityType, one entity's thread
2316
+ */
2317
+ list: (projectId, { documentId, entityType, entityId } = {}) =>
2318
+ listAll(this, `/api/v1/projects/${projectId}/comments`, {
2319
+ query: {
2320
+ "document-id": documentId,
2321
+ "entity-type": entityType,
2322
+ "entity-id": entityId,
2323
+ },
2324
+ }),
2325
+ /**
2326
+ * Fetch a single page of a project's comments.
2327
+ * @param {string} projectId - The project to read
2328
+ * @param {object} [opts]
2329
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2330
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
2331
+ * @param {string} [opts.documentId] - Every comment anywhere in one document
2332
+ * @param {string} [opts.entityType] - With entityId, one entity's thread
2333
+ * @param {string} [opts.entityId] - With entityType, one entity's thread
2334
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2335
+ */
2336
+ listPage: (projectId, { limit, cursor, documentId, entityType, entityId } = {}) =>
2337
+ listPage(this, `/api/v1/projects/${projectId}/comments`, {
2338
+ limit,
2339
+ cursor,
2340
+ query: {
2341
+ "document-id": documentId,
2342
+ "entity-type": entityType,
2343
+ "entity-id": entityId,
2344
+ },
2345
+ }),
2346
+ /**
2347
+ * Async-iterate a project's comments page by page; yields each page's entries array.
2348
+ * 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.
2349
+ * @param {string} projectId - The project to read
2350
+ * @param {object} [opts]
2351
+ * @param {number} [opts.pageSize] - Per-request page size
2352
+ * @param {string} [opts.documentId] - Every comment anywhere in one document
2353
+ * @param {string} [opts.entityType] - With entityId, one entity's thread
2354
+ * @param {string} [opts.entityId] - With entityType, one entity's thread
2355
+ * @returns {AsyncGenerator<Array>}
2356
+ */
2357
+ iterPages: (projectId, { pageSize, documentId, entityType, entityId } = {}) =>
2358
+ iterPages(this, `/api/v1/projects/${projectId}/comments`, {
2359
+ pageSize,
2360
+ query: {
2361
+ "document-id": documentId,
2362
+ "entity-type": entityType,
2363
+ "entity-id": entityId,
2364
+ },
2365
+ }),
2366
+ /**
2367
+ * Comment counts per entity, as an `{entityId: n}` map, over the same
2368
+ * scope and filters as `list`. One cheap request paints a comment
2369
+ * indicator on every annotated item in a document without paging
2370
+ * through the bodies.
2371
+ *
2372
+ * The response is NOT key-transformed: its keys are entity ids, and
2373
+ * camelCasing would mangle the hyphens in a UUID.
2374
+ * @param {string} projectId - The project to read
2375
+ * @param {object} [filters] - Same filters as list()
2376
+ * @returns {Promise<Object<string, number>>}
2377
+ */
2378
+ counts: (projectId, { documentId, entityType, entityId } = {}) =>
2379
+ this._request("GET", `/api/v1/projects/${projectId}/comments/counts`, {
2380
+ queryParams: {
2381
+ "document-id": documentId,
2382
+ "entity-type": entityType,
2383
+ "entity-id": entityId,
2384
+ },
2385
+ skipResponseTransform: true,
2386
+ }),
2387
+ /**
2388
+ * List the comments on a vocabulary's entries, oldest first. Requires
2389
+ * read access to the vocabulary. Transparently follows pagination
2390
+ * cursors and returns the full flat array.
2391
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching - use listInVocabPage() for a single page in a batch.
2392
+ * @param {string} vocabId - The vocab layer to read
2393
+ * @param {object} [filters]
2394
+ * @param {string} [filters.entityId] - One entry's thread
2395
+ */
2396
+ listInVocab: (vocabId, { entityId } = {}) =>
2397
+ listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
2398
+ query: { "entity-id": entityId },
2399
+ }),
2400
+ /**
2401
+ * Fetch a single page of a vocabulary's comments.
2402
+ * @param {string} vocabId - The vocab layer to read
2403
+ * @param {object} [opts]
2404
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2405
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
2406
+ * @param {string} [opts.entityId] - One entry's thread
2407
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2408
+ */
2409
+ listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
2410
+ listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
2411
+ limit,
2412
+ cursor,
2413
+ query: { "entity-id": entityId },
2414
+ }),
2415
+ /**
2416
+ * Comment counts per entry of a vocabulary, as an `{entityId: n}` map.
2417
+ * The response is NOT key-transformed: its keys are entry ids.
2418
+ * @param {string} vocabId - The vocab layer to read
2419
+ * @param {object} [filters]
2420
+ * @param {string} [filters.entityId] - One entry only
2421
+ * @returns {Promise<Object<string, number>>}
2422
+ */
2423
+ countsInVocab: (vocabId, { entityId } = {}) =>
2424
+ this._request("GET", `/api/v1/vocab-layers/${vocabId}/comments/counts`, {
2425
+ queryParams: { "entity-id": entityId },
2426
+ skipResponseTransform: true,
2427
+ }),
1707
2428
  };
1708
2429
  }
1709
2430
 
@@ -1811,7 +2532,9 @@ class PlaidClient {
1811
2532
  async withOperation(message, fn) {
1812
2533
  this.beginOperation(message);
1813
2534
  const group = this.operationGroup;
1814
- const setMessage = (msg) => { if (group.depth === 1) group.refined = msg; };
2535
+ const setMessage = (msg) => {
2536
+ if (group.depth === 1) group.refined = msg;
2537
+ };
1815
2538
  try {
1816
2539
  return await fn(setMessage);
1817
2540
  } finally {
@@ -1835,7 +2558,7 @@ class PlaidClient {
1835
2558
  */
1836
2559
  async submitBatch() {
1837
2560
  if (!this.isBatching) {
1838
- throw new Error('No active batch. Call beginBatch() first.');
2561
+ throw new Error("No active batch. Call beginBatch() first.");
1839
2562
  }
1840
2563
 
1841
2564
  if (this.batchOperations.length === 0) {
@@ -1844,55 +2567,84 @@ class PlaidClient {
1844
2567
  }
1845
2568
 
1846
2569
  try {
1847
- let url = `${this.baseUrl}/api/v1/batch`;
1848
- const body = this.batchOperations.map(op => ({
2570
+ const url = `${this.baseUrl}/api/v1/batch`;
2571
+ const ops = this.batchOperations.map((op) => ({
1849
2572
  path: op.path,
1850
2573
  method: op.method.toUpperCase(),
1851
2574
  ...(op.body && { body: op.body }),
1852
2575
  }));
2576
+ // The server caps a batch at MAX_BATCH_OPS so one transaction cannot
2577
+ // hold the write lock without bound. A larger batch goes as consecutive
2578
+ // requests, results concatenated in queue order: it could not have been
2579
+ // one transaction anyway, and a repair or bulk edit over a big document
2580
+ // must not fail on its size alone.
2581
+ const results = [];
2582
+ for (let i = 0; i < ops.length; i += MAX_BATCH_OPS) {
2583
+ results.push(...(await this._postBatch(url, ops.slice(i, i + MAX_BATCH_OPS))));
2584
+ }
2585
+ return results;
2586
+ } finally {
2587
+ this.isBatching = false;
2588
+ this.batchOperations = [];
2589
+ }
2590
+ }
1853
2591
 
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;
2592
+ /** POST one batch request (at most MAX_BATCH_OPS operations); resolves to its transformed results. */
2593
+ async _postBatch(url, body) {
2594
+ const fetchOptions = {
2595
+ method: "POST",
2596
+ headers: {
2597
+ Authorization: `Bearer ${this.token}`,
2598
+ "Content-Type": "application/json",
2599
+ },
2600
+ body: JSON.stringify(body),
2601
+ };
1864
2602
 
1865
- try {
1866
- const response = await fetch(url, fetchOptions);
1867
- if (!response.ok) {
1868
- throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
2603
+ try {
2604
+ // Retry a 503: the batch is atomic, so a refused one wrote nothing and
2605
+ // repeating it is safe. A fresh timeout signal per attempt — an
2606
+ // AbortSignal.timeout stays aborted once it fires.
2607
+ const response = await retryWhileBusy(async () => {
2608
+ const signal = timeoutSignal(this.batchTimeout);
2609
+ const res = await fetch(
2610
+ url,
2611
+ signal ? { ...fetchOptions, signal } : fetchOptions,
2612
+ );
2613
+ if (!res.ok) {
2614
+ throw makeHttpError(res, await parseErrorBody(res), url, "POST");
1869
2615
  }
2616
+ return res;
2617
+ });
2618
+
2619
+ const results = await response.json();
1870
2620
 
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);
2621
+ // Extract document versions from each batch response
2622
+ for (const result of results) {
2623
+ if (result.headers && result.headers["X-Document-Versions"]) {
2624
+ try {
2625
+ const versionsMap = JSON.parse(
2626
+ result.headers["X-Document-Versions"],
2627
+ );
2628
+ if (typeof versionsMap === "object" && versionsMap !== null) {
2629
+ // Clone once per response, then merge — not once per entry.
2630
+ this.documentVersions = {
2631
+ ...this.documentVersions,
2632
+ ...versionsMap,
2633
+ };
1884
2634
  }
2635
+ } catch (e) {
2636
+ console.warn(
2637
+ "Failed to parse document versions header from batch response:",
2638
+ e,
2639
+ );
1885
2640
  }
1886
2641
  }
1887
-
1888
- return results.map(result => transformResponse(result));
1889
- } catch (error) {
1890
- if (error.status) throw error;
1891
- throw makeNetworkError(error, url, 'POST');
1892
2642
  }
1893
- } finally {
1894
- this.isBatching = false;
1895
- this.batchOperations = [];
2643
+
2644
+ return results.map((result) => transformResponse(result));
2645
+ } catch (error) {
2646
+ if (error.status) throw error;
2647
+ throw makeNetworkError(error, url, "POST");
1896
2648
  }
1897
2649
  }
1898
2650
 
@@ -1961,7 +2713,7 @@ class PlaidClient {
1961
2713
  * @returns {string}
1962
2714
  */
1963
2715
  static inviteUrl(appUrl, code) {
1964
- return `${appUrl.replace(/#.*$/, '').replace(/\/$/, '')}/#/invite/${encodeURIComponent(code)}`;
2716
+ return `${appUrl.replace(/#.*$/, "").replace(/\/$/, "")}/#/invite/${encodeURIComponent(code)}`;
1965
2717
  }
1966
2718
 
1967
2719
  /**
@@ -1975,63 +2727,90 @@ class PlaidClient {
1975
2727
  * @param {string} baseUrl - The API base URL
1976
2728
  * @param {string} code - The invite code
1977
2729
  * @param {object} [options]
1978
- * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, username?: string, grantAdmin: boolean}>}
2730
+ * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
1979
2731
  */
1980
2732
  static async lookupInvite(baseUrl, code, options = {}) {
1981
- return anonymousPost(baseUrl, '/api/v1/invite-codes/lookup', { code }, options);
2733
+ return anonymousPost(
2734
+ baseUrl,
2735
+ "/api/v1/invite-codes/lookup",
2736
+ { code },
2737
+ options,
2738
+ );
1982
2739
  }
1983
2740
 
1984
2741
  /**
1985
2742
  * Redeem an invite code, with NO authentication.
1986
2743
  *
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.
2744
+ * For a signup invite, pass `email` and `password` to create the account
2745
+ * (and optionally `displayName`); the invite's grants are applied in the
2746
+ * same transaction. For a password reset link, pass `password` only.
2747
+ * Resolves to a logged-in client, exactly like login() — the redeemer just
2748
+ * chose these credentials, so there is no reason to send them to a login
2749
+ * form to retype them.
1992
2750
  * @param {string} baseUrl - The API base URL
1993
2751
  * @param {string} code - The invite code
1994
2752
  * @param {object} credentials
1995
- * @param {string} [credentials.username] - Desired username (signup only)
2753
+ * @param {string} [credentials.email] - The new account's email address,
2754
+ * which becomes its id and login (signup only)
2755
+ * @param {string} [credentials.displayName] - How the new user is shown in
2756
+ * the UI; defaults to the local part of the email (signup only)
1996
2757
  * @param {string} credentials.password - Desired password (min 8 characters)
1997
2758
  * @param {object} [options]
1998
2759
  * @returns {Promise<{client: PlaidClient, userId: string, kind: string}>}
1999
2760
  */
2000
- static async redeemInvite(baseUrl, code, { username, password } = {}, options = {}) {
2761
+ static async redeemInvite(
2762
+ baseUrl,
2763
+ code,
2764
+ { email, password, displayName } = {},
2765
+ options = {},
2766
+ ) {
2001
2767
  const data = await anonymousPost(
2002
- baseUrl, '/api/v1/invite-codes/redeem',
2003
- bodyOf({ code, username, password }), options,
2768
+ baseUrl,
2769
+ "/api/v1/invite-codes/redeem",
2770
+ bodyOf({ code, email, password, "display-name": displayName }),
2771
+ options,
2004
2772
  );
2005
2773
  return {
2006
- client: new PlaidClient(baseUrl.replace(/\/$/, ''), data.token || '', options),
2774
+ client: new PlaidClient(
2775
+ baseUrl.replace(/\/$/, ""),
2776
+ data.token || "",
2777
+ options,
2778
+ ),
2007
2779
  userId: data.userId,
2008
2780
  kind: data.kind,
2009
2781
  };
2010
2782
  }
2011
2783
 
2012
2784
  static async login(baseUrl, userId, password, options = {}) {
2013
- baseUrl = baseUrl.replace(/\/$/, '');
2785
+ baseUrl = baseUrl.replace(/\/$/, "");
2014
2786
  const url = `${baseUrl}/api/v1/login`;
2015
2787
  try {
2016
2788
  const fetchOptions = {
2017
- method: 'POST',
2018
- headers: { 'Content-Type': 'application/json' },
2019
- body: JSON.stringify({ 'user-id': userId, password }),
2789
+ method: "POST",
2790
+ headers: { "Content-Type": "application/json" },
2791
+ body: JSON.stringify({ "user-id": userId, password }),
2020
2792
  };
2021
- const signal = timeoutSignal(options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS);
2793
+ const signal = timeoutSignal(
2794
+ options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
2795
+ );
2022
2796
  if (signal) fetchOptions.signal = signal;
2023
2797
 
2024
2798
  const response = await fetch(url, fetchOptions);
2025
2799
  if (!response.ok) {
2026
- throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
2800
+ throw makeHttpError(
2801
+ response,
2802
+ await parseErrorBody(response),
2803
+ url,
2804
+ "POST",
2805
+ );
2027
2806
  }
2028
2807
 
2029
2808
  const data = await response.json();
2030
- const token = data.token || '';
2809
+ const token = data.token || "";
2031
2810
  return new PlaidClient(baseUrl, token, options);
2032
2811
  } catch (error) {
2033
2812
  if (error.status) throw error;
2034
- throw makeNetworkError(error, url, 'POST');
2813
+ throw makeNetworkError(error, url, "POST");
2035
2814
  }
2036
2815
  }
2037
2816
  }
@@ -2041,8 +2820,21 @@ export { PlaidClient };
2041
2820
 
2042
2821
  // Unicode code-point helpers for text offsets (token begin/end are code-point
2043
2822
  // 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';
2823
+ export {
2824
+ cpLength,
2825
+ cpSlice,
2826
+ cpSlicer,
2827
+ utf16ToCp,
2828
+ cpToUtf16,
2829
+ cpIndexOf,
2830
+ } from "./codepoint.js";
2831
+ export {
2832
+ PLAID_NAMESPACE,
2833
+ ROLE_KEY,
2834
+ ROLES,
2835
+ readRole,
2836
+ findByRole,
2837
+ } from "./roles.js";
2046
2838
  // Service self-description helpers: filter discovered services by task, read a
2047
2839
  // service's parameter schema/summary, and build/coerce form values. See
2048
2840
  // ./serviceSchema.js.
@@ -2054,20 +2846,34 @@ export {
2054
2846
  getServiceSummary,
2055
2847
  buildDefaultValues,
2056
2848
  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".
2849
+ } from "./serviceSchema.js";
2850
+ // Provenance: the cross-app convention for who made an annotation (flat
2851
+ // prov/provSource/provConfirmed metadata; absence = a verifier; 'inferred' =
2852
+ // a machine; 'contributed' = a person whose work is reviewed), plus the
2853
+ // machine-writer contract. See ./provenance.js and the manual, "Provenance".
2062
2854
  export {
2063
2855
  PROV,
2064
2856
  PROV_STATES,
2065
2857
  PROV_CONFIRMED,
2066
2858
  stampInferred,
2067
2859
  confirmedInferred,
2860
+ stampContributed,
2068
2861
  provState,
2862
+ provOrigin,
2069
2863
  isMachine,
2070
2864
  isProtected,
2865
+ needsReview,
2071
2866
  verifyOnEdit,
2867
+ contributeOnEdit,
2868
+ mergeMetadata,
2072
2869
  serviceSource,
2073
- } from './provenance.js';
2870
+ userSource,
2871
+ REVIEW_KEY,
2872
+ PROJECT_ROLES,
2873
+ readReview,
2874
+ projectRole,
2875
+ isReviewed,
2876
+ withReviewedUser,
2877
+ writerPolicy,
2878
+ } from "./provenance.js";
2879
+ export { MAX_BATCH_OPS };