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

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
@@ -11,6 +11,7 @@ import { listAll, listPage, iterPages } from './pagination.js';
11
11
  import { createSSEConnection } from './sse.js';
12
12
  import {
13
13
  discoverServices,
14
+ discardService,
14
15
  serve,
15
16
  requestService,
16
17
  } from './services.js';
@@ -24,6 +25,32 @@ function bodyOf(obj) {
24
25
  return result;
25
26
  }
26
27
 
28
+ // POST to an endpoint that takes no Authorization header (login, invite lookup
29
+ // and redemption). Deliberately does not go through PlaidClient._request: these
30
+ // are called before any client exists, which is the whole point.
31
+ async function anonymousPost(baseUrl, path, body, options = {}) {
32
+ const base = baseUrl.replace(/\/$/, '');
33
+ const url = `${base}${path}`;
34
+ try {
35
+ const fetchOptions = {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify(body),
39
+ };
40
+ const signal = timeoutSignal(options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS);
41
+ if (signal) fetchOptions.signal = signal;
42
+
43
+ const response = await fetch(url, fetchOptions);
44
+ if (!response.ok) {
45
+ throw makeHttpError(response, await parseErrorBody(response), url, 'POST');
46
+ }
47
+ return transformResponse(await response.json());
48
+ } catch (error) {
49
+ if (error.status) throw error;
50
+ throw makeNetworkError(error, url, 'POST');
51
+ }
52
+ }
53
+
27
54
  class PlaidClient {
28
55
  /**
29
56
  * Create a new PlaidClient instance
@@ -40,6 +67,16 @@ class PlaidClient {
40
67
  this.batchOperations = [];
41
68
  this.documentVersions = {};
42
69
  this.strictModeDocumentId = null;
70
+ // The open logical operation (audit-log group), or null. While set, every
71
+ // write is stamped with `?group-id=` (+ `group-message`) so the audit log
72
+ // folds them into ONE expandable entry. See beginOperation / withOperation.
73
+ // Shape: { id, message, depth, written, refined }.
74
+ this.operationGroup = null;
75
+ // Optional callback fired once (per client) when any request returns HTTP
76
+ // 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
77
+ // the stored token and route back to login. See makeRequest in http.js.
78
+ this.onAuthError = options.onAuthError || null;
79
+ this._authErrorFired = false;
43
80
 
44
81
  // --- API Bundles ---
45
82
 
@@ -50,25 +87,40 @@ class PlaidClient {
50
87
  * @param {Array} tokens - The tokens to link
51
88
  * @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
52
89
  */
53
- create: (vocabItem, tokens, metadata) =>
54
- this._request('POST', '/api/v1/vocab-links', {
90
+ create: (vocabItem, tokens, metadata, auditMessage) =>
91
+ this._request('POST', '/api/v1/vocab-links', { auditMessage,
55
92
  body: bodyOf({ 'vocab-item': vocabItem, tokens, metadata }),
56
93
  }),
94
+ /**
95
+ * Create multiple vocab links in a single operation. Entries may
96
+ * reference different vocab items, but all tokens across the call must
97
+ * belong to one document.
98
+ * @param {Array<{vocabItem: string, tokens: string[], metadata?: any}>} body - The vocab links to create
99
+ * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
100
+ */
101
+ bulkCreate: (body, auditMessage) =>
102
+ this._request('POST', '/api/v1/vocab-links/bulk', { auditMessage, body }),
103
+ /**
104
+ * Delete multiple vocab links in a single operation. Provide an array of IDs.
105
+ * @param {string[]} body - The vocab link IDs to delete
106
+ */
107
+ bulkDelete: (body, auditMessage) =>
108
+ this._request('DELETE', '/api/v1/vocab-links/bulk', { auditMessage, body }),
57
109
  /**
58
110
  * Replace all metadata for a vocab link. The entire metadata map is replaced - existing metadata keys not included in the request will be removed.
59
111
  * @param {string} id - The resource ID
60
112
  * @param {any} body - The request body
61
113
  */
62
- setMetadata: (id, body) =>
63
- this._request('PUT', `/api/v1/vocab-links/${id}/metadata`, {
114
+ setMetadata: (id, body, auditMessage) =>
115
+ this._request('PUT', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
64
116
  rawBody: body, skipResponseTransform: true,
65
117
  }),
66
118
  /**
67
119
  * Remove all metadata from a vocab link.
68
120
  * @param {string} id - The resource ID
69
121
  */
70
- deleteMetadata: (id) =>
71
- this._request('DELETE', `/api/v1/vocab-links/${id}/metadata`, {
122
+ deleteMetadata: (id, auditMessage) =>
123
+ this._request('DELETE', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
72
124
  skipResponseTransform: true,
73
125
  }),
74
126
  /**
@@ -76,8 +128,8 @@ class PlaidClient {
76
128
  * @param {string} id - The resource ID
77
129
  * @param {any} body - The metadata patch
78
130
  */
79
- patchMetadata: (id, body) =>
80
- this._request('PATCH', `/api/v1/vocab-links/${id}/metadata`, {
131
+ patchMetadata: (id, body, auditMessage) =>
132
+ this._request('PATCH', `/api/v1/vocab-links/${id}/metadata`, { auditMessage,
81
133
  rawBody: body, skipResponseTransform: true,
82
134
  }),
83
135
  /**
@@ -93,8 +145,8 @@ class PlaidClient {
93
145
  * Delete a vocab link
94
146
  * @param {string} id - The resource ID
95
147
  */
96
- delete: (id) =>
97
- this._request('DELETE', `/api/v1/vocab-links/${id}`),
148
+ delete: (id, auditMessage) =>
149
+ this._request('DELETE', `/api/v1/vocab-links/${id}`, { auditMessage }),
98
150
  };
99
151
 
100
152
  this.vocabLayers = {
@@ -112,15 +164,15 @@ class PlaidClient {
112
164
  * Delete a vocab layer.
113
165
  * @param {string} id - The resource ID
114
166
  */
115
- delete: (id) =>
116
- this._request('DELETE', `/api/v1/vocab-layers/${id}`),
167
+ delete: (id, auditMessage) =>
168
+ this._request('DELETE', `/api/v1/vocab-layers/${id}`, { auditMessage }),
117
169
  /**
118
170
  * Update a vocab layer's name.
119
171
  * @param {string} id - The resource ID
120
172
  * @param {string} name - The name
121
173
  */
122
- update: (id, name) =>
123
- this._request('PATCH', `/api/v1/vocab-layers/${id}`, {
174
+ update: (id, name, auditMessage) =>
175
+ this._request('PATCH', `/api/v1/vocab-layers/${id}`, { auditMessage,
124
176
  body: bodyOf({ name }),
125
177
  }),
126
178
  /**
@@ -130,8 +182,8 @@ class PlaidClient {
130
182
  * @param {string} configKey - The config key
131
183
  * @param {any} configValue - Configuration value to set
132
184
  */
133
- setConfig: (id, namespace, configKey, configValue) =>
134
- this._request('PUT', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, {
185
+ setConfig: (id, namespace, configKey, configValue, auditMessage) =>
186
+ this._request('PUT', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, { auditMessage,
135
187
  rawBody: configValue, skipResponseTransform: true,
136
188
  }),
137
189
  /**
@@ -140,8 +192,8 @@ class PlaidClient {
140
192
  * @param {string} namespace - The config namespace
141
193
  * @param {string} configKey - The config key
142
194
  */
143
- deleteConfig: (id, namespace, configKey) =>
144
- this._request('DELETE', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, {
195
+ deleteConfig: (id, namespace, configKey, auditMessage) =>
196
+ this._request('DELETE', `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`, { auditMessage,
145
197
  skipResponseTransform: true,
146
198
  }),
147
199
  /**
@@ -176,8 +228,8 @@ class PlaidClient {
176
228
  * Create a new vocab layer. Note: this also registers the user as a maintainer.
177
229
  * @param {string} name - The name
178
230
  */
179
- create: (name) =>
180
- this._request('POST', '/api/v1/vocab-layers', {
231
+ create: (name, auditMessage) =>
232
+ this._request('POST', '/api/v1/vocab-layers', { auditMessage,
181
233
  body: bodyOf({ name }),
182
234
  }),
183
235
  /**
@@ -185,15 +237,15 @@ class PlaidClient {
185
237
  * @param {string} id - The resource ID
186
238
  * @param {string} userId - The user ID
187
239
  */
188
- addMaintainer: (id, userId) =>
189
- this._request('POST', `/api/v1/vocab-layers/${id}/maintainers/${userId}`),
240
+ addMaintainer: (id, userId, auditMessage) =>
241
+ this._request('POST', `/api/v1/vocab-layers/${id}/maintainers/${userId}`, { auditMessage }),
190
242
  /**
191
243
  * Remove a user's maintainer privileges for this vocab layer.
192
244
  * @param {string} id - The resource ID
193
245
  * @param {string} userId - The user ID
194
246
  */
195
- removeMaintainer: (id, userId) =>
196
- this._request('DELETE', `/api/v1/vocab-layers/${id}/maintainers/${userId}`),
247
+ removeMaintainer: (id, userId, auditMessage) =>
248
+ this._request('DELETE', `/api/v1/vocab-layers/${id}/maintainers/${userId}`, { auditMessage }),
197
249
  };
198
250
 
199
251
  this.relations = {
@@ -202,16 +254,16 @@ class PlaidClient {
202
254
  * @param {string} relationId - The relation ID
203
255
  * @param {any} body - The request body
204
256
  */
205
- setMetadata: (relationId, body) =>
206
- this._request('PUT', `/api/v1/relations/${relationId}/metadata`, {
257
+ setMetadata: (relationId, body, auditMessage) =>
258
+ this._request('PUT', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
207
259
  rawBody: body, skipResponseTransform: true,
208
260
  }),
209
261
  /**
210
262
  * Remove all metadata from a relation.
211
263
  * @param {string} relationId - The relation ID
212
264
  */
213
- deleteMetadata: (relationId) =>
214
- this._request('DELETE', `/api/v1/relations/${relationId}/metadata`, {
265
+ deleteMetadata: (relationId, auditMessage) =>
266
+ this._request('DELETE', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
215
267
  skipResponseTransform: true,
216
268
  }),
217
269
  /**
@@ -219,8 +271,8 @@ class PlaidClient {
219
271
  * @param {string} relationId - The relation ID
220
272
  * @param {any} body - The metadata patch
221
273
  */
222
- patchMetadata: (relationId, body) =>
223
- this._request('PATCH', `/api/v1/relations/${relationId}/metadata`, {
274
+ patchMetadata: (relationId, body, auditMessage) =>
275
+ this._request('PATCH', `/api/v1/relations/${relationId}/metadata`, { auditMessage,
224
276
  rawBody: body, skipResponseTransform: true,
225
277
  }),
226
278
  /**
@@ -228,8 +280,8 @@ class PlaidClient {
228
280
  * @param {string} relationId - The relation ID
229
281
  * @param {string} spanId - The span ID
230
282
  */
231
- setTarget: (relationId, spanId) =>
232
- this._request('PUT', `/api/v1/relations/${relationId}/target`, {
283
+ setTarget: (relationId, spanId, auditMessage) =>
284
+ this._request('PUT', `/api/v1/relations/${relationId}/target`, { auditMessage,
233
285
  body: bodyOf({ 'span-id': spanId }),
234
286
  }),
235
287
  /**
@@ -245,15 +297,15 @@ class PlaidClient {
245
297
  * Delete a relation.
246
298
  * @param {string} relationId - The relation ID
247
299
  */
248
- delete: (relationId) =>
249
- this._request('DELETE', `/api/v1/relations/${relationId}`),
300
+ delete: (relationId, auditMessage) =>
301
+ this._request('DELETE', `/api/v1/relations/${relationId}`, { auditMessage }),
250
302
  /**
251
303
  * Update a relation's value.
252
304
  * @param {string} relationId - The relation ID
253
305
  * @param {any} value - The value
254
306
  */
255
- update: (relationId, value) =>
256
- this._request('PATCH', `/api/v1/relations/${relationId}`, {
307
+ update: (relationId, value, auditMessage) =>
308
+ this._request('PATCH', `/api/v1/relations/${relationId}`, { auditMessage,
257
309
  body: bodyOf({ value }),
258
310
  }),
259
311
  /**
@@ -261,8 +313,8 @@ class PlaidClient {
261
313
  * @param {string} relationId - The relation ID
262
314
  * @param {string} spanId - The span ID
263
315
  */
264
- setSource: (relationId, spanId) =>
265
- this._request('PUT', `/api/v1/relations/${relationId}/source`, {
316
+ setSource: (relationId, spanId, auditMessage) =>
317
+ this._request('PUT', `/api/v1/relations/${relationId}/source`, { auditMessage,
266
318
  body: bodyOf({ 'span-id': spanId }),
267
319
  }),
268
320
  /**
@@ -275,22 +327,23 @@ class PlaidClient {
275
327
  * @param {any} value - The value
276
328
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
277
329
  */
278
- create: (layerId, sourceId, targetId, value, metadata) =>
279
- this._request('POST', '/api/v1/relations', {
330
+ create: (layerId, sourceId, targetId, value, metadata, auditMessage) =>
331
+ this._request('POST', '/api/v1/relations', { auditMessage,
280
332
  body: bodyOf({ 'layer-id': layerId, 'source-id': sourceId, 'target-id': targetId, value, metadata }),
281
333
  }),
282
334
  /**
283
335
  * Create multiple relations in a single operation.
284
336
  * @param {Array} body - The request body
337
+ * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
285
338
  */
286
- bulkCreate: (body) =>
287
- this._request('POST', '/api/v1/relations/bulk', { body }),
339
+ bulkCreate: (body, auditMessage) =>
340
+ this._request('POST', '/api/v1/relations/bulk', { auditMessage, body }),
288
341
  /**
289
342
  * Delete multiple relations in a single operation. Provide an array of IDs.
290
343
  * @param {Array} body - The request body
291
344
  */
292
- bulkDelete: (body) =>
293
- this._request('DELETE', '/api/v1/relations/bulk', { body }),
345
+ bulkDelete: (body, auditMessage) =>
346
+ this._request('DELETE', '/api/v1/relations/bulk', { auditMessage, body }),
294
347
  };
295
348
 
296
349
  this.spanLayers = {
@@ -301,8 +354,8 @@ class PlaidClient {
301
354
  * @param {string} configKey - The config key
302
355
  * @param {any} configValue - Configuration value to set
303
356
  */
304
- setConfig: (spanLayerId, namespace, configKey, configValue) =>
305
- this._request('PUT', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, {
357
+ setConfig: (spanLayerId, namespace, configKey, configValue, auditMessage) =>
358
+ this._request('PUT', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, { auditMessage,
306
359
  rawBody: configValue, skipResponseTransform: true,
307
360
  }),
308
361
  /**
@@ -311,8 +364,8 @@ class PlaidClient {
311
364
  * @param {string} namespace - The config namespace
312
365
  * @param {string} configKey - The config key
313
366
  */
314
- deleteConfig: (spanLayerId, namespace, configKey) =>
315
- this._request('DELETE', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, {
367
+ deleteConfig: (spanLayerId, namespace, configKey, auditMessage) =>
368
+ this._request('DELETE', `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`, { auditMessage,
316
369
  skipResponseTransform: true,
317
370
  }),
318
371
  /**
@@ -328,15 +381,15 @@ class PlaidClient {
328
381
  * Delete a span layer.
329
382
  * @param {string} spanLayerId - The span layer ID
330
383
  */
331
- delete: (spanLayerId) =>
332
- this._request('DELETE', `/api/v1/span-layers/${spanLayerId}`),
384
+ delete: (spanLayerId, auditMessage) =>
385
+ this._request('DELETE', `/api/v1/span-layers/${spanLayerId}`, { auditMessage }),
333
386
  /**
334
387
  * Update a span layer's name.
335
388
  * @param {string} spanLayerId - The span layer ID
336
389
  * @param {string} name - The name
337
390
  */
338
- update: (spanLayerId, name) =>
339
- this._request('PATCH', `/api/v1/span-layers/${spanLayerId}`, {
391
+ update: (spanLayerId, name, auditMessage) =>
392
+ this._request('PATCH', `/api/v1/span-layers/${spanLayerId}`, { auditMessage,
340
393
  body: bodyOf({ name }),
341
394
  }),
342
395
  /**
@@ -344,8 +397,8 @@ class PlaidClient {
344
397
  * @param {string} tokenLayerId - The token layer ID
345
398
  * @param {string} name - The name
346
399
  */
347
- create: (tokenLayerId, name) =>
348
- this._request('POST', '/api/v1/span-layers', {
400
+ create: (tokenLayerId, name, auditMessage) =>
401
+ this._request('POST', '/api/v1/span-layers', { auditMessage,
349
402
  body: bodyOf({ 'token-layer-id': tokenLayerId, name }),
350
403
  }),
351
404
  /**
@@ -353,8 +406,8 @@ class PlaidClient {
353
406
  * @param {string} spanLayerId - The span layer ID
354
407
  * @param {string} direction - The direction ("up" or "down")
355
408
  */
356
- shift: (spanLayerId, direction) =>
357
- this._request('POST', `/api/v1/span-layers/${spanLayerId}/shift`, {
409
+ shift: (spanLayerId, direction, auditMessage) =>
410
+ this._request('POST', `/api/v1/span-layers/${spanLayerId}/shift`, { auditMessage,
358
411
  body: bodyOf({ direction }),
359
412
  }),
360
413
  };
@@ -365,8 +418,8 @@ class PlaidClient {
365
418
  * @param {string} spanId - The span ID
366
419
  * @param {Array} tokens - The tokens
367
420
  */
368
- setTokens: (spanId, tokens) =>
369
- this._request('PUT', `/api/v1/spans/${spanId}/tokens`, {
421
+ setTokens: (spanId, tokens, auditMessage) =>
422
+ this._request('PUT', `/api/v1/spans/${spanId}/tokens`, { auditMessage,
370
423
  body: bodyOf({ tokens }),
371
424
  }),
372
425
  /**
@@ -377,8 +430,8 @@ class PlaidClient {
377
430
  * @param {any} value - The value
378
431
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
379
432
  */
380
- create: (spanLayerId, tokens, value, metadata) =>
381
- this._request('POST', '/api/v1/spans', {
433
+ create: (spanLayerId, tokens, value, metadata, auditMessage) =>
434
+ this._request('POST', '/api/v1/spans', { auditMessage,
382
435
  body: bodyOf({ 'span-layer-id': spanLayerId, tokens, value, metadata }),
383
436
  }),
384
437
  /**
@@ -394,44 +447,45 @@ class PlaidClient {
394
447
  * Delete a span.
395
448
  * @param {string} spanId - The span ID
396
449
  */
397
- delete: (spanId) =>
398
- this._request('DELETE', `/api/v1/spans/${spanId}`),
450
+ delete: (spanId, auditMessage) =>
451
+ this._request('DELETE', `/api/v1/spans/${spanId}`, { auditMessage }),
399
452
  /**
400
453
  * Update a span's value.
401
454
  * @param {string} spanId - The span ID
402
455
  * @param {any} value - The value
403
456
  */
404
- update: (spanId, value) =>
405
- this._request('PATCH', `/api/v1/spans/${spanId}`, {
457
+ update: (spanId, value, auditMessage) =>
458
+ this._request('PATCH', `/api/v1/spans/${spanId}`, { auditMessage,
406
459
  body: bodyOf({ value }),
407
460
  }),
408
461
  /**
409
462
  * Create multiple spans in a single operation.
410
463
  * @param {Array} body - The request body
464
+ * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
411
465
  */
412
- bulkCreate: (body) =>
413
- this._request('POST', '/api/v1/spans/bulk', { body }),
466
+ bulkCreate: (body, auditMessage) =>
467
+ this._request('POST', '/api/v1/spans/bulk', { auditMessage, body }),
414
468
  /**
415
469
  * Delete multiple spans in a single operation. Provide an array of IDs.
416
470
  * @param {Array} body - The request body
417
471
  */
418
- bulkDelete: (body) =>
419
- this._request('DELETE', '/api/v1/spans/bulk', { body }),
472
+ bulkDelete: (body, auditMessage) =>
473
+ this._request('DELETE', '/api/v1/spans/bulk', { auditMessage, body }),
420
474
  /**
421
475
  * Replace all metadata for a span.
422
476
  * @param {string} spanId - The span ID
423
477
  * @param {any} body - The request body
424
478
  */
425
- setMetadata: (spanId, body) =>
426
- this._request('PUT', `/api/v1/spans/${spanId}/metadata`, {
479
+ setMetadata: (spanId, body, auditMessage) =>
480
+ this._request('PUT', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
427
481
  rawBody: body, skipResponseTransform: true,
428
482
  }),
429
483
  /**
430
484
  * Remove all metadata from a span.
431
485
  * @param {string} spanId - The span ID
432
486
  */
433
- deleteMetadata: (spanId) =>
434
- this._request('DELETE', `/api/v1/spans/${spanId}/metadata`, {
487
+ deleteMetadata: (spanId, auditMessage) =>
488
+ this._request('DELETE', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
435
489
  skipResponseTransform: true,
436
490
  }),
437
491
  /**
@@ -439,8 +493,8 @@ class PlaidClient {
439
493
  * @param {string} spanId - The span ID
440
494
  * @param {any} body - The metadata patch
441
495
  */
442
- patchMetadata: (spanId, body) =>
443
- this._request('PATCH', `/api/v1/spans/${spanId}/metadata`, {
496
+ patchMetadata: (spanId, body, auditMessage) =>
497
+ this._request('PATCH', `/api/v1/spans/${spanId}/metadata`, { auditMessage,
444
498
  rawBody: body, skipResponseTransform: true,
445
499
  }),
446
500
  };
@@ -451,8 +505,8 @@ class PlaidClient {
451
505
  * changes are rolled back.
452
506
  * @param {Array} body - The request body
453
507
  */
454
- submit: (body) =>
455
- this._request('POST', '/api/v1/batch', {
508
+ submit: (body, auditMessage) =>
509
+ this._request('POST', '/api/v1/batch', { auditMessage,
456
510
  body, noBatch: true,
457
511
  }),
458
512
  };
@@ -463,16 +517,16 @@ class PlaidClient {
463
517
  * @param {string} textId - The text ID
464
518
  * @param {any} body - The request body
465
519
  */
466
- setMetadata: (textId, body) =>
467
- this._request('PUT', `/api/v1/texts/${textId}/metadata`, {
520
+ setMetadata: (textId, body, auditMessage) =>
521
+ this._request('PUT', `/api/v1/texts/${textId}/metadata`, { auditMessage,
468
522
  rawBody: body, skipResponseTransform: true,
469
523
  }),
470
524
  /**
471
525
  * Remove all metadata from a text.
472
526
  * @param {string} textId - The text ID
473
527
  */
474
- deleteMetadata: (textId) =>
475
- this._request('DELETE', `/api/v1/texts/${textId}/metadata`, {
528
+ deleteMetadata: (textId, auditMessage) =>
529
+ this._request('DELETE', `/api/v1/texts/${textId}/metadata`, { auditMessage,
476
530
  skipResponseTransform: true,
477
531
  }),
478
532
  /**
@@ -480,8 +534,8 @@ class PlaidClient {
480
534
  * @param {string} textId - The text ID
481
535
  * @param {any} body - The metadata patch
482
536
  */
483
- patchMetadata: (textId, body) =>
484
- this._request('PATCH', `/api/v1/texts/${textId}/metadata`, {
537
+ patchMetadata: (textId, body, auditMessage) =>
538
+ this._request('PATCH', `/api/v1/texts/${textId}/metadata`, { auditMessage,
485
539
  rawBody: body, skipResponseTransform: true,
486
540
  }),
487
541
  /**
@@ -492,8 +546,8 @@ class PlaidClient {
492
546
  * @param {string} body - The request body
493
547
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
494
548
  */
495
- create: (textLayerId, documentId, body, metadata) =>
496
- this._request('POST', '/api/v1/texts', {
549
+ create: (textLayerId, documentId, body, metadata, auditMessage) =>
550
+ this._request('POST', '/api/v1/texts', { auditMessage,
497
551
  body: bodyOf({ 'text-layer-id': textLayerId, 'document-id': documentId, body, metadata }),
498
552
  }),
499
553
  /**
@@ -509,8 +563,8 @@ class PlaidClient {
509
563
  * Delete a text and all dependent data.
510
564
  * @param {string} textId - The text ID
511
565
  */
512
- delete: (textId) =>
513
- this._request('DELETE', `/api/v1/texts/${textId}`),
566
+ delete: (textId, auditMessage) =>
567
+ this._request('DELETE', `/api/v1/texts/${textId}`, { auditMessage }),
514
568
  /**
515
569
  * Update a text's body. A diff is computed and token indices are updated
516
570
  * so that tokens remain intact. Alternatively, `body` can be a list of
@@ -518,8 +572,8 @@ class PlaidClient {
518
572
  * @param {string} textId - The text ID
519
573
  * @param {any} body - The request body
520
574
  */
521
- update: (textId, body) =>
522
- this._request('PATCH', `/api/v1/texts/${textId}`, {
575
+ update: (textId, body, auditMessage) =>
576
+ this._request('PATCH', `/api/v1/texts/${textId}`, { auditMessage,
523
577
  body: bodyOf({ body }),
524
578
  }),
525
579
  };
@@ -563,8 +617,8 @@ class PlaidClient {
563
617
  * @param {string} password - The password
564
618
  * @param {boolean} isAdmin - Whether the user is an admin
565
619
  */
566
- create: (username, password, isAdmin) =>
567
- this._request('POST', '/api/v1/users', {
620
+ create: (username, password, isAdmin, auditMessage) =>
621
+ this._request('POST', '/api/v1/users', { auditMessage,
568
622
  body: bodyOf({ username, password, 'is-admin': isAdmin }),
569
623
  }),
570
624
  /**
@@ -597,16 +651,16 @@ class PlaidClient {
597
651
  * Reversible via activate(), which restores login only.
598
652
  * @param {string} id - The resource ID
599
653
  */
600
- delete: (id) =>
601
- this._request('DELETE', `/api/v1/users/${id}`),
654
+ delete: (id, auditMessage) =>
655
+ this._request('DELETE', `/api/v1/users/${id}`, { auditMessage }),
602
656
  /**
603
657
  * Reactivate a deactivated user, restoring their ability to log in.
604
658
  * Project memberships, vocab maintainerships, and API tokens removed
605
659
  * at deactivation are NOT restored — re-grant them deliberately.
606
660
  * @param {string} id - The resource ID
607
661
  */
608
- activate: (id) =>
609
- this._request('POST', `/api/v1/users/${id}/activate`),
662
+ activate: (id, auditMessage) =>
663
+ this._request('POST', `/api/v1/users/${id}/activate`, { auditMessage }),
610
664
  /**
611
665
  * Modify a user. Admins may change the username, password, and admin
612
666
  * status of any user. All other users may only modify their own username
@@ -616,10 +670,64 @@ class PlaidClient {
616
670
  * @param {string} [username] - New username
617
671
  * @param {boolean} [isAdmin] - New admin status
618
672
  */
619
- update: (id, password, username, isAdmin) =>
620
- this._request('PATCH', `/api/v1/users/${id}`, {
673
+ update: (id, password, username, isAdmin, auditMessage) =>
674
+ this._request('PATCH', `/api/v1/users/${id}`, { auditMessage,
621
675
  body: bodyOf({ password, username, 'is-admin': isAdmin }),
622
676
  }),
677
+ /**
678
+ * Build a URL for a user's profile picture, suitable for use directly as
679
+ * an `<img>` src. An image element cannot send an Authorization header,
680
+ * so the session token rides in the query string, the same way document
681
+ * media does.
682
+ *
683
+ * Pass the user record's `avatarHash` as the second argument whenever you
684
+ * have it: the URL then addresses that exact picture, so the browser can
685
+ * cache it indefinitely and still pick up a replacement the moment the
686
+ * user changes it. Returns null when the user has no picture, so callers
687
+ * can fall back to initials without a wasted request.
688
+ * @param {string} id - The user ID
689
+ * @param {string} [avatarHash] - The user record's `avatarHash`
690
+ * @returns {string|null}
691
+ */
692
+ avatarUrl: (id, avatarHash) => {
693
+ if (avatarHash === null) return null;
694
+ const params = new URLSearchParams({ token: this.token });
695
+ if (avatarHash) params.set('v', avatarHash);
696
+ return `${this.baseUrl}/api/v1/users/${id}/avatar?${params}`;
697
+ },
698
+ /**
699
+ * Fetch a user's profile picture as raw bytes. Most callers want
700
+ * avatarUrl() instead. This one is for non-browser consumers.
701
+ * @param {string} id - The user ID
702
+ */
703
+ getAvatar: (id) =>
704
+ this._request('GET', `/api/v1/users/${id}/avatar`, {
705
+ noBatch: true,
706
+ binaryResponse: true,
707
+ }),
708
+ /**
709
+ * Upload a profile picture. Your own, or anyone's if you are an admin.
710
+ * The server center-crops to a square, scales to the configured edge
711
+ * length, and re-encodes, so no client-side resizing is needed. Accepts
712
+ * PNG, JPEG, WebP, and GIF. Resolves to the updated user record.
713
+ * @param {string} id - The user ID
714
+ * @param {File} file - The image to upload
715
+ */
716
+ setAvatar: (id, file, auditMessage) => {
717
+ 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,
721
+ });
722
+ },
723
+ /**
724
+ * Remove a profile picture. Your own, or anyone's if you are an admin.
725
+ * @param {string} id - The user ID
726
+ */
727
+ deleteAvatar: (id, auditMessage) =>
728
+ this._request('DELETE', `/api/v1/users/${id}/avatar`, { auditMessage,
729
+ noBatch: true,
730
+ }),
623
731
  };
624
732
 
625
733
  this.apiTokens = {
@@ -661,8 +769,8 @@ class PlaidClient {
661
769
  * @param {string} name - A human label, e.g. "Stanza Parser"
662
770
  * @returns {Promise<{id: string, name: string, token: string}>}
663
771
  */
664
- create: (userId, name) =>
665
- this._request('POST', `/api/v1/users/${userId}/tokens`, {
772
+ create: (userId, name, auditMessage) =>
773
+ this._request('POST', `/api/v1/users/${userId}/tokens`, { auditMessage,
666
774
  body: bodyOf({ name }),
667
775
  }),
668
776
  /**
@@ -670,8 +778,76 @@ class PlaidClient {
670
778
  * @param {string} userId - The user ID who owns the token
671
779
  * @param {string} tokenId - The token ID to revoke
672
780
  */
673
- revoke: (userId, tokenId) =>
674
- this._request('DELETE', `/api/v1/users/${userId}/tokens/${tokenId}`),
781
+ revoke: (userId, tokenId, auditMessage) =>
782
+ this._request('DELETE', `/api/v1/users/${userId}/tokens/${tokenId}`, { auditMessage }),
783
+ };
784
+
785
+ this.invites = {
786
+ /**
787
+ * List invites you minted, oldest first. With `projectId`, lists that
788
+ * project's invites instead (including co-maintainers'), which requires
789
+ * maintainer or admin on it. Never includes invite codes — the code is
790
+ * returned once, by create(), and is not recoverable afterward.
791
+ * Transparently follows pagination cursors and returns the full flat array.
792
+ * Cannot be used inside a batch (auto-paginates across requests); throws if called while batching — use listPage() for a single page in a batch.
793
+ * @param {object} [opts]
794
+ * @param {string} [opts.projectId] - List this project's invites instead of your own
795
+ */
796
+ list: ({ projectId } = {}) =>
797
+ listAll(this, '/api/v1/invites', { query: { 'project-id': projectId } }),
798
+ /**
799
+ * Fetch a single page of invites.
800
+ * @param {object} [opts]
801
+ * @param {string} [opts.projectId] - List this project's invites instead of your own
802
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
803
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
804
+ * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
805
+ */
806
+ listPage: ({ projectId, limit, cursor } = {}) =>
807
+ listPage(this, '/api/v1/invites', { limit, cursor, query: { 'project-id': projectId } }),
808
+ /**
809
+ * Async-iterate invites page by page; yields each page's entries array.
810
+ * @param {object} [opts]
811
+ * @param {string} [opts.projectId] - List this project's invites instead of your own
812
+ * @param {number} [opts.pageSize] - Per-request page size
813
+ * Cannot be used inside a batch (auto-paginates across requests); throws on first iteration if called while batching — use listPage() for a single page in a batch.
814
+ * @returns {AsyncGenerator<Array>}
815
+ */
816
+ iterPages: ({ projectId, pageSize } = {}) =>
817
+ iterPages(this, '/api/v1/invites', { pageSize, query: { 'project-id': projectId } }),
818
+ /**
819
+ * Mint an invite. The returned `code` is shown ONLY here — it is never
820
+ * stored and cannot be recovered, so build and hand off the link now.
821
+ * Use PlaidClient.inviteUrl() to turn it into one.
822
+ *
823
+ * Admins may mint anything. A project maintainer may mint role grants on
824
+ * 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"
829
+ * @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)
831
+ * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
832
+ * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
833
+ * @param {string} [opts.note] - Human label shown in your invite list
834
+ * @returns {Promise<{id: string, code: string, kind: string, status: string}>}
835
+ */
836
+ create: ({ projectId, projectRole, grantAdmin, targetUserId, maxUses, ttlDays, note } = {}, auditMessage) =>
837
+ this._request('POST', '/api/v1/invites', { auditMessage,
838
+ 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,
842
+ }),
843
+ }),
844
+ /**
845
+ * Revoke an invite, killing the link immediately. Idempotent. Allowed for
846
+ * the creator, an admin, or a maintainer of the invite's project.
847
+ * @param {string} id - The invite ID
848
+ */
849
+ revoke: (id, auditMessage) =>
850
+ this._request('DELETE', `/api/v1/invites/${id}`, { auditMessage }),
675
851
  };
676
852
 
677
853
  this.tokenLayers = {
@@ -680,8 +856,8 @@ class PlaidClient {
680
856
  * @param {string} tokenLayerId - The token layer ID
681
857
  * @param {string} direction - The direction ("up" or "down")
682
858
  */
683
- shift: (tokenLayerId, direction) =>
684
- this._request('POST', `/api/v1/token-layers/${tokenLayerId}/shift`, {
859
+ shift: (tokenLayerId, direction, auditMessage) =>
860
+ this._request('POST', `/api/v1/token-layers/${tokenLayerId}/shift`, { auditMessage,
685
861
  body: bodyOf({ direction }),
686
862
  }),
687
863
  /**
@@ -691,8 +867,8 @@ class PlaidClient {
691
867
  * @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.
692
868
  * @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).
693
869
  */
694
- create: (textLayerId, name, overlapMode, parentTokenLayerId) =>
695
- this._request('POST', '/api/v1/token-layers', {
870
+ create: (textLayerId, name, overlapMode, parentTokenLayerId, auditMessage) =>
871
+ this._request('POST', '/api/v1/token-layers', { auditMessage,
696
872
  body: bodyOf({ 'text-layer-id': textLayerId, name, 'overlap-mode': overlapMode, 'parent-token-layer-id': parentTokenLayerId }),
697
873
  }),
698
874
  /**
@@ -702,8 +878,8 @@ class PlaidClient {
702
878
  * @param {string} configKey - The config key
703
879
  * @param {any} configValue - Configuration value to set
704
880
  */
705
- setConfig: (tokenLayerId, namespace, configKey, configValue) =>
706
- this._request('PUT', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, {
881
+ setConfig: (tokenLayerId, namespace, configKey, configValue, auditMessage) =>
882
+ this._request('PUT', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, { auditMessage,
707
883
  rawBody: configValue, skipResponseTransform: true,
708
884
  }),
709
885
  /**
@@ -712,8 +888,8 @@ class PlaidClient {
712
888
  * @param {string} namespace - The config namespace
713
889
  * @param {string} configKey - The config key
714
890
  */
715
- deleteConfig: (tokenLayerId, namespace, configKey) =>
716
- this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, {
891
+ deleteConfig: (tokenLayerId, namespace, configKey, auditMessage) =>
892
+ this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`, { auditMessage,
717
893
  skipResponseTransform: true,
718
894
  }),
719
895
  /**
@@ -729,15 +905,15 @@ class PlaidClient {
729
905
  * Delete a token layer.
730
906
  * @param {string} tokenLayerId - The token layer ID
731
907
  */
732
- delete: (tokenLayerId) =>
733
- this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}`),
908
+ delete: (tokenLayerId, auditMessage) =>
909
+ this._request('DELETE', `/api/v1/token-layers/${tokenLayerId}`, { auditMessage }),
734
910
  /**
735
911
  * Update a token layer's name.
736
912
  * @param {string} tokenLayerId - The token layer ID
737
913
  * @param {string} name - The name
738
914
  */
739
- update: (tokenLayerId, name) =>
740
- this._request('PATCH', `/api/v1/token-layers/${tokenLayerId}`, {
915
+ update: (tokenLayerId, name, auditMessage) =>
916
+ this._request('PATCH', `/api/v1/token-layers/${tokenLayerId}`, { auditMessage,
741
917
  body: bodyOf({ name }),
742
918
  }),
743
919
  };
@@ -756,14 +932,14 @@ class PlaidClient {
756
932
  * Acquire or refresh a document lock
757
933
  * @param {string} documentId - The document ID
758
934
  */
759
- acquireLock: (documentId) =>
760
- this._request('POST', `/api/v1/documents/${documentId}/lock`),
935
+ acquireLock: (documentId, auditMessage) =>
936
+ this._request('POST', `/api/v1/documents/${documentId}/lock`, { auditMessage }),
761
937
  /**
762
938
  * Release a document lock
763
939
  * @param {string} documentId - The document ID
764
940
  */
765
- releaseLock: (documentId) =>
766
- this._request('DELETE', `/api/v1/documents/${documentId}/lock`),
941
+ releaseLock: (documentId, auditMessage) =>
942
+ this._request('DELETE', `/api/v1/documents/${documentId}/lock`, { auditMessage }),
767
943
  /**
768
944
  * Get media file for a document
769
945
  * @param {string} documentId - The document ID
@@ -780,10 +956,10 @@ class PlaidClient {
780
956
  * @param {string} documentId - The document ID
781
957
  * @param {File} file - The file to upload
782
958
  */
783
- uploadMedia: (documentId, file) => {
959
+ uploadMedia: (documentId, file, auditMessage) => {
784
960
  const fd = new FormData();
785
961
  fd.append('file', file);
786
- return this._request('PUT', `/api/v1/documents/${documentId}/media`, {
962
+ return this._request('PUT', `/api/v1/documents/${documentId}/media`, { auditMessage,
787
963
  body: fd, formData: true, noBatch: true,
788
964
  });
789
965
  },
@@ -791,8 +967,8 @@ class PlaidClient {
791
967
  * Delete media file for a document
792
968
  * @param {string} documentId - The document ID
793
969
  */
794
- deleteMedia: (documentId) =>
795
- this._request('DELETE', `/api/v1/documents/${documentId}/media`, {
970
+ deleteMedia: (documentId, auditMessage) =>
971
+ this._request('DELETE', `/api/v1/documents/${documentId}/media`, { auditMessage,
796
972
  noBatch: true,
797
973
  }),
798
974
  /**
@@ -800,16 +976,16 @@ class PlaidClient {
800
976
  * @param {string} documentId - The document ID
801
977
  * @param {any} body - The request body
802
978
  */
803
- setMetadata: (documentId, body) =>
804
- this._request('PUT', `/api/v1/documents/${documentId}/metadata`, {
979
+ setMetadata: (documentId, body, auditMessage) =>
980
+ this._request('PUT', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
805
981
  rawBody: body, skipResponseTransform: true,
806
982
  }),
807
983
  /**
808
984
  * Remove all metadata from a document.
809
985
  * @param {string} documentId - The document ID
810
986
  */
811
- deleteMetadata: (documentId) =>
812
- this._request('DELETE', `/api/v1/documents/${documentId}/metadata`, {
987
+ deleteMetadata: (documentId, auditMessage) =>
988
+ this._request('DELETE', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
813
989
  skipResponseTransform: true,
814
990
  }),
815
991
  /**
@@ -817,8 +993,8 @@ class PlaidClient {
817
993
  * @param {string} documentId - The document ID
818
994
  * @param {any} body - The metadata patch
819
995
  */
820
- patchMetadata: (documentId, body) =>
821
- this._request('PATCH', `/api/v1/documents/${documentId}/metadata`, {
996
+ patchMetadata: (documentId, body, auditMessage) =>
997
+ this._request('PATCH', `/api/v1/documents/${documentId}/metadata`, { auditMessage,
822
998
  rawBody: body, skipResponseTransform: true,
823
999
  }),
824
1000
  /**
@@ -848,15 +1024,15 @@ class PlaidClient {
848
1024
  * Delete a document and all data contained.
849
1025
  * @param {string} documentId - The document ID
850
1026
  */
851
- delete: (documentId) =>
852
- this._request('DELETE', `/api/v1/documents/${documentId}`),
1027
+ delete: (documentId, auditMessage) =>
1028
+ this._request('DELETE', `/api/v1/documents/${documentId}`, { auditMessage }),
853
1029
  /**
854
1030
  * Update a document's name.
855
1031
  * @param {string} documentId - The document ID
856
1032
  * @param {string} name - The name
857
1033
  */
858
- update: (documentId, name) =>
859
- this._request('PATCH', `/api/v1/documents/${documentId}`, {
1034
+ update: (documentId, name, auditMessage) =>
1035
+ this._request('PATCH', `/api/v1/documents/${documentId}`, { auditMessage,
860
1036
  body: bodyOf({ name }),
861
1037
  }),
862
1038
  /**
@@ -865,8 +1041,8 @@ class PlaidClient {
865
1041
  * @param {string} name - The name
866
1042
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
867
1043
  */
868
- create: (projectId, name, metadata) =>
869
- this._request('POST', '/api/v1/documents', {
1044
+ create: (projectId, name, metadata, auditMessage) =>
1045
+ this._request('POST', '/api/v1/documents', { auditMessage,
870
1046
  body: bodyOf({ 'project-id': projectId, name, metadata }),
871
1047
  }),
872
1048
  };
@@ -877,29 +1053,29 @@ class PlaidClient {
877
1053
  * @param {string} id - The resource ID
878
1054
  * @param {string} userId - The user ID
879
1055
  */
880
- addWriter: (id, userId) =>
881
- this._request('POST', `/api/v1/projects/${id}/writers/${userId}`),
1056
+ addWriter: (id, userId, auditMessage) =>
1057
+ this._request('POST', `/api/v1/projects/${id}/writers/${userId}`, { auditMessage }),
882
1058
  /**
883
1059
  * Remove a user's writer privileges for this project.
884
1060
  * @param {string} id - The resource ID
885
1061
  * @param {string} userId - The user ID
886
1062
  */
887
- removeWriter: (id, userId) =>
888
- this._request('DELETE', `/api/v1/projects/${id}/writers/${userId}`),
1063
+ removeWriter: (id, userId, auditMessage) =>
1064
+ this._request('DELETE', `/api/v1/projects/${id}/writers/${userId}`, { auditMessage }),
889
1065
  /**
890
1066
  * Set a user's access level to read-only for this project.
891
1067
  * @param {string} id - The resource ID
892
1068
  * @param {string} userId - The user ID
893
1069
  */
894
- addReader: (id, userId) =>
895
- this._request('POST', `/api/v1/projects/${id}/readers/${userId}`),
1070
+ addReader: (id, userId, auditMessage) =>
1071
+ this._request('POST', `/api/v1/projects/${id}/readers/${userId}`, { auditMessage }),
896
1072
  /**
897
1073
  * Remove a user's reader privileges for this project.
898
1074
  * @param {string} id - The resource ID
899
1075
  * @param {string} userId - The user ID
900
1076
  */
901
- removeReader: (id, userId) =>
902
- this._request('DELETE', `/api/v1/projects/${id}/readers/${userId}`),
1077
+ removeReader: (id, userId, auditMessage) =>
1078
+ this._request('DELETE', `/api/v1/projects/${id}/readers/${userId}`, { auditMessage }),
903
1079
  /**
904
1080
  * Set a configuration value for a project in an editor namespace.
905
1081
  * @param {string} id - The resource ID
@@ -907,8 +1083,8 @@ class PlaidClient {
907
1083
  * @param {string} configKey - The config key
908
1084
  * @param {any} configValue - Configuration value to set
909
1085
  */
910
- setConfig: (id, namespace, configKey, configValue) =>
911
- this._request('PUT', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, {
1086
+ setConfig: (id, namespace, configKey, configValue, auditMessage) =>
1087
+ this._request('PUT', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, { auditMessage,
912
1088
  rawBody: configValue, skipResponseTransform: true,
913
1089
  }),
914
1090
  /**
@@ -917,8 +1093,8 @@ class PlaidClient {
917
1093
  * @param {string} namespace - The config namespace
918
1094
  * @param {string} configKey - The config key
919
1095
  */
920
- deleteConfig: (id, namespace, configKey) =>
921
- this._request('DELETE', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, {
1096
+ deleteConfig: (id, namespace, configKey, auditMessage) =>
1097
+ this._request('DELETE', `/api/v1/projects/${id}/config/${namespace}/${configKey}`, { auditMessage,
922
1098
  skipResponseTransform: true,
923
1099
  }),
924
1100
  /**
@@ -926,15 +1102,15 @@ class PlaidClient {
926
1102
  * @param {string} id - The resource ID
927
1103
  * @param {string} userId - The user ID
928
1104
  */
929
- addMaintainer: (id, userId) =>
930
- this._request('POST', `/api/v1/projects/${id}/maintainers/${userId}`),
1105
+ addMaintainer: (id, userId, auditMessage) =>
1106
+ this._request('POST', `/api/v1/projects/${id}/maintainers/${userId}`, { auditMessage }),
931
1107
  /**
932
1108
  * Remove a user's maintainer privileges for this project.
933
1109
  * @param {string} id - The resource ID
934
1110
  * @param {string} userId - The user ID
935
1111
  */
936
- removeMaintainer: (id, userId) =>
937
- this._request('DELETE', `/api/v1/projects/${id}/maintainers/${userId}`),
1112
+ removeMaintainer: (id, userId, auditMessage) =>
1113
+ this._request('DELETE', `/api/v1/projects/${id}/maintainers/${userId}`, { auditMessage }),
938
1114
  /**
939
1115
  * Get audit log for a project. Transparently follows pagination cursors
940
1116
  * and returns the full flat array.
@@ -953,15 +1129,15 @@ class PlaidClient {
953
1129
  * @param {string} id - The resource ID
954
1130
  * @param {string} vocabId - The vocab layer ID
955
1131
  */
956
- linkVocab: (id, vocabId) =>
957
- this._request('POST', `/api/v1/projects/${id}/vocabs/${vocabId}`),
1132
+ linkVocab: (id, vocabId, auditMessage) =>
1133
+ this._request('POST', `/api/v1/projects/${id}/vocabs/${vocabId}`, { auditMessage }),
958
1134
  /**
959
1135
  * Unlink a vocabulary from a project.
960
1136
  * @param {string} id - The resource ID
961
1137
  * @param {string} vocabId - The vocab layer ID
962
1138
  */
963
- unlinkVocab: (id, vocabId) =>
964
- this._request('DELETE', `/api/v1/projects/${id}/vocabs/${vocabId}`),
1139
+ unlinkVocab: (id, vocabId, auditMessage) =>
1140
+ this._request('DELETE', `/api/v1/projects/${id}/vocabs/${vocabId}`, { auditMessage }),
965
1141
  /**
966
1142
  * Get a project by ID. To fetch the project's documents, use
967
1143
  * listDocuments(id) — the include-documents flag has been removed.
@@ -1011,18 +1187,25 @@ class PlaidClient {
1011
1187
  iterDocuments: (id, { pageSize } = {}) =>
1012
1188
  iterPages(this, `/api/v1/projects/${id}/documents`, { pageSize }),
1013
1189
  /**
1014
- * Delete a project.
1190
+ * Delete a project and everything in it. This is irrecoverable.
1191
+ *
1192
+ * For a large project this can take a long time, so the client's
1193
+ * per-request timeout is DISABLED by default. Pass `{ timeout }` to
1194
+ * impose a finite bound.
1015
1195
  * @param {string} id - The resource ID
1196
+ * @param {string} [auditMessage] - Custom audit-log message
1197
+ * @param {object} [options]
1198
+ * @param {number} [options.timeout=0] - Per-request timeout in ms (0/null disables)
1016
1199
  */
1017
- delete: (id) =>
1018
- this._request('DELETE', `/api/v1/projects/${id}`),
1200
+ delete: (id, auditMessage, { timeout = 0 } = {}) =>
1201
+ this._request('DELETE', `/api/v1/projects/${id}`, { auditMessage, timeout }),
1019
1202
  /**
1020
1203
  * Update a project's name.
1021
1204
  * @param {string} id - The resource ID
1022
1205
  * @param {string} name - The name
1023
1206
  */
1024
- update: (id, name) =>
1025
- this._request('PATCH', `/api/v1/projects/${id}`, {
1207
+ update: (id, name, auditMessage) =>
1208
+ this._request('PATCH', `/api/v1/projects/${id}`, { auditMessage,
1026
1209
  body: bodyOf({ name }),
1027
1210
  }),
1028
1211
  /**
@@ -1057,8 +1240,8 @@ class PlaidClient {
1057
1240
  * Create a new project. Note: this also registers the user as a maintainer.
1058
1241
  * @param {string} name - The name
1059
1242
  */
1060
- create: (name) =>
1061
- this._request('POST', '/api/v1/projects', {
1243
+ create: (name, auditMessage) =>
1244
+ this._request('POST', '/api/v1/projects', { auditMessage,
1062
1245
  body: bodyOf({ name }),
1063
1246
  }),
1064
1247
  };
@@ -1071,8 +1254,8 @@ class PlaidClient {
1071
1254
  * @param {string} configKey - The config key
1072
1255
  * @param {any} configValue - Configuration value to set
1073
1256
  */
1074
- setConfig: (textLayerId, namespace, configKey, configValue) =>
1075
- this._request('PUT', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, {
1257
+ setConfig: (textLayerId, namespace, configKey, configValue, auditMessage) =>
1258
+ this._request('PUT', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1076
1259
  rawBody: configValue, skipResponseTransform: true,
1077
1260
  }),
1078
1261
  /**
@@ -1081,8 +1264,8 @@ class PlaidClient {
1081
1264
  * @param {string} namespace - The config namespace
1082
1265
  * @param {string} configKey - The config key
1083
1266
  */
1084
- deleteConfig: (textLayerId, namespace, configKey) =>
1085
- this._request('DELETE', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, {
1267
+ deleteConfig: (textLayerId, namespace, configKey, auditMessage) =>
1268
+ this._request('DELETE', `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1086
1269
  skipResponseTransform: true,
1087
1270
  }),
1088
1271
  /**
@@ -1098,15 +1281,15 @@ class PlaidClient {
1098
1281
  * Delete a text layer.
1099
1282
  * @param {string} textLayerId - The text layer ID
1100
1283
  */
1101
- delete: (textLayerId) =>
1102
- this._request('DELETE', `/api/v1/text-layers/${textLayerId}`),
1284
+ delete: (textLayerId, auditMessage) =>
1285
+ this._request('DELETE', `/api/v1/text-layers/${textLayerId}`, { auditMessage }),
1103
1286
  /**
1104
1287
  * Update a text layer's name.
1105
1288
  * @param {string} textLayerId - The text layer ID
1106
1289
  * @param {string} name - The name
1107
1290
  */
1108
- update: (textLayerId, name) =>
1109
- this._request('PATCH', `/api/v1/text-layers/${textLayerId}`, {
1291
+ update: (textLayerId, name, auditMessage) =>
1292
+ this._request('PATCH', `/api/v1/text-layers/${textLayerId}`, { auditMessage,
1110
1293
  body: bodyOf({ name }),
1111
1294
  }),
1112
1295
  /**
@@ -1114,8 +1297,8 @@ class PlaidClient {
1114
1297
  * @param {string} textLayerId - The text layer ID
1115
1298
  * @param {string} direction - The direction ("up" or "down")
1116
1299
  */
1117
- shift: (textLayerId, direction) =>
1118
- this._request('POST', `/api/v1/text-layers/${textLayerId}/shift`, {
1300
+ shift: (textLayerId, direction, auditMessage) =>
1301
+ this._request('POST', `/api/v1/text-layers/${textLayerId}/shift`, { auditMessage,
1119
1302
  body: bodyOf({ direction }),
1120
1303
  }),
1121
1304
  /**
@@ -1123,8 +1306,8 @@ class PlaidClient {
1123
1306
  * @param {string} projectId - The project ID
1124
1307
  * @param {string} name - The name
1125
1308
  */
1126
- create: (projectId, name) =>
1127
- this._request('POST', '/api/v1/text-layers', {
1309
+ create: (projectId, name, auditMessage) =>
1310
+ this._request('POST', '/api/v1/text-layers', { auditMessage,
1128
1311
  body: bodyOf({ 'project-id': projectId, name }),
1129
1312
  }),
1130
1313
  };
@@ -1135,16 +1318,16 @@ class PlaidClient {
1135
1318
  * @param {string} id - The resource ID
1136
1319
  * @param {any} body - The request body
1137
1320
  */
1138
- setMetadata: (id, body) =>
1139
- this._request('PUT', `/api/v1/vocab-items/${id}/metadata`, {
1321
+ setMetadata: (id, body, auditMessage) =>
1322
+ this._request('PUT', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1140
1323
  rawBody: body, skipResponseTransform: true,
1141
1324
  }),
1142
1325
  /**
1143
1326
  * Remove all metadata from a vocab item.
1144
1327
  * @param {string} id - The resource ID
1145
1328
  */
1146
- deleteMetadata: (id) =>
1147
- this._request('DELETE', `/api/v1/vocab-items/${id}/metadata`, {
1329
+ deleteMetadata: (id, auditMessage) =>
1330
+ this._request('DELETE', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1148
1331
  skipResponseTransform: true,
1149
1332
  }),
1150
1333
  /**
@@ -1152,8 +1335,8 @@ class PlaidClient {
1152
1335
  * @param {string} id - The resource ID
1153
1336
  * @param {any} body - The metadata patch
1154
1337
  */
1155
- patchMetadata: (id, body) =>
1156
- this._request('PATCH', `/api/v1/vocab-items/${id}/metadata`, {
1338
+ patchMetadata: (id, body, auditMessage) =>
1339
+ this._request('PATCH', `/api/v1/vocab-items/${id}/metadata`, { auditMessage,
1157
1340
  rawBody: body, skipResponseTransform: true,
1158
1341
  }),
1159
1342
  /**
@@ -1162,10 +1345,24 @@ class PlaidClient {
1162
1345
  * @param {string} form - The vocab item form
1163
1346
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1164
1347
  */
1165
- create: (vocabLayerId, form, metadata) =>
1166
- this._request('POST', '/api/v1/vocab-items', {
1348
+ create: (vocabLayerId, form, metadata, auditMessage) =>
1349
+ this._request('POST', '/api/v1/vocab-items', { auditMessage,
1167
1350
  body: bodyOf({ 'vocab-layer-id': vocabLayerId, form, metadata }),
1168
1351
  }),
1352
+ /**
1353
+ * Create multiple vocab items in a single operation. Entries may target
1354
+ * different vocab layers; the user must have write access to each.
1355
+ * @param {Array<{vocabLayerId: string, form: string, metadata?: any}>} body - The vocab items to create
1356
+ */
1357
+ bulkCreate: (body, auditMessage) =>
1358
+ this._request('POST', '/api/v1/vocab-items/bulk', { auditMessage, body }),
1359
+ /**
1360
+ * Delete multiple vocab items in a single operation. Each item's
1361
+ * descendant vocab links are deleted too. Provide an array of IDs.
1362
+ * @param {string[]} body - The vocab item IDs to delete
1363
+ */
1364
+ bulkDelete: (body, auditMessage) =>
1365
+ this._request('DELETE', '/api/v1/vocab-items/bulk', { auditMessage, body }),
1169
1366
  /**
1170
1367
  * Get a vocab item by ID
1171
1368
  * @param {string} id - The resource ID
@@ -1179,15 +1376,15 @@ class PlaidClient {
1179
1376
  * Delete a vocab item
1180
1377
  * @param {string} id - The resource ID
1181
1378
  */
1182
- delete: (id) =>
1183
- this._request('DELETE', `/api/v1/vocab-items/${id}`),
1379
+ delete: (id, auditMessage) =>
1380
+ this._request('DELETE', `/api/v1/vocab-items/${id}`, { auditMessage }),
1184
1381
  /**
1185
1382
  * Update a vocab item's form
1186
1383
  * @param {string} id - The resource ID
1187
1384
  * @param {string} form - The vocab item form
1188
1385
  */
1189
- update: (id, form) =>
1190
- this._request('PATCH', `/api/v1/vocab-items/${id}`, {
1386
+ update: (id, form, auditMessage) =>
1387
+ this._request('PATCH', `/api/v1/vocab-items/${id}`, { auditMessage,
1191
1388
  body: bodyOf({ form }),
1192
1389
  }),
1193
1390
  };
@@ -1198,8 +1395,8 @@ class PlaidClient {
1198
1395
  * @param {string} relationLayerId - The relation layer ID
1199
1396
  * @param {string} direction - The direction ("up" or "down")
1200
1397
  */
1201
- shift: (relationLayerId, direction) =>
1202
- this._request('POST', `/api/v1/relation-layers/${relationLayerId}/shift`, {
1398
+ shift: (relationLayerId, direction, auditMessage) =>
1399
+ this._request('POST', `/api/v1/relation-layers/${relationLayerId}/shift`, { auditMessage,
1203
1400
  body: bodyOf({ direction }),
1204
1401
  }),
1205
1402
  /**
@@ -1207,8 +1404,8 @@ class PlaidClient {
1207
1404
  * @param {string} spanLayerId - The span layer ID
1208
1405
  * @param {string} name - The name
1209
1406
  */
1210
- create: (spanLayerId, name) =>
1211
- this._request('POST', '/api/v1/relation-layers', {
1407
+ create: (spanLayerId, name, auditMessage) =>
1408
+ this._request('POST', '/api/v1/relation-layers', { auditMessage,
1212
1409
  body: bodyOf({ 'span-layer-id': spanLayerId, name }),
1213
1410
  }),
1214
1411
  /**
@@ -1218,8 +1415,8 @@ class PlaidClient {
1218
1415
  * @param {string} configKey - The config key
1219
1416
  * @param {any} configValue - Configuration value to set
1220
1417
  */
1221
- setConfig: (relationLayerId, namespace, configKey, configValue) =>
1222
- this._request('PUT', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, {
1418
+ setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage) =>
1419
+ this._request('PUT', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1223
1420
  rawBody: configValue, skipResponseTransform: true,
1224
1421
  }),
1225
1422
  /**
@@ -1228,8 +1425,8 @@ class PlaidClient {
1228
1425
  * @param {string} namespace - The config namespace
1229
1426
  * @param {string} configKey - The config key
1230
1427
  */
1231
- deleteConfig: (relationLayerId, namespace, configKey) =>
1232
- this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, {
1428
+ deleteConfig: (relationLayerId, namespace, configKey, auditMessage) =>
1429
+ this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`, { auditMessage,
1233
1430
  skipResponseTransform: true,
1234
1431
  }),
1235
1432
  /**
@@ -1245,15 +1442,15 @@ class PlaidClient {
1245
1442
  * Delete a relation layer.
1246
1443
  * @param {string} relationLayerId - The relation layer ID
1247
1444
  */
1248
- delete: (relationLayerId) =>
1249
- this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}`),
1445
+ delete: (relationLayerId, auditMessage) =>
1446
+ this._request('DELETE', `/api/v1/relation-layers/${relationLayerId}`, { auditMessage }),
1250
1447
  /**
1251
1448
  * Update a relation layer's name.
1252
1449
  * @param {string} relationLayerId - The relation layer ID
1253
1450
  * @param {string} name - The name
1254
1451
  */
1255
- update: (relationLayerId, name) =>
1256
- this._request('PATCH', `/api/v1/relation-layers/${relationLayerId}`, {
1452
+ update: (relationLayerId, name, auditMessage) =>
1453
+ this._request('PATCH', `/api/v1/relation-layers/${relationLayerId}`, { auditMessage,
1257
1454
  body: bodyOf({ name }),
1258
1455
  }),
1259
1456
  };
@@ -1277,8 +1474,8 @@ class PlaidClient {
1277
1474
  * @param {number} [precedence] - Ordering precedence
1278
1475
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1279
1476
  */
1280
- create: (tokenLayerId, text, begin, end, precedence, metadata) =>
1281
- this._request('POST', '/api/v1/tokens', {
1477
+ create: (tokenLayerId, text, begin, end, precedence, metadata, auditMessage) =>
1478
+ this._request('POST', '/api/v1/tokens', { auditMessage,
1282
1479
  body: bodyOf({ 'token-layer-id': tokenLayerId, text, begin, end, precedence, metadata }),
1283
1480
  }),
1284
1481
  /**
@@ -1295,8 +1492,8 @@ class PlaidClient {
1295
1492
  * have no remaining tokens, the span will also be deleted.
1296
1493
  * @param {string} tokenId - The token ID
1297
1494
  */
1298
- delete: (tokenId) =>
1299
- this._request('DELETE', `/api/v1/tokens/${tokenId}`),
1495
+ delete: (tokenId, auditMessage) =>
1496
+ this._request('DELETE', `/api/v1/tokens/${tokenId}`, { auditMessage }),
1300
1497
  /**
1301
1498
  * Update a token.
1302
1499
  * @param {string} tokenId - The token ID
@@ -1307,30 +1504,31 @@ class PlaidClient {
1307
1504
  * CLEAR it (revert to no explicit ordering). bodyOf keeps null but
1308
1505
  * drops undefined, so the three cases map correctly to the server.
1309
1506
  */
1310
- update: (tokenId, begin, end, precedence) =>
1311
- this._request('PATCH', `/api/v1/tokens/${tokenId}`, {
1507
+ update: (tokenId, begin, end, precedence, auditMessage) =>
1508
+ this._request('PATCH', `/api/v1/tokens/${tokenId}`, { auditMessage,
1312
1509
  body: bodyOf({ begin, end, precedence }),
1313
1510
  }),
1314
1511
  /**
1315
1512
  * Create multiple tokens in a single operation.
1316
1513
  * @param {Array} body - The request body
1514
+ * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
1317
1515
  */
1318
- bulkCreate: (body) =>
1319
- this._request('POST', '/api/v1/tokens/bulk', { body }),
1516
+ bulkCreate: (body, auditMessage) =>
1517
+ this._request('POST', '/api/v1/tokens/bulk', { auditMessage, body }),
1320
1518
  /**
1321
1519
  * Delete multiple tokens in a single operation. Provide an array of IDs.
1322
1520
  * @param {Array} body - The request body
1323
1521
  */
1324
- bulkDelete: (body) =>
1325
- this._request('DELETE', '/api/v1/tokens/bulk', { body }),
1522
+ bulkDelete: (body, auditMessage) =>
1523
+ this._request('DELETE', '/api/v1/tokens/bulk', { auditMessage, body }),
1326
1524
  /**
1327
1525
  * Split a token at a Unicode code-point offset. The original token becomes the
1328
1526
  * left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
1329
1527
  * @param {string} tokenId - The token ID
1330
1528
  * @param {number} position - Code-point offset to split at (strictly between begin and end)
1331
1529
  */
1332
- split: (tokenId, position) =>
1333
- this._request('POST', `/api/v1/tokens/${tokenId}/split`, {
1530
+ split: (tokenId, position, auditMessage) =>
1531
+ this._request('POST', `/api/v1/tokens/${tokenId}/split`, { auditMessage,
1334
1532
  body: bodyOf({ position }),
1335
1533
  }),
1336
1534
  /**
@@ -1341,8 +1539,8 @@ class PlaidClient {
1341
1539
  * @param {string} tokenId - The anchor token ID
1342
1540
  * @param {string} otherTokenId - The other token to merge in
1343
1541
  */
1344
- merge: (tokenId, otherTokenId) =>
1345
- this._request('POST', `/api/v1/tokens/${tokenId}/merge`, {
1542
+ merge: (tokenId, otherTokenId, auditMessage) =>
1543
+ this._request('POST', `/api/v1/tokens/${tokenId}/merge`, { auditMessage,
1346
1544
  body: bodyOf({ 'other-token-id': otherTokenId }),
1347
1545
  }),
1348
1546
  /**
@@ -1353,8 +1551,8 @@ class PlaidClient {
1353
1551
  * @param {number} [begin] - New start offset, inclusive (Unicode code points)
1354
1552
  * @param {number} [end] - New end offset, exclusive (Unicode code points)
1355
1553
  */
1356
- shift: (tokenId, begin, end) =>
1357
- this._request('POST', `/api/v1/tokens/${tokenId}/shift`, {
1554
+ shift: (tokenId, begin, end, auditMessage) =>
1555
+ this._request('POST', `/api/v1/tokens/${tokenId}/shift`, { auditMessage,
1358
1556
  body: bodyOf({ begin, end }),
1359
1557
  }),
1360
1558
  /**
@@ -1362,16 +1560,16 @@ class PlaidClient {
1362
1560
  * @param {string} tokenId - The token ID
1363
1561
  * @param {any} body - The request body
1364
1562
  */
1365
- setMetadata: (tokenId, body) =>
1366
- this._request('PUT', `/api/v1/tokens/${tokenId}/metadata`, {
1563
+ setMetadata: (tokenId, body, auditMessage) =>
1564
+ this._request('PUT', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1367
1565
  rawBody: body, skipResponseTransform: true,
1368
1566
  }),
1369
1567
  /**
1370
1568
  * Remove all metadata from a token.
1371
1569
  * @param {string} tokenId - The token ID
1372
1570
  */
1373
- deleteMetadata: (tokenId) =>
1374
- this._request('DELETE', `/api/v1/tokens/${tokenId}/metadata`, {
1571
+ deleteMetadata: (tokenId, auditMessage) =>
1572
+ this._request('DELETE', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1375
1573
  skipResponseTransform: true,
1376
1574
  }),
1377
1575
  /**
@@ -1379,8 +1577,8 @@ class PlaidClient {
1379
1577
  * @param {string} tokenId - The token ID
1380
1578
  * @param {any} body - The metadata patch
1381
1579
  */
1382
- patchMetadata: (tokenId, body) =>
1383
- this._request('PATCH', `/api/v1/tokens/${tokenId}/metadata`, {
1580
+ patchMetadata: (tokenId, body, auditMessage) =>
1581
+ this._request('PATCH', `/api/v1/tokens/${tokenId}/metadata`, { auditMessage,
1384
1582
  rawBody: body, skipResponseTransform: true,
1385
1583
  }),
1386
1584
  };
@@ -1402,19 +1600,30 @@ class PlaidClient {
1402
1600
  * @param {any} data - The message data to send
1403
1601
  * @returns {Promise<any>} Response from the send operation
1404
1602
  */
1405
- sendMessage: (projectId, data) =>
1406
- this._request('POST', `/api/v1/projects/${projectId}/message`, {
1603
+ sendMessage: (projectId, data, auditMessage) =>
1604
+ this._request('POST', `/api/v1/projects/${projectId}/message`, { auditMessage,
1407
1605
  body: { body: data },
1408
1606
  }),
1409
1607
 
1410
1608
  /**
1411
- * Discover the services currently connected to a project (synchronous GET).
1609
+ * Discover the services seen on a project (synchronous GET). Currently
1610
+ * connected services carry `online: true`; previously-seen offline ones
1611
+ * carry `online: false` plus a `lastSeenAt` stamp.
1412
1612
  * @param {string} projectId - The UUID of the project to query
1413
- * @param {number} [timeout] - Ignored; kept for back-compat
1414
1613
  * @returns {Promise<Array>} Array of discovered service information
1415
1614
  */
1416
- discoverServices: (projectId, timeout) =>
1417
- discoverServices(this, projectId, timeout),
1615
+ discoverServices: (projectId) =>
1616
+ discoverServices(this, projectId),
1617
+
1618
+ /**
1619
+ * Forget a previously-seen (offline) service. Maintainer-only; 409 if
1620
+ * the service is currently connected.
1621
+ * @param {string} projectId - The UUID of the project
1622
+ * @param {string} serviceId - The ID of the service to forget
1623
+ * @returns {Promise<void>}
1624
+ */
1625
+ discardService: (projectId, serviceId) =>
1626
+ discardService(this, projectId, serviceId),
1418
1627
 
1419
1628
  /**
1420
1629
  * Register as a service and handle incoming work requests.
@@ -1475,8 +1684,27 @@ class PlaidClient {
1475
1684
  * For 'count': {return: 'count', count}. Entity cells are full entity objects
1476
1685
  * (same shape as the GET endpoints).
1477
1686
  */
1478
- this.query = (body) =>
1479
- this._request('POST', '/api/v1/query', { body });
1687
+ this.query = (body, auditMessage) =>
1688
+ this._request('POST', '/api/v1/query', { auditMessage, body });
1689
+
1690
+ // Logical-operation groups (audit-log grouping). There is no create: a
1691
+ // group row is made lazily by the first write carrying `?group-id=`
1692
+ // (see beginOperation). Not an audited write, so no auditMessage arg.
1693
+ this.operationGroups = {
1694
+ /**
1695
+ * Get a logical-operation group (its label + creator).
1696
+ * @param {string} id - The group id
1697
+ */
1698
+ get: (id) =>
1699
+ this._request('GET', `/api/v1/operation-groups/${id}`),
1700
+ /**
1701
+ * Relabel a logical-operation group after the fact. Owner or admin only.
1702
+ * @param {string} id - The group id
1703
+ * @param {string|null} message - The new label
1704
+ */
1705
+ update: (id, message) =>
1706
+ this._request('PATCH', `/api/v1/operation-groups/${id}`, { body: { message } }),
1707
+ };
1480
1708
  }
1481
1709
 
1482
1710
  // --- Core methods ---
@@ -1499,10 +1727,105 @@ class PlaidClient {
1499
1727
  this.strictModeDocumentId = null;
1500
1728
  }
1501
1729
 
1730
+ /**
1731
+ * Begin a LOGICAL OPERATION: a user-meaningful action ("Merge morphemes",
1732
+ * "Re-transcribe") implemented as many low-level writes, possibly across
1733
+ * several batches and even a service round-trip. Until `endOperation()`,
1734
+ * every write is stamped with a client-minted `?group-id=` (and the message)
1735
+ * so the audit log shows the whole run as ONE expandable entry labeled
1736
+ * `message`, with each write's own description underneath.
1737
+ *
1738
+ * Grouping is orthogonal to batches: a batch is a transaction boundary, an
1739
+ * operation is an intent boundary. An operation is NOT atomic — if write 3
1740
+ * of 5 fails, writes 1–2 stay committed (and logged under the group). Use a
1741
+ * batch inside the operation for any step that must be all-or-nothing.
1742
+ *
1743
+ * Nesting flattens: a `beginOperation` while one is open is a no-op that
1744
+ * joins the outer operation (the outer label wins), and the matching
1745
+ * `endOperation` is likewise a no-op. The label is recorded on the FIRST
1746
+ * write, so an operation that is never ended (crash, closed tab) is still
1747
+ * labeled in the log.
1748
+ *
1749
+ * @param {string} message - Human label for the operation.
1750
+ * @param {object} [opts] - Optional `{ id }`: adopt an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically).
1751
+ * @returns {string} The operation's group id.
1752
+ */
1753
+ beginOperation(message, { id } = {}) {
1754
+ if (this.operationGroup) {
1755
+ this.operationGroup.depth += 1;
1756
+ return this.operationGroup.id;
1757
+ }
1758
+ this.operationGroup = {
1759
+ id: id || crypto.randomUUID(),
1760
+ message: message == null ? null : String(message),
1761
+ depth: 1,
1762
+ written: false,
1763
+ refined: undefined,
1764
+ };
1765
+ return this.operationGroup.id;
1766
+ }
1767
+
1768
+ /**
1769
+ * End the current logical operation. With no argument this is purely local
1770
+ * (no request). Pass a refined `message` to relabel the group now that the
1771
+ * outcome is known (e.g. `endOperation('Merged 3 morphemes')`) — that sends
1772
+ * one PATCH, skipped if the operation never wrote anything. A refine from a
1773
+ * nested (flattened) `endOperation` is ignored; the outer label wins.
1774
+ * @param {string} [message] - Optional refined label.
1775
+ * @returns {Promise<void>}
1776
+ */
1777
+ async endOperation(message) {
1778
+ const group = this.operationGroup;
1779
+ if (!group) return;
1780
+ if (group.depth > 1) {
1781
+ group.depth -= 1;
1782
+ return;
1783
+ }
1784
+ this.operationGroup = null;
1785
+ const refined = message !== undefined ? message : group.refined;
1786
+ if (refined !== undefined && group.written) {
1787
+ try {
1788
+ await this.operationGroups.update(group.id, refined);
1789
+ } catch (e) {
1790
+ // 404: the group never materialized server-side (every tagged write
1791
+ // failed or a batch was aborted). Nothing to relabel — not an error.
1792
+ if (e.status !== 404) throw e;
1793
+ }
1794
+ }
1795
+ }
1796
+
1797
+ /**
1798
+ * Run `fn` as one logical operation (see beginOperation), ending it when
1799
+ * `fn` settles — including on throw. `fn` receives a `setMessage(msg)`
1800
+ * callback to refine the label once the outcome is known.
1801
+ *
1802
+ * await client.withOperation('Merge morphemes', async (setMessage) => {
1803
+ * await client.batched(() => { ... });
1804
+ * setMessage(`Merged ${n} morphemes`);
1805
+ * });
1806
+ *
1807
+ * @param {string} message - Human label for the operation.
1808
+ * @param {function} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
1809
+ * @returns {Promise<any>} Whatever `fn` resolves to.
1810
+ */
1811
+ async withOperation(message, fn) {
1812
+ this.beginOperation(message);
1813
+ const group = this.operationGroup;
1814
+ const setMessage = (msg) => { if (group.depth === 1) group.refined = msg; };
1815
+ try {
1816
+ return await fn(setMessage);
1817
+ } finally {
1818
+ await this.endOperation();
1819
+ }
1820
+ }
1821
+
1502
1822
  /** Begin a batch of operations. Subsequent API calls will be queued. */
1503
1823
  beginBatch() {
1504
1824
  this.isBatching = true;
1505
1825
  this.batchOperations = [];
1826
+ // Strict mode stamps the expected document-version on the FIRST write of
1827
+ // the batch only (see _request) — reset the marker per batch.
1828
+ this.batchVersionStamped = false;
1506
1829
  }
1507
1830
 
1508
1831
  /**
@@ -1587,6 +1910,38 @@ class PlaidClient {
1587
1910
  return this.isBatching;
1588
1911
  }
1589
1912
 
1913
+ /**
1914
+ * Run `fn` with a batch open, then submit all queued ops as ONE atomic
1915
+ * request — or abort the batch if `fn` throws, so a half-open batch can never
1916
+ * silently swallow later non-batch calls. `fn` makes the (queued) client
1917
+ * calls; it must NOT call submitBatch itself. Resolves to the batch results
1918
+ * array (`[]` if `fn` queued nothing).
1919
+ *
1920
+ * const [sentRes, wordRes] = await client.batched(async () => {
1921
+ * client.tokens.bulkCreate(sentenceOps);
1922
+ * client.tokens.bulkCreate(wordOps);
1923
+ * });
1924
+ *
1925
+ * Server-side a batch runs sequentially in one transaction (a child op sees
1926
+ * parents created earlier in the same `fn`; any op's failure rolls the whole
1927
+ * batch back). Not nestable. Named `batched()` because `client.batch` is the
1928
+ * low-level batch resource.
1929
+ * @param {() => (void | Promise<void>)} fn
1930
+ * @returns {Promise<Array>}
1931
+ */
1932
+ async batched(fn) {
1933
+ this.beginBatch();
1934
+ try {
1935
+ await fn();
1936
+ } catch (e) {
1937
+ // fn failed: drop the queued ops so the client leaves batch mode and
1938
+ // later plain calls don't queue into a never-submitted batch.
1939
+ if (this.isBatchMode()) this.abortBatch();
1940
+ throw e;
1941
+ }
1942
+ return this.submitBatch();
1943
+ }
1944
+
1590
1945
  /**
1591
1946
  * Authenticate and return a new client instance with token. This is the
1592
1947
  * single auth entry point — there is no `client.login` resource.
@@ -1596,6 +1951,64 @@ class PlaidClient {
1596
1951
  * @param {object} [options] - Client options forwarded to the constructor (e.g. { timeout })
1597
1952
  * @returns {Promise<PlaidClient>} - Authenticated client instance
1598
1953
  */
1954
+ /**
1955
+ * Build the link to hand someone for an invite code. The server never sees
1956
+ * an app URL, so the app that minted the invite is the one that names it.
1957
+ * Both SPAs use HashRouter, so the code rides in the fragment — which also
1958
+ * keeps it out of server access logs.
1959
+ * @param {string} appUrl - Where the SPA lives, e.g. "https://plaid.example.org/igt/"
1960
+ * @param {string} code - The code returned by invites.create()
1961
+ * @returns {string}
1962
+ */
1963
+ static inviteUrl(appUrl, code) {
1964
+ return `${appUrl.replace(/#.*$/, '').replace(/\/$/, '')}/#/invite/${encodeURIComponent(code)}`;
1965
+ }
1966
+
1967
+ /**
1968
+ * Describe an invite code, with NO authentication — this is what a signup
1969
+ * page calls before the redeemer has an account. Returns the kind of link
1970
+ * ("signup" or "password-reset"), its status ("active", "used", "expired",
1971
+ * "revoked"), and the project it grants access to, if any.
1972
+ *
1973
+ * Throws a 404-shaped error if the code is unknown. A known-but-dead code
1974
+ * resolves normally with a non-"active" status, so the page can say why.
1975
+ * @param {string} baseUrl - The API base URL
1976
+ * @param {string} code - The invite code
1977
+ * @param {object} [options]
1978
+ * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, username?: string, grantAdmin: boolean}>}
1979
+ */
1980
+ static async lookupInvite(baseUrl, code, options = {}) {
1981
+ return anonymousPost(baseUrl, '/api/v1/invite-codes/lookup', { code }, options);
1982
+ }
1983
+
1984
+ /**
1985
+ * Redeem an invite code, with NO authentication.
1986
+ *
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.
1992
+ * @param {string} baseUrl - The API base URL
1993
+ * @param {string} code - The invite code
1994
+ * @param {object} credentials
1995
+ * @param {string} [credentials.username] - Desired username (signup only)
1996
+ * @param {string} credentials.password - Desired password (min 8 characters)
1997
+ * @param {object} [options]
1998
+ * @returns {Promise<{client: PlaidClient, userId: string, kind: string}>}
1999
+ */
2000
+ static async redeemInvite(baseUrl, code, { username, password } = {}, options = {}) {
2001
+ const data = await anonymousPost(
2002
+ baseUrl, '/api/v1/invite-codes/redeem',
2003
+ bodyOf({ code, username, password }), options,
2004
+ );
2005
+ return {
2006
+ client: new PlaidClient(baseUrl.replace(/\/$/, ''), data.token || '', options),
2007
+ userId: data.userId,
2008
+ kind: data.kind,
2009
+ };
2010
+ }
2011
+
1599
2012
  static async login(baseUrl, userId, password, options = {}) {
1600
2013
  baseUrl = baseUrl.replace(/\/$/, '');
1601
2014
  const url = `${baseUrl}/api/v1/login`;
@@ -1642,3 +2055,19 @@ export {
1642
2055
  buildDefaultValues,
1643
2056
  coerceParamValues,
1644
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".
2062
+ export {
2063
+ PROV,
2064
+ PROV_STATES,
2065
+ PROV_CONFIRMED,
2066
+ stampInferred,
2067
+ confirmedInferred,
2068
+ provState,
2069
+ isMachine,
2070
+ isProtected,
2071
+ verifyOnEdit,
2072
+ serviceSource,
2073
+ } from './provenance.js';