@larc-iu/plaid-client 0.2.0-alpha.22 → 0.2.0-alpha.26

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
@@ -22,7 +22,7 @@ import {
22
22
  DEFAULT_BATCH_TIMEOUT_MS,
23
23
  queryTimeout,
24
24
  } from "./http.js";
25
- import { uuidv7 } from "./ids.js";
25
+ import { uuidv4, uuidv7, normalizeUserId } from "./ids.js";
26
26
  import { constraintsBody } from "./constraints.js";
27
27
  import { listAll, listPage, iterPages } from "./pagination.js";
28
28
  import { recorderFor } from "./events.js";
@@ -39,6 +39,294 @@ import {
39
39
  cancelServiceRequest,
40
40
  } from "./services.js";
41
41
 
42
+ // ---- Types --------------------------------------------------------------
43
+ // The shapes the API answers with and takes, for editors and for the
44
+ // declarations `npm run types` generates from this JSDoc (index.d.ts).
45
+
46
+ /**
47
+ * @typedef {import("./textEdits.js").TextEditOp} TextEditOp
48
+ * @typedef {import("./textEdits.js").TextGap} TextGap
49
+ * @typedef {import("./metadataOps.js").MetadataOp} MetadataOp
50
+ * @typedef {import("./constraints.js").LayerConstraint} LayerConstraint
51
+ * @typedef {import("./constraints.js").ConstraintWriteOptions} ConstraintWriteOptions
52
+ * @typedef {import("./constraints.js").ConstraintViolation} ConstraintViolation
53
+ * @typedef {import("./requester.js").Requester} Requester
54
+ * @typedef {import("./batchRef.js").BatchRef} BatchRef
55
+ * @typedef {import("./serviceSchema.js").ServiceParam} ServiceParam
56
+ * @typedef {import("./serviceSchema.js").ServiceInfo} ServiceInfo
57
+ * @typedef {import("./serviceSchema.js").DiscoveredService} DiscoveredService
58
+ * @typedef {import("./services.js").ResponseHelper} ResponseHelper
59
+ * @typedef {import("./services.js").ServiceRegistration} ServiceRegistration
60
+ * @typedef {import("./services.js").ServiceStatusEvent} ServiceStatusEvent
61
+ * @typedef {PlaidClient["operationGroups"]} OperationGroupsBundle
62
+ */
63
+
64
+ /**
65
+ * A single page of a cursor-paginated collection.
66
+ * @template [T=any]
67
+ * @typedef {{entries: T[], nextCursor: string | null}} Page
68
+ */
69
+
70
+ /**
71
+ * Client options.
72
+ * @typedef {object} PlaidClientOptions
73
+ * @property {number | null} [timeout] Per-request timeout in ms (default
74
+ * 30000, 0 or null disables it).
75
+ * @property {number | null} [batchTimeout] Timeout for batch submissions in
76
+ * ms (default 180000, 0 or null disables it). Aborting a batch does NOT
77
+ * stop the server, which keeps running the transaction and holding the
78
+ * write lock. Defaults to `timeout` when that is given and this is not.
79
+ * @property {number[]} [retryDelaysMs] Delays in ms before sending a keyed
80
+ * write again when its answer was lost (no response, 502 or 504). Default
81
+ * [1000, 3000, 9000].
82
+ * @property {((error: Error) => void) | null} [onAuthError] Fired once when
83
+ * a request returns HTTP 401 (missing, expired or invalid token). 403 does
84
+ * NOT trigger it.
85
+ */
86
+
87
+ /**
88
+ * What kind of operation a logical-operation group is (`?group-kind=`).
89
+ * @typedef {"assistant-plan" | "service-run" | "import" | "bulk-edit"
90
+ * | "guess-adoption" | "repair" | "review"} OperationKind
91
+ */
92
+
93
+ /**
94
+ * Idempotency-Key seed for an operation run more than once (`client.keySeed()`).
95
+ * @typedef {{seed: string, stamps: Map<number, number | null>}} KeySeed
96
+ */
97
+
98
+ /**
99
+ * A batch: the client's bundles, on which every write of project data queues
100
+ * until `submit()`. See `PlaidClient.batch()`. `submit()` resolves to one
101
+ * result per operation, in order. `ref(opIndex, index)` stands for the id a
102
+ * queued operation will create, in the body of a later write on this batch.
103
+ * @typedef {PlaidClient & {
104
+ * readonly client: PlaidClient,
105
+ * readonly operations: Array<{path: string, method: string, body?: any}>,
106
+ * submit(): Promise<any[]>,
107
+ * abort(): void,
108
+ * ref(opIndex?: number, index?: number): BatchRef,
109
+ * }} PlaidBatch
110
+ */
111
+
112
+ /**
113
+ * Options for a single page of any audit log. `kinds` keeps only the entries
114
+ * of operations of these kinds, each whole. `order: "desc"` pages newest
115
+ * first, and a cursor belongs to the direction that made it. `opsLimit` keeps
116
+ * only each entry's oldest N operations in `ops` (1..1000), and every entry
117
+ * has `opCount`. `entryId` reads only that entry rather than a page.
118
+ * @typedef {object} AuditPageOptions
119
+ * @property {string} [startTime]
120
+ * @property {string} [endTime]
121
+ * @property {string[] | string} [opTypes]
122
+ * @property {OperationKind[] | string} [kinds]
123
+ * @property {"asc" | "desc"} [order]
124
+ * @property {number} [limit]
125
+ * @property {string} [cursor]
126
+ * @property {number} [opsLimit]
127
+ * @property {string} [entryId]
128
+ */
129
+
130
+ /**
131
+ * What `vocabLayers.restoreItem` did, or with `dryRun` would do.
132
+ * @typedef {object} VocabItemRestoreSummary
133
+ * @property {boolean} inserted The entry was gone and came back under its old id.
134
+ * @property {boolean} form A living entry's form was set back.
135
+ * @property {boolean} metadata A living entry's fields were set back.
136
+ * @property {number} total 0 when nothing changes.
137
+ */
138
+
139
+ /**
140
+ * One private data entry. `version` is raised by one on every write of the
141
+ * entry, and a new entry is 1.
142
+ * @typedef {{key: string, updatedAt: string, version: number, value?: any}} UserDataEntry
143
+ */
144
+
145
+ /** @typedef {UserDataEntry & {userId: string}} AdminUserDataEntry */
146
+
147
+ /**
148
+ * One user's activity over a scope and window (`audit.tally`).
149
+ * @typedef {object} ActivityTallyRow
150
+ * @property {{id: string, displayName?: string}} user
151
+ * @property {number} operations
152
+ * @property {number} changes
153
+ * @property {number} documents
154
+ * @property {string} firstTs
155
+ * @property {string} lastTs
156
+ * @property {Array<{date: string, changes: number}>} [byDay]
157
+ */
158
+
159
+ /**
160
+ * What a constraint repair did in one document.
161
+ * @typedef {{document: string, constraint: string, deleted: number, joined: number}} ConstraintRepair
162
+ */
163
+
164
+ /**
165
+ * Entities that can carry a comment.
166
+ * @typedef {"document" | "text" | "token" | "span" | "relation" | "vocab-item"} CommentableType
167
+ */
168
+
169
+ /**
170
+ * One comment. `projectId` and `documentId` are null for a comment on a
171
+ * vocabulary entry, and `vocabLayerId` is set only for one. `anchorLabel` is
172
+ * what the comment is about, in words, captured when it was posted: a
173
+ * comment outlives its anchor. `edited` is true once the body has been
174
+ * rewritten since it was posted.
175
+ * @typedef {object} Comment
176
+ * @property {string} id
177
+ * @property {string | null} projectId
178
+ * @property {string | null} documentId
179
+ * @property {string | null} vocabLayerId
180
+ * @property {CommentableType} entityType
181
+ * @property {string} entityId
182
+ * @property {string | null} anchorLabel
183
+ * @property {string} authorId
184
+ * @property {string} body
185
+ * @property {string} createdAt
186
+ * @property {string} updatedAt
187
+ * @property {boolean} edited
188
+ */
189
+
190
+ /**
191
+ * One entry in a project's annotation manual. `body` is present on a single
192
+ * read and on a list made with `includeBodies`, `bodyChars` on a list made
193
+ * without. `pinned`: the assistant is given this one in full on every turn.
194
+ * @typedef {object} Guideline
195
+ * @property {string} id
196
+ * @property {string} projectId
197
+ * @property {string} title
198
+ * @property {string} [body]
199
+ * @property {number} [bodyChars]
200
+ * @property {boolean} pinned
201
+ * @property {string} createdAt
202
+ * @property {string} updatedAt
203
+ */
204
+
205
+ /**
206
+ * A research-telemetry event type.
207
+ * @typedef {"suggestion.shown" | "suggestion.adopted" | "suggestion.dismissed" | "plan.opened"} ClientEventType
208
+ */
209
+
210
+ /**
211
+ * One research-telemetry event as the server stores it.
212
+ * @typedef {object} ClientEvent
213
+ * @property {number} id
214
+ * @property {string} projectId
215
+ * @property {string | null} documentId
216
+ * @property {string} userId
217
+ * @property {ClientEventType} type
218
+ * @property {string | null} targetId
219
+ * @property {Record<string, any> | null} data
220
+ * @property {string | null} clientTs
221
+ * @property {string} ts
222
+ */
223
+
224
+ /**
225
+ * An invite. `kind` is "signup" or "password-reset", `status` "active",
226
+ * "used", "expired", "revoked" or "inactive".
227
+ * @typedef {object} Invite
228
+ * @property {string} id
229
+ * @property {string} kind
230
+ * @property {string} status
231
+ * @property {string} createdBy
232
+ * @property {string} createdAt
233
+ * @property {string} expiresAt
234
+ * @property {number} maxUses
235
+ * @property {number} uses
236
+ * @property {string | null} revokedAt
237
+ * @property {string | null} note
238
+ * @property {string | null} targetUserId
239
+ * @property {boolean} grantAdmin
240
+ * @property {string | null} projectId
241
+ * @property {string | null} projectRole
242
+ */
243
+
244
+ /**
245
+ * Server limits, as reported by /api/v1/info. An unset limit is absent.
246
+ * `passwordMinLength` is the shortest password the server accepts on any
247
+ * path, `lockExpirationMs` how long a document lock is held before it lapses.
248
+ * @typedef {object} ServerLimits
249
+ * @property {number} [mediaFileBytes]
250
+ * @property {number} [jsonBodyBytes]
251
+ * @property {number} [batchOperations]
252
+ * @property {number} [metadataDepth]
253
+ * @property {number} [metadataKeyCount]
254
+ * @property {number} [metadataStringLength]
255
+ * @property {number} [metadataTotalBytes]
256
+ * @property {number} [userDataValueBytes]
257
+ * @property {number} [guidelineTitleLength]
258
+ * @property {number} [guidelineBodyLength]
259
+ * @property {number} [passwordMinLength]
260
+ * @property {number} [lockExpirationMs]
261
+ */
262
+
263
+ /**
264
+ * @typedef {object} ServerHealth
265
+ * @property {boolean} ok
266
+ * @property {string} version
267
+ * @property {number} uptimeMs
268
+ * @property {{dbSizeMb?: number, auditRows?: number, error?: string}} audit
269
+ */
270
+
271
+ /**
272
+ * @typedef {{ip: string, userId?: string, failures: number, limit: number, blocked: boolean}} RateLimitBucket
273
+ */
274
+
275
+ /**
276
+ * @typedef {object} RateLimitSnapshot
277
+ * @property {number} windowMs
278
+ * @property {RateLimitBucket[]} logins
279
+ * @property {RateLimitBucket[]} ips
280
+ * @property {RateLimitBucket[]} invites
281
+ */
282
+
283
+ /**
284
+ * @typedef {object} BackupStatus
285
+ * @property {boolean} enabled
286
+ * @property {string} directory
287
+ * @property {number} retention
288
+ * @property {string} time
289
+ * @property {Array<{name: string, bytes: number, modified: string}>} backups
290
+ * @property {boolean} [ok]
291
+ */
292
+
293
+ /**
294
+ * @typedef {object} ServerReport
295
+ * @property {string} version
296
+ * @property {{uptimeMs: number, startedAt: string, java: string, heapUsed: number, heapMax: number}} jvm
297
+ * @property {{path: string | null, bytes: number | null, walBytes: number | null,
298
+ * slowQueryThresholdMs?: number, journalMode?: string, maxPoolSize?: number,
299
+ * tables: Record<string, number | null>}} database
300
+ * @property {{directory: string, files: number, bytes: number, orphans: number, orphanBytes: number}} media
301
+ * @property {BackupStatus} backup
302
+ * @property {Record<string, any>} settings
303
+ */
304
+
305
+ /**
306
+ * The live log. Requests and events are buffered separately, so a burst of
307
+ * requests cannot evict an error. In each, `matched` is how many passed the
308
+ * filters (which may exceed what `entries` holds) and `held` how many are
309
+ * buffered in total. `file` is the configured log file's absolute path, or
310
+ * null when logging to stdout.
311
+ * @typedef {object} LiveLog
312
+ * @property {{entries: object[], matched: number, held: number, capacity: number,
313
+ * stats: Record<string, number | null>}} requests
314
+ * @property {{entries: object[], matched: number, held: number, capacity: number,
315
+ * byLevel: Record<string, number>}} events
316
+ * @property {string | null} file
317
+ */
318
+
319
+ /**
320
+ * An open server-sent-events stream. Once CLOSED, `error` says why: null when
321
+ * this side closed it. A refused stream's error carries the HTTP `status`,
322
+ * and one the server ended is named "StreamClosed".
323
+ * @typedef {object} SSEConnection
324
+ * @property {() => void} close
325
+ * @property {() => any} getStats
326
+ * @property {number} readyState
327
+ * @property {(Error & {status?: number}) | null} error
328
+ */
329
+
42
330
  // Helper: normalize a document-read `layers` filter to the wire's
43
331
  // comma-separated form. Accepts an array of layer ids or a ready-made string;
44
332
  // anything empty becomes undefined so no `?layers=` is sent at all.
@@ -70,18 +358,22 @@ function bodyOf(obj) {
70
358
  return result;
71
359
  }
72
360
 
73
- // POST to an endpoint that takes no Authorization header (login, invite lookup
74
- // and redemption). Deliberately does not go through PlaidClient._request: these
75
- // are called before any client exists, which is the whole point.
76
- async function anonymousPost(baseUrl, path, body, options = {}) {
361
+ // A request with no Authorization header: login, invite lookup and redemption
362
+ // (POST, with a body), /health and /api/v1/info (GET). Deliberately does not
363
+ // go through PlaidClient._request: these are called before any client exists,
364
+ // which is the whole point.
365
+ async function anonymousRequest(method, baseUrl, path, body, options = {}) {
77
366
  const base = baseUrl.replace(/\/$/, "");
78
367
  const url = `${base}${path}`;
79
368
  try {
80
- const fetchOptions = {
81
- method: "POST",
82
- headers: { "Content-Type": "application/json" },
83
- body: JSON.stringify(body),
84
- };
369
+ const fetchOptions =
370
+ method === "GET"
371
+ ? { method }
372
+ : {
373
+ method,
374
+ headers: { "Content-Type": "application/json" },
375
+ body: JSON.stringify(body),
376
+ };
85
377
  const signal = timeoutSignal(
86
378
  options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
87
379
  );
@@ -89,42 +381,12 @@ async function anonymousPost(baseUrl, path, body, options = {}) {
89
381
 
90
382
  const response = await fetch(url, fetchOptions);
91
383
  if (!response.ok) {
92
- throw makeHttpError(
93
- response,
94
- await parseErrorBody(response),
95
- url,
96
- "POST",
97
- );
384
+ throw makeHttpError(response, await parseErrorBody(response), url, method);
98
385
  }
99
386
  return transformResponse(await response.json());
100
387
  } catch (error) {
101
388
  if (error.status) throw error;
102
- throw makeNetworkError(error, url, "POST");
103
- }
104
- }
105
-
106
- /**
107
- * GET a path with no authentication, for the two endpoints a client may need
108
- * before it has a session: /health and /api/v1/info.
109
- */
110
- async function anonymousGet(baseUrl, path, options = {}) {
111
- const base = baseUrl.replace(/\/$/, "");
112
- const url = `${base}${path}`;
113
- try {
114
- const fetchOptions = { method: "GET" };
115
- const signal = timeoutSignal(
116
- options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
117
- );
118
- if (signal) fetchOptions.signal = signal;
119
-
120
- const response = await fetch(url, fetchOptions);
121
- if (!response.ok) {
122
- throw makeHttpError(response, await parseErrorBody(response), url, "GET");
123
- }
124
- return transformResponse(await response.json());
125
- } catch (error) {
126
- if (error.status) throw error;
127
- throw makeNetworkError(error, url, "GET");
389
+ throw makeNetworkError(error, url, method);
128
390
  }
129
391
  }
130
392
 
@@ -155,6 +417,12 @@ const OPERATION_KINDS = [
155
417
  */
156
418
  export const BACKUP_TIMEOUT_MS = 30 * 60 * 1000;
157
419
 
420
+ /** An avatar token is minted again once less than this is left of it. */
421
+ const AVATAR_TOKEN_RENEW_MS = 10 * 60 * 1000;
422
+ // An avatar token minted this recently is the renewal a caller asking for one
423
+ // wanted.
424
+ const AVATAR_TOKEN_FRESH_MS = 5 * 1000;
425
+
158
426
  /**
159
427
  * A batch is the client with a different `_request` (see `queueRequest`) and
160
428
  * its own copy of the bundles. Everything else resolves to the client through
@@ -316,13 +584,12 @@ class PlaidClient {
316
584
  * Create a new PlaidClient instance
317
585
  * @param {string} baseUrl - The base URL for the API
318
586
  * @param {string} token - The authentication token
319
- * @param {object} [options] - Client options
320
- * @param {number} [options.timeout=30000] - Per-request timeout in ms (0 or null disables it)
321
- * @param {number} [options.batchTimeout=180000] - Timeout for batch submissions in ms (0 or null disables it)
587
+ * @param {PlaidClientOptions} [options] - Client options
322
588
  */
323
589
  constructor(baseUrl, token, options = {}) {
324
590
  this.baseUrl = baseUrl.replace(/\/$/, "");
325
591
  this.token = token;
592
+ /** @type {number | null} */
326
593
  this.timeout =
327
594
  options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
328
595
  // Batch submissions get a longer budget than a single request. Aborting a
@@ -330,6 +597,7 @@ class PlaidClient {
330
597
  // holding the single SQLite write lock, so a client that gives up early
331
598
  // just adds a retry on top of a write that is still in progress. Falls
332
599
  // back to an explicit `timeout` when one was given.
600
+ /** @type {number | null} */
333
601
  this.batchTimeout =
334
602
  options.batchTimeout !== undefined
335
603
  ? options.batchTimeout
@@ -338,33 +606,90 @@ class PlaidClient {
338
606
  : DEFAULT_BATCH_TIMEOUT_MS;
339
607
  // Delays (ms) before sending a keyed write again when its answer was lost
340
608
  // (no response, 502, 504). See the Idempotency-Key note in http.js.
609
+ /** @type {number[] | undefined} */
341
610
  this.retryDelaysMs = options.retryDelaysMs;
611
+ /**
612
+ * The latest version seen for each document written through this client,
613
+ * keyed by document id.
614
+ * @type {Record<string, number>}
615
+ */
342
616
  this.documentVersions = {};
617
+ /**
618
+ * True once a response left its version list out
619
+ * (X-Document-Versions-Omitted, past fifty documents). Every version held
620
+ * was then forgotten, and the strict-mode document's is read again before
621
+ * the next write.
622
+ * @type {boolean | undefined}
623
+ */
624
+ this.documentVersionsOmitted = undefined;
343
625
  // The server's clock minus this machine's, in ms, from the last response
344
626
  // with a Date header (null before one). See serverNow().
627
+ /** @type {number | null} */
345
628
  this.serverClockOffsetMs = null;
629
+ /**
630
+ * The document strict mode is entered for, or null.
631
+ * @type {string | null}
632
+ */
346
633
  this.strictModeDocumentId = null;
347
634
  // Set to a DocumentLockLost while a `documents.locked()` block's keep-alive
348
635
  // has failed. Every write throws it until the block exits; see
349
636
  // documentLock.js.
637
+ /** @type {import("./documentLock.js").DocumentLockLost | null} */
350
638
  this.documentLockLost = null;
351
639
  // The open logical operation (audit-log group), or null. While set, every
352
640
  // write is stamped with `?group-id=` (+ `group-message`) so the audit log
353
641
  // folds them into ONE expandable entry. See beginOperation / withOperation.
354
642
  // Shape: { id, message, depth, written, refined, frames }, where each of
355
643
  // `frames` is { keys, count, minted, depth, owned } and the last numbers.
644
+ /**
645
+ * @type {{id: string, message: string | null, kind: OperationKind | null,
646
+ * ref: string | null, depth: number, written: boolean, refined: any,
647
+ * frames: object[]} | null}
648
+ */
356
649
  this.operationGroup = null;
357
650
  // Optional callback fired once (per client) when any request returns HTTP
358
651
  // 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
359
652
  // the stored token and route back to login. See makeRequest in http.js.
653
+ /** @type {((error: Error) => void) | null} */
360
654
  this.onAuthError = options.onAuthError || null;
655
+ /** @private */
361
656
  this._authErrorFired = false;
362
657
  // Who hears that a request is being sent again (onRetry).
658
+ /** @private */
363
659
  this._retryListeners = new Set();
660
+ // GET /info as last read (`server.info()`), and whether a stream of this
661
+ // client dropped since.
662
+ /** @private */
663
+ this._serverFacts = { info: null, stale: false };
664
+ // The key frame the last beginOperation opened, or null (withOperation).
665
+ /** @private @type {object | null} */
666
+ this._openedFrame = null;
364
667
 
365
668
  this._installResources();
366
669
  }
367
670
 
671
+ /**
672
+ * A stream of this client ended without being closed here: the server may
673
+ * have restarted, with other limits.
674
+ * @private
675
+ */
676
+ _noteStreamDropped() {
677
+ this._serverFacts.stale = true;
678
+ }
679
+
680
+ /**
681
+ * A stream of this client is open. After a drop the limits are read again,
682
+ * once however many streams reopen.
683
+ * @private
684
+ */
685
+ _noteStreamOpened() {
686
+ if (!this._serverFacts.stale) return;
687
+ this.server.refresh().catch(() => {
688
+ // The figures known before stand, and the next open asks again.
689
+ this._serverFacts.stale = true;
690
+ });
691
+ }
692
+
368
693
  /**
369
694
  * Hear every time a request is sent again: after a 503 (the database was
370
695
  * busy), or for a keyed write after its answer was lost (no response, 502,
@@ -379,7 +704,10 @@ class PlaidClient {
379
704
  return () => this._retryListeners.delete(listener);
380
705
  }
381
706
 
382
- /** Tell every `onRetry` listener. A listener that throws is passed over. */
707
+ /**
708
+ * Tell every `onRetry` listener. A listener that throws is passed over.
709
+ * @private
710
+ */
383
711
  _noteRetry(info) {
384
712
  for (const listener of this._retryListeners) {
385
713
  try {
@@ -406,6 +734,7 @@ class PlaidClient {
406
734
  * the client, and again on every batch it opens (see `batch()`): a batch is
407
735
  * a view of the client with its own `_request`, so the same bundles built
408
736
  * on it queue instead of sending.
737
+ * @private
409
738
  */
410
739
  _installResources() {
411
740
  this.vocabLinks = {
@@ -414,6 +743,9 @@ class PlaidClient {
414
743
  * @param {string} vocabItem - The vocab item to link
415
744
  * @param {Array} tokens - The tokens to link
416
745
  * @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
746
+ * @param {string} [auditMessage] - Audit message for this write
747
+ * @param {object} [opts]
748
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
417
749
  */
418
750
  create: (vocabItem, tokens, metadata, auditMessage, { id } = {}) =>
419
751
  this._request("POST", "/api/v1/vocab-links", {
@@ -425,6 +757,7 @@ class PlaidClient {
425
757
  * reference different vocab items, but all tokens across the call must
426
758
  * belong to one document.
427
759
  * @param {Array<{vocabItem: string, tokens: string[], metadata?: any}>} body - The vocab links to create
760
+ * @param {string} [auditMessage] - Audit message for this write
428
761
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
429
762
  */
430
763
  bulkCreate: (body, auditMessage) =>
@@ -435,40 +768,15 @@ class PlaidClient {
435
768
  /**
436
769
  * Delete multiple vocab links in a single operation. Provide an array of IDs.
437
770
  * @param {string[]} body - The vocab link IDs to delete
771
+ * @param {string} [auditMessage] - Audit message for this write
772
+ * @returns {Promise<void>}
438
773
  */
439
774
  bulkDelete: (body, auditMessage) =>
440
775
  this._request("DELETE", "/api/v1/vocab-links/bulk", {
441
776
  auditMessage,
442
777
  body,
443
778
  }),
444
- /**
445
- * Replace all metadata for a vocab link. The entire metadata map is replaced - existing metadata keys not included in the request will be removed.
446
- * @param {string} id - The resource ID
447
- * @param {any} body - The request body
448
- */
449
- setMetadata: (id, body, auditMessage) =>
450
- this._request("PUT", `/api/v1/vocab-links/${id}/metadata`, {
451
- auditMessage,
452
- rawBody: body,
453
- }),
454
- /**
455
- * Remove all metadata from a vocab link.
456
- * @param {string} id - The resource ID
457
- */
458
- deleteMetadata: (id, auditMessage) =>
459
- this._request("DELETE", `/api/v1/vocab-links/${id}/metadata`, {
460
- auditMessage,
461
- }),
462
- /**
463
- * Edit metadata for a vocab link with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
464
- * @param {string} id - The resource ID
465
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
466
- */
467
- patchMetadata: (id, body, auditMessage) =>
468
- this._request("PATCH", `/api/v1/vocab-links/${id}/metadata`, {
469
- auditMessage,
470
- rawBody: body,
471
- }),
779
+ ...metadataMethods(this, "vocab-links"),
472
780
  /**
473
781
  * Get a vocab link by ID
474
782
  * @param {string} id - The resource ID
@@ -478,6 +786,7 @@ class PlaidClient {
478
786
  /**
479
787
  * Delete a vocab link
480
788
  * @param {string} id - The resource ID
789
+ * @param {string} [auditMessage] - Audit message for this write
481
790
  */
482
791
  delete: (id, auditMessage) =>
483
792
  this._request("DELETE", `/api/v1/vocab-links/${id}`, { auditMessage }),
@@ -541,6 +850,10 @@ class PlaidClient {
541
850
  * that may be long: audit() walks every page before it resolves.
542
851
  * @param {string} id - The vocabulary ID
543
852
  * @param {object} [opts]
853
+ * @param {string} [opts.startTime] - Only entries at or after this instant
854
+ * @param {string} [opts.endTime] - Only entries at or before this instant
855
+ * @param {string[]|string} [opts.opTypes] - Only these op types
856
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
544
857
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
545
858
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
546
859
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -586,9 +899,10 @@ class PlaidClient {
586
899
  * @param {string} itemId - The entry ID
587
900
  * @param {string} asOf - The moment to go back to (ISO-8601 instant),
588
901
  * typically a history entry's `endTime`
589
- * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
902
+ * @param {{dryRun?: boolean}} [options] - `{ dryRun }`: with `dryRun` true nothing
590
903
  * is written and the summary says what would change
591
904
  * @param {string} [auditMessage] - Custom audit message for this operation
905
+ * @returns {Promise<VocabItemRestoreSummary>}
592
906
  */
593
907
  restoreItem: (id, itemId, asOf, { dryRun } = {}, auditMessage) =>
594
908
  this._request(
@@ -605,6 +919,7 @@ class PlaidClient {
605
919
  /**
606
920
  * Delete a vocab layer.
607
921
  * @param {string} id - The resource ID
922
+ * @param {string} [auditMessage] - Audit message for this write
608
923
  */
609
924
  delete: (id, auditMessage) =>
610
925
  this._request("DELETE", `/api/v1/vocab-layers/${id}`, { auditMessage }),
@@ -612,42 +927,14 @@ class PlaidClient {
612
927
  * Update a vocab layer's name.
613
928
  * @param {string} id - The resource ID
614
929
  * @param {string} name - The name
930
+ * @param {string} [auditMessage] - Audit message for this write
615
931
  */
616
932
  update: (id, name, auditMessage) =>
617
933
  this._request("PATCH", `/api/v1/vocab-layers/${id}`, {
618
934
  auditMessage,
619
935
  body: bodyOf({ name }),
620
936
  }),
621
- /**
622
- * Set a configuration value for a layer in an editor namespace.
623
- * @param {string} id - The resource ID
624
- * @param {string} namespace - The config namespace
625
- * @param {string} configKey - The config key
626
- * @param {any} configValue - Configuration value to set
627
- * @param {string} [auditMessage] - Audit message for this write
628
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
629
- * was absent). The write is then refused with a 409 when the stored value differs.
630
- */
631
- setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
632
- this._request(
633
- "PUT",
634
- `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
635
- configRequest(auditMessage, configValue, options),
636
- ),
637
- /**
638
- * Remove a configuration value for a layer.
639
- * @param {string} id - The resource ID
640
- * @param {string} namespace - The config namespace
641
- * @param {string} configKey - The config key
642
- * @param {string} [auditMessage] - Audit message for this write
643
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
644
- */
645
- deleteConfig: (id, namespace, configKey, auditMessage, options) =>
646
- this._request(
647
- "DELETE",
648
- `/api/v1/vocab-layers/${id}/config/${namespace}/${configKey}`,
649
- configRequest(auditMessage, undefined, options, true),
650
- ),
937
+ ...configMethods(this, "vocab-layers"),
651
938
  /**
652
939
  * List all vocab layers accessible to user. Transparently follows
653
940
  * pagination cursors and returns the full flat array.
@@ -678,6 +965,9 @@ class PlaidClient {
678
965
  /**
679
966
  * Create a new vocab layer. Note: this also registers the user as a maintainer.
680
967
  * @param {string} name - The name
968
+ * @param {string} [auditMessage] - Audit message for this write
969
+ * @param {object} [opts]
970
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
681
971
  */
682
972
  create: (name, auditMessage, { id } = {}) =>
683
973
  this._request("POST", "/api/v1/vocab-layers", {
@@ -688,6 +978,7 @@ class PlaidClient {
688
978
  * Assign a user as a maintainer for this vocab layer.
689
979
  * @param {string} id - The resource ID
690
980
  * @param {string} userId - The user ID
981
+ * @param {string} [auditMessage] - Audit message for this write
691
982
  */
692
983
  addMaintainer: (id, userId, auditMessage) =>
693
984
  this._request(
@@ -699,6 +990,7 @@ class PlaidClient {
699
990
  * Remove a user's maintainer privileges for this vocab layer.
700
991
  * @param {string} id - The resource ID
701
992
  * @param {string} userId - The user ID
993
+ * @param {string} [auditMessage] - Audit message for this write
702
994
  */
703
995
  removeMaintainer: (id, userId, auditMessage) =>
704
996
  this._request(
@@ -709,38 +1001,12 @@ class PlaidClient {
709
1001
  };
710
1002
 
711
1003
  this.relations = {
712
- /**
713
- * Replace all metadata for a relation.
714
- * @param {string} relationId - The relation ID
715
- * @param {any} body - The request body
716
- */
717
- setMetadata: (relationId, body, auditMessage) =>
718
- this._request("PUT", `/api/v1/relations/${relationId}/metadata`, {
719
- auditMessage,
720
- rawBody: body,
721
- }),
722
- /**
723
- * Remove all metadata from a relation.
724
- * @param {string} relationId - The relation ID
725
- */
726
- deleteMetadata: (relationId, auditMessage) =>
727
- this._request("DELETE", `/api/v1/relations/${relationId}/metadata`, {
728
- auditMessage,
729
- }),
730
- /**
731
- * Edit metadata for a relation with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
732
- * @param {string} relationId - The relation ID
733
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
734
- */
735
- patchMetadata: (relationId, body, auditMessage) =>
736
- this._request("PATCH", `/api/v1/relations/${relationId}/metadata`, {
737
- auditMessage,
738
- rawBody: body,
739
- }),
1004
+ ...metadataMethods(this, "relations"),
740
1005
  /**
741
1006
  * Update the target span of a relation.
742
1007
  * @param {string} relationId - The relation ID
743
1008
  * @param {string} spanId - The span ID
1009
+ * @param {string} [auditMessage] - Audit message for this write
744
1010
  */
745
1011
  setTarget: (relationId, spanId, auditMessage) =>
746
1012
  this._request("PUT", `/api/v1/relations/${relationId}/target`, {
@@ -756,6 +1022,7 @@ class PlaidClient {
756
1022
  /**
757
1023
  * Delete a relation.
758
1024
  * @param {string} relationId - The relation ID
1025
+ * @param {string} [auditMessage] - Audit message for this write
759
1026
  */
760
1027
  delete: (relationId, auditMessage) =>
761
1028
  this._request("DELETE", `/api/v1/relations/${relationId}`, {
@@ -765,6 +1032,7 @@ class PlaidClient {
765
1032
  * Update a relation's value.
766
1033
  * @param {string} relationId - The relation ID
767
1034
  * @param {any} value - The value
1035
+ * @param {string} [auditMessage] - Audit message for this write
768
1036
  */
769
1037
  update: (relationId, value, auditMessage) =>
770
1038
  this._request("PATCH", `/api/v1/relations/${relationId}`, {
@@ -775,6 +1043,7 @@ class PlaidClient {
775
1043
  * Update the source span of a relation.
776
1044
  * @param {string} relationId - The relation ID
777
1045
  * @param {string} spanId - The span ID
1046
+ * @param {string} [auditMessage] - Audit message for this write
778
1047
  */
779
1048
  setSource: (relationId, spanId, auditMessage) =>
780
1049
  this._request("PUT", `/api/v1/relations/${relationId}/source`, {
@@ -790,6 +1059,9 @@ class PlaidClient {
790
1059
  * @param {string} targetId - The target span ID
791
1060
  * @param {any} value - The value
792
1061
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1062
+ * @param {string} [auditMessage] - Audit message for this write
1063
+ * @param {object} [opts]
1064
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
793
1065
  */
794
1066
  create: (
795
1067
  layerId,
@@ -814,6 +1086,7 @@ class PlaidClient {
814
1086
  /**
815
1087
  * Create multiple relations in a single operation.
816
1088
  * @param {Array} body - The request body
1089
+ * @param {string} [auditMessage] - Audit message for this write
817
1090
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
818
1091
  */
819
1092
  bulkCreate: (body, auditMessage) =>
@@ -821,6 +1094,8 @@ class PlaidClient {
821
1094
  /**
822
1095
  * Delete multiple relations in a single operation. Provide an array of IDs.
823
1096
  * @param {Array} body - The request body
1097
+ * @param {string} [auditMessage] - Audit message for this write
1098
+ * @returns {Promise<void>}
824
1099
  */
825
1100
  bulkDelete: (body, auditMessage) =>
826
1101
  this._request("DELETE", "/api/v1/relations/bulk", {
@@ -830,6 +1105,7 @@ class PlaidClient {
830
1105
  /**
831
1106
  * Update many relations in a single operation: set values and/or patch metadata.
832
1107
  * @param {Array} body - Objects of the shape `{id, value?, metadata?}`. `value` is set only when the key is present (null sends JSON null); `metadata` is a list of metadata ops, as for patchMetadata. The relations may lie in several documents of one project; every document touched has its version bumped and every new version comes back in `X-Document-Versions` (past fifty documents, only their number, in `X-Document-Versions-Omitted`). A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
1108
+ * @param {string} [auditMessage] - Audit message for this write
833
1109
  * @returns {Promise<{count: number}>} How many relations were updated.
834
1110
  */
835
1111
  bulkUpdate: (body, auditMessage) =>
@@ -840,81 +1116,22 @@ class PlaidClient {
840
1116
  };
841
1117
 
842
1118
  this.spanLayers = {
843
- /**
844
- * Set a configuration value for a layer in an editor namespace.
845
- * @param {string} spanLayerId - The span layer ID
846
- * @param {string} namespace - The config namespace
847
- * @param {string} configKey - The config key
848
- * @param {any} configValue - Configuration value to set
849
- * @param {string} [auditMessage] - Audit message for this write
850
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
851
- * was absent). The write is then refused with a 409 when the stored value differs.
852
- */
853
- setConfig: (spanLayerId, namespace, configKey, configValue, auditMessage, options) =>
854
- this._request(
855
- "PUT",
856
- `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
857
- configRequest(auditMessage, configValue, options),
858
- ),
859
- /**
860
- * Remove a configuration value for a layer.
861
- * @param {string} spanLayerId - The span layer ID
862
- * @param {string} namespace - The config namespace
863
- * @param {string} configKey - The config key
864
- * @param {string} [auditMessage] - Audit message for this write
865
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
866
- */
867
- deleteConfig: (spanLayerId, namespace, configKey, auditMessage, options) =>
868
- this._request(
869
- "DELETE",
870
- `/api/v1/span-layers/${spanLayerId}/config/${namespace}/${configKey}`,
871
- configRequest(auditMessage, undefined, options, true),
872
- ),
1119
+ ...configMethods(this, "span-layers"),
873
1120
  ...constraintMethods(this, "span-layers"),
874
- /**
875
- * Get a span layer by ID.
876
- * @param {string} spanLayerId - The span layer ID
877
- */
878
- get: (spanLayerId) =>
879
- this._request("GET", `/api/v1/span-layers/${spanLayerId}`),
880
- /**
881
- * Delete a span layer.
882
- * @param {string} spanLayerId - The span layer ID
883
- */
884
- delete: (spanLayerId, auditMessage) =>
885
- this._request("DELETE", `/api/v1/span-layers/${spanLayerId}`, {
886
- auditMessage,
887
- }),
888
- /**
889
- * Update a span layer's name.
890
- * @param {string} spanLayerId - The span layer ID
891
- * @param {string} name - The name
892
- */
893
- update: (spanLayerId, name, auditMessage) =>
894
- this._request("PATCH", `/api/v1/span-layers/${spanLayerId}`, {
895
- auditMessage,
896
- body: bodyOf({ name }),
897
- }),
1121
+ ...layerMethods(this, "span-layers"),
898
1122
  /**
899
1123
  * Create a new span layer.
900
1124
  * @param {string} tokenLayerId - The token layer ID
901
1125
  * @param {string} name - The name
1126
+ * @param {string} [auditMessage] - Audit message for this write
1127
+ * @param {object} [opts]
1128
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
902
1129
  */
903
1130
  create: (tokenLayerId, name, auditMessage, { id } = {}) =>
904
1131
  this._request("POST", "/api/v1/span-layers", {
905
1132
  auditMessage,
906
1133
  body: bodyOf({ id, "token-layer-id": tokenLayerId, name }),
907
1134
  }),
908
- /**
909
- * Shift a span layer's display order.
910
- * @param {string} spanLayerId - The span layer ID
911
- * @param {string} direction - The direction ("up" or "down")
912
- */
913
- shift: (spanLayerId, direction, auditMessage) =>
914
- this._request("POST", `/api/v1/span-layers/${spanLayerId}/shift`, {
915
- auditMessage,
916
- body: bodyOf({ direction }),
917
- }),
918
1135
  };
919
1136
 
920
1137
  this.spans = {
@@ -922,6 +1139,7 @@ class PlaidClient {
922
1139
  * Replace tokens for a span.
923
1140
  * @param {string} spanId - The span ID
924
1141
  * @param {Array} tokens - The tokens
1142
+ * @param {string} [auditMessage] - Audit message for this write
925
1143
  */
926
1144
  setTokens: (spanId, tokens, auditMessage) =>
927
1145
  this._request("PUT", `/api/v1/spans/${spanId}/tokens`, {
@@ -935,6 +1153,9 @@ class PlaidClient {
935
1153
  * @param {Array} tokens - The tokens
936
1154
  * @param {any} value - The value
937
1155
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1156
+ * @param {string} [auditMessage] - Audit message for this write
1157
+ * @param {object} [opts]
1158
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
938
1159
  */
939
1160
  create: (spanLayerId, tokens, value, metadata, auditMessage, { id } = {}) =>
940
1161
  this._request("POST", "/api/v1/spans", {
@@ -956,6 +1177,7 @@ class PlaidClient {
956
1177
  /**
957
1178
  * Delete a span.
958
1179
  * @param {string} spanId - The span ID
1180
+ * @param {string} [auditMessage] - Audit message for this write
959
1181
  */
960
1182
  delete: (spanId, auditMessage) =>
961
1183
  this._request("DELETE", `/api/v1/spans/${spanId}`, { auditMessage }),
@@ -963,6 +1185,7 @@ class PlaidClient {
963
1185
  * Update a span's value.
964
1186
  * @param {string} spanId - The span ID
965
1187
  * @param {any} value - The value
1188
+ * @param {string} [auditMessage] - Audit message for this write
966
1189
  */
967
1190
  update: (spanId, value, auditMessage) =>
968
1191
  this._request("PATCH", `/api/v1/spans/${spanId}`, {
@@ -972,6 +1195,7 @@ class PlaidClient {
972
1195
  /**
973
1196
  * Create multiple spans in a single operation.
974
1197
  * @param {Array} body - The request body
1198
+ * @param {string} [auditMessage] - Audit message for this write
975
1199
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
976
1200
  */
977
1201
  bulkCreate: (body, auditMessage) =>
@@ -979,12 +1203,15 @@ class PlaidClient {
979
1203
  /**
980
1204
  * Delete multiple spans in a single operation. Provide an array of IDs.
981
1205
  * @param {Array} body - The request body
1206
+ * @param {string} [auditMessage] - Audit message for this write
1207
+ * @returns {Promise<void>}
982
1208
  */
983
1209
  bulkDelete: (body, auditMessage) =>
984
1210
  this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
985
1211
  /**
986
1212
  * Update many spans in a single operation: set values and/or patch metadata.
987
1213
  * @param {Array} body - Objects of the shape `{id, value?, metadata?}`. `value` is set only when the key is present (null sends JSON null); `metadata` is a list of metadata ops, as for patchMetadata. The spans may lie in several documents of one project; every document touched has its version bumped and every new version comes back in `X-Document-Versions` (past fifty documents, only their number, in `X-Document-Versions-Omitted`). A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
1214
+ * @param {string} [auditMessage] - Audit message for this write
988
1215
  * @returns {Promise<{count: number}>} How many spans were updated.
989
1216
  */
990
1217
  bulkUpdate: (body, auditMessage) =>
@@ -992,64 +1219,71 @@ class PlaidClient {
992
1219
  auditMessage,
993
1220
  body,
994
1221
  }),
995
- /**
996
- * Replace all metadata for a span.
997
- * @param {string} spanId - The span ID
998
- * @param {any} body - The request body
999
- */
1000
- setMetadata: (spanId, body, auditMessage) =>
1001
- this._request("PUT", `/api/v1/spans/${spanId}/metadata`, {
1002
- auditMessage,
1003
- rawBody: body,
1004
- }),
1005
- /**
1006
- * Remove all metadata from a span.
1007
- * @param {string} spanId - The span ID
1008
- */
1009
- deleteMetadata: (spanId, auditMessage) =>
1010
- this._request("DELETE", `/api/v1/spans/${spanId}/metadata`, {
1011
- auditMessage,
1012
- }),
1013
- /**
1014
- * Edit metadata for a span with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
1015
- * @param {string} spanId - The span ID
1016
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
1017
- */
1018
- patchMetadata: (spanId, body, auditMessage) =>
1019
- this._request("PATCH", `/api/v1/spans/${spanId}/metadata`, {
1020
- auditMessage,
1021
- rawBody: body,
1022
- }),
1222
+ ...metadataMethods(this, "spans"),
1023
1223
  };
1024
1224
 
1025
- // Server-level facts. Fetched at most once: the limits cannot change while
1026
- // the server is up, and a caller asking "will this file be accepted" should
1027
- // not pay a round trip to find out.
1028
- let infoPromise = null;
1225
+ // Server-level facts. Read once, and again only when they may have
1226
+ // changed: after this client's connection to the server was lost and came
1227
+ // back (a restart, the only way the limits change, always drops it), or
1228
+ // after the server refused a private data write as too large. A caller
1229
+ // asking "will this file be accepted" does not pay a round trip to find
1230
+ // out. The facts live on the client (`_serverFacts`), so a batch opened
1231
+ // on it reads the same.
1029
1232
  this.server = {
1030
1233
  /**
1031
1234
  * This server's version and the limits it enforces, e.g.
1032
1235
  * `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
1033
1236
  * Sizes are in bytes. Unauthenticated.
1237
+ * @returns {Promise<{limits: ServerLimits}>}
1034
1238
  */
1035
1239
  info: () => {
1036
- if (!infoPromise) {
1037
- infoPromise = this._request("GET", "/api/v1/info").catch((err) => {
1038
- // A failure must not be cached: a client that starts before the
1240
+ const facts = this._serverFacts;
1241
+ if (!facts.info) {
1242
+ const asked = this._request("GET", "/api/v1/info").catch((err) => {
1243
+ // A failure must not be kept: a client that starts before the
1039
1244
  // server is up would never see the limits at all.
1040
- infoPromise = null;
1245
+ if (facts.info === asked) facts.info = null;
1041
1246
  throw err;
1042
1247
  });
1248
+ facts.info = asked;
1043
1249
  }
1044
- return infoPromise;
1250
+ return facts.info;
1251
+ },
1252
+ /**
1253
+ * Read `GET /info` again and resolve to it. Until it answers, a reader
1254
+ * waits for it. The facts known before are kept when the server does
1255
+ * not answer, and the error is thrown.
1256
+ * @returns {Promise<{limits: ServerLimits}>}
1257
+ */
1258
+ refresh: () => {
1259
+ const facts = this._serverFacts;
1260
+ const before = facts.info;
1261
+ facts.stale = false;
1262
+ const fresh = this._request("GET", "/api/v1/info");
1263
+ const held = fresh.catch((err) => {
1264
+ if (facts.info === held) facts.info = before;
1265
+ if (before) return before;
1266
+ throw err;
1267
+ });
1268
+ held.catch(() => {});
1269
+ facts.info = held;
1270
+ return fresh;
1271
+ },
1272
+ /**
1273
+ * Just the limits, which is what a caller almost always wants.
1274
+ * @returns {Promise<ServerLimits>}
1275
+ */
1276
+ limits: async () => {
1277
+ // Through `self`: tsc types a bundle that names itself as
1278
+ // `this.server` in its own methods `any` in the declarations.
1279
+ const self = this;
1280
+ return (await self.server.info()).limits;
1045
1281
  },
1046
- /** Just the limits, which is what a caller almost always wants. */
1047
- limits: async () => (await this.server.info()).limits,
1048
1282
  /**
1049
1283
  * Liveness, version and database size. Unauthenticated, and served
1050
1284
  * outside the REST router at /health, so it answers even while the API
1051
1285
  * is refusing requests. Never cached — the point is that it is current.
1052
- * @returns {Promise<{ok: boolean, version: string, uptimeMs: number, audit: object}>}
1286
+ * @returns {Promise<ServerHealth>}
1053
1287
  */
1054
1288
  health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
1055
1289
  };
@@ -1073,34 +1307,7 @@ class PlaidClient {
1073
1307
  };
1074
1308
 
1075
1309
  this.texts = {
1076
- /**
1077
- * Replace all metadata for a text.
1078
- * @param {string} textId - The text ID
1079
- * @param {any} body - The request body
1080
- */
1081
- setMetadata: (textId, body, auditMessage) =>
1082
- this._request("PUT", `/api/v1/texts/${textId}/metadata`, {
1083
- auditMessage,
1084
- rawBody: body,
1085
- }),
1086
- /**
1087
- * Remove all metadata from a text.
1088
- * @param {string} textId - The text ID
1089
- */
1090
- deleteMetadata: (textId, auditMessage) =>
1091
- this._request("DELETE", `/api/v1/texts/${textId}/metadata`, {
1092
- auditMessage,
1093
- }),
1094
- /**
1095
- * Edit metadata for a text with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
1096
- * @param {string} textId - The text ID
1097
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
1098
- */
1099
- patchMetadata: (textId, body, auditMessage) =>
1100
- this._request("PATCH", `/api/v1/texts/${textId}/metadata`, {
1101
- auditMessage,
1102
- rawBody: body,
1103
- }),
1310
+ ...metadataMethods(this, "texts"),
1104
1311
  /**
1105
1312
  * Create a new text in a document's text layer. A text is a container for
1106
1313
  * one long string in `body` for a given layer.
@@ -1108,6 +1315,11 @@ class PlaidClient {
1108
1315
  * @param {string} documentId - The document ID
1109
1316
  * @param {string} body - The request body
1110
1317
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1318
+ * @param {string} [auditMessage]
1319
+ * @param {{id?: string, tokenEdges?: number[]}} [options] - `id`: the new text's id, a UUIDv7
1320
+ * this client minted. `tokenEdges`: the code-point offsets where the tokens made next on
1321
+ * this text begin and end. The body is stored composed, except a character one of them
1322
+ * falls inside, which stays as sent (see composeText).
1111
1323
  */
1112
1324
  create: (
1113
1325
  textLayerId,
@@ -1115,7 +1327,7 @@ class PlaidClient {
1115
1327
  body,
1116
1328
  metadata,
1117
1329
  auditMessage,
1118
- { id } = {},
1330
+ { id, tokenEdges } = {},
1119
1331
  ) =>
1120
1332
  this._request("POST", "/api/v1/texts", {
1121
1333
  auditMessage,
@@ -1124,6 +1336,7 @@ class PlaidClient {
1124
1336
  "text-layer-id": textLayerId,
1125
1337
  "document-id": documentId,
1126
1338
  body,
1339
+ "token-edges": tokenEdges,
1127
1340
  metadata,
1128
1341
  }),
1129
1342
  }),
@@ -1136,6 +1349,7 @@ class PlaidClient {
1136
1349
  /**
1137
1350
  * Delete a text and all dependent data.
1138
1351
  * @param {string} textId - The text ID
1352
+ * @param {string} [auditMessage] - Audit message for this write
1139
1353
  */
1140
1354
  delete: (textId, auditMessage) =>
1141
1355
  this._request("DELETE", `/api/v1/texts/${textId}`, { auditMessage }),
@@ -1195,14 +1409,14 @@ class PlaidClient {
1195
1409
  * List (or search) users. Transparently follows pagination cursors and
1196
1410
  * returns the full flat array. Admin-or-maintainer only.
1197
1411
  * @param {object} [opts]
1198
- * @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
1412
+ * @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text holds an at sign), case-insensitive
1199
1413
  */
1200
1414
  list: ({ q } = {}) =>
1201
1415
  listAll(this, "/api/v1/users", { query: { q } }),
1202
1416
  /**
1203
1417
  * Fetch a single page of users (optionally filtered by `q`).
1204
1418
  * @param {object} [opts]
1205
- * @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
1419
+ * @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text holds an at sign), case-insensitive
1206
1420
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1207
1421
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1208
1422
  * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
@@ -1216,7 +1430,7 @@ class PlaidClient {
1216
1430
  /**
1217
1431
  * Async-iterate users page by page; yields each page's entries array.
1218
1432
  * @param {object} [opts]
1219
- * @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text has an @), case-insensitive
1433
+ * @param {string} [opts.q] - Filter to users whose display name contains this text, or whose email starts with it (contains it, when the text holds an at sign), case-insensitive
1220
1434
  * @param {number} [opts.pageSize] - Per-request page size
1221
1435
  * @returns {AsyncGenerator<Array>}
1222
1436
  */
@@ -1234,12 +1448,13 @@ class PlaidClient {
1234
1448
  * @param {boolean} isAdmin - Whether the user is an admin
1235
1449
  * @param {string} [displayName] - How the user is shown in the UI.
1236
1450
  * Defaults to the local part of the email.
1451
+ * @param {string} [auditMessage] - Audit message for this write
1237
1452
  */
1238
1453
  create: (email, password, isAdmin, displayName, auditMessage) =>
1239
1454
  this._request("POST", "/api/v1/users", {
1240
1455
  auditMessage,
1241
1456
  body: bodyOf({
1242
- email,
1457
+ email: normalizeUserId(email),
1243
1458
  password,
1244
1459
  "is-admin": isAdmin,
1245
1460
  // Left out when not given (null too), so core gives the user the
@@ -1275,6 +1490,10 @@ class PlaidClient {
1275
1490
  * that may be long: audit() walks every page before it resolves.
1276
1491
  * @param {string} userId - The user ID
1277
1492
  * @param {object} [opts]
1493
+ * @param {string} [opts.startTime] - Only entries at or after this instant
1494
+ * @param {string} [opts.endTime] - Only entries at or before this instant
1495
+ * @param {string[]|string} [opts.opTypes] - Only these op types
1496
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
1278
1497
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1279
1498
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1280
1499
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -1314,6 +1533,7 @@ class PlaidClient {
1314
1533
  * stays visible in listings with a deactivated-at timestamp.
1315
1534
  * Reversible via activate(), which restores login only.
1316
1535
  * @param {string} id - The resource ID
1536
+ * @param {string} [auditMessage] - Audit message for this write
1317
1537
  */
1318
1538
  delete: (id, auditMessage) =>
1319
1539
  this._request("DELETE", `/api/v1/users/${id}`, { auditMessage }),
@@ -1322,6 +1542,7 @@ class PlaidClient {
1322
1542
  * Project memberships, vocab maintainerships, and API tokens removed
1323
1543
  * at deactivation are NOT restored — re-grant them deliberately.
1324
1544
  * @param {string} id - The resource ID
1545
+ * @param {string} [auditMessage] - Audit message for this write
1325
1546
  */
1326
1547
  activate: (id, auditMessage) =>
1327
1548
  this._request("POST", `/api/v1/users/${id}/activate`, { auditMessage }),
@@ -1337,6 +1558,7 @@ class PlaidClient {
1337
1558
  * @param {string} [password] - New password
1338
1559
  * @param {string} [displayName] - New display name
1339
1560
  * @param {boolean} [isAdmin] - New admin status
1561
+ * @param {string} [auditMessage] - Audit message for this write
1340
1562
  */
1341
1563
  update: (id, password, displayName, isAdmin, auditMessage) =>
1342
1564
  this._request("PATCH", `/api/v1/users/${id}`, {
@@ -1348,23 +1570,89 @@ class PlaidClient {
1348
1570
  }),
1349
1571
  }),
1350
1572
  /**
1351
- * Build a URL for a user's profile picture, suitable for use directly as
1352
- * an `<img>` src. An image element cannot send an Authorization header,
1353
- * so the session token rides in the query string, the same way document
1354
- * media does.
1573
+ * A token that shows any user's profile picture without an
1574
+ * Authorization header, for an `<img>`, which cannot send one. Resolves
1575
+ * to `{ token, expiresAt }` (`expiresAt` an ISO-8601 instant, a day on by
1576
+ * default). The server refuses it on every route but the picture, and
1577
+ * it stops working when the user signs out or changes password.
1578
+ *
1579
+ * One token serves the whole client: it is kept and handed out again
1580
+ * until under ten minutes of it remain (half its life, for one minted
1581
+ * near the end of a login), calls made while one is being minted share
1582
+ * that request, and it is dropped when the client's token changes.
1583
+ * avatarUrl() uses it.
1584
+ * @param {{renew?: boolean}} [options] - `renew`: mint a new one
1585
+ * whatever is kept, unless one was minted in the last few seconds
1586
+ * (or is being minted), which is shared
1587
+ * @returns {Promise<{token: string, expiresAt: string}>}
1588
+ */
1589
+ avatarToken: ({ renew = false } = {}) => {
1590
+ // A batch is a view of its client and shares the client's token.
1591
+ const owner = this.client ?? this;
1592
+ const cached = owner._avatarToken;
1593
+ if (cached && cached.forToken === owner.token) {
1594
+ if (cached.pending) return cached.pending;
1595
+ const now = owner.serverNow().getTime();
1596
+ // A renewal shares one minted a moment ago: every picture on a page
1597
+ // fails together when a token dies, and each asks.
1598
+ if (renew && now - cached.mintedAt < AVATAR_TOKEN_FRESH_MS) {
1599
+ return Promise.resolve(cached.link);
1600
+ }
1601
+ const expires = Date.parse(cached.link.expiresAt);
1602
+ // Renewed ten minutes before it runs out, or at half its life when
1603
+ // it was minted for less than twenty: the server caps it at the
1604
+ // login's own expiry, and near the end of a login every token it
1605
+ // mints runs out at that same moment, so a fixed ten minutes asked
1606
+ // for a new one on every call.
1607
+ const renewAt = Math.min(AVATAR_TOKEN_RENEW_MS, (expires - cached.mintedAt) / 2);
1608
+ if (!renew && expires - now > renewAt) return Promise.resolve(cached.link);
1609
+ }
1610
+ const entry = {
1611
+ forToken: owner.token,
1612
+ pending: null,
1613
+ link: null,
1614
+ mintedAt: owner.serverNow().getTime(),
1615
+ };
1616
+ entry.pending = owner
1617
+ ._request("POST", "/api/v1/avatar-link", { outOfBand: true })
1618
+ .then(
1619
+ (link) => {
1620
+ entry.pending = null;
1621
+ entry.link = link;
1622
+ return link;
1623
+ },
1624
+ (e) => {
1625
+ if (owner._avatarToken === entry) owner._avatarToken = null;
1626
+ throw e;
1627
+ },
1628
+ );
1629
+ owner._avatarToken = entry;
1630
+ return entry.pending;
1631
+ },
1632
+ /**
1633
+ * Build a URL for a user's profile picture, for an `<img>` src. The
1634
+ * URL carries the client's avatar token (see avatarToken()), never the
1635
+ * login token.
1355
1636
  *
1356
1637
  * Pass the user record's `avatarHash` as the second argument whenever you
1357
1638
  * have it: the URL then addresses that exact picture, so the browser can
1358
1639
  * cache it indefinitely and still pick up a replacement the moment the
1359
- * user changes it. Returns null when the user has no picture, so callers
1360
- * can fall back to initials without a wasted request.
1640
+ * user changes it. Resolves to null when `avatarHash` is null (the user
1641
+ * has no picture), so callers can fall back to initials without a
1642
+ * wasted request.
1361
1643
  * @param {string} id - The user ID
1362
- * @param {string} [avatarHash] - The user record's `avatarHash`
1363
- * @returns {string|null}
1644
+ * @param {string|null} [avatarHash] - The user record's `avatarHash`
1645
+ * @param {{renew?: boolean}} [options] - `renew`: mint a new avatar
1646
+ * token first, for a picture its URL failed to load (the token
1647
+ * stops working when the login it was minted under ends)
1648
+ * @returns {Promise<string|null>}
1364
1649
  */
1365
- avatarUrl: (id, avatarHash) => {
1650
+ avatarUrl: async (id, avatarHash, { renew = false } = {}) => {
1366
1651
  if (avatarHash === null) return null;
1367
- const params = new URLSearchParams({ token: this.token });
1652
+ // Through `self`, as in server.limits.
1653
+ const self = this;
1654
+ const { token } = await self.users.avatarToken({ renew });
1655
+ const params = new URLSearchParams({ "avatar-token": token });
1368
1656
  if (avatarHash) params.set("v", avatarHash);
1369
1657
  return `${this.baseUrl}/api/v1/users/${id}/avatar?${params}`;
1370
1658
  },
@@ -1384,6 +1672,7 @@ class PlaidClient {
1384
1672
  * PNG, JPEG, WebP, and GIF. Resolves to the updated user record.
1385
1673
  * @param {string} id - The user ID
1386
1674
  * @param {File} file - The image to upload
1675
+ * @param {string} [auditMessage] - Audit message for this write
1387
1676
  */
1388
1677
  setAvatar: (id, file, auditMessage) => {
1389
1678
  const fd = new FormData();
@@ -1398,6 +1687,7 @@ class PlaidClient {
1398
1687
  /**
1399
1688
  * Remove a profile picture. Your own, or anyone's if you are an admin.
1400
1689
  * @param {string} id - The user ID
1690
+ * @param {string} [auditMessage] - Audit message for this write
1401
1691
  */
1402
1692
  // No flag: the upload beside this one is multipart and cannot be
1403
1693
  // batched, but a DELETE carries no blob, so the batch transport takes
@@ -1421,7 +1711,8 @@ class PlaidClient {
1421
1711
  * every project without dragging down the transcripts beside them.
1422
1712
  *
1423
1713
  * `pageSize` is exposed here, and defaults lower than elsewhere, because
1424
- * one value runs to 1 MB: a page of them with `includeValues` is the
1714
+ * one value runs to megabytes (`userDataValueBytes` in
1715
+ * `server.limits()`): a page of them with `includeValues` is the
1425
1716
  * largest response this API can be asked for. Raise it when the listing
1426
1717
  * is keys, or the values are known to be small.
1427
1718
  * @param {string} userId
@@ -1430,6 +1721,7 @@ class PlaidClient {
1430
1721
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1431
1722
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1432
1723
  * @param {number} [opts.pageSize=100] - Entries per request (1..1000)
1724
+ * @returns {Promise<UserDataEntry[]>}
1433
1725
  */
1434
1726
  list: (userId, { prefix, pattern, includeValues, pageSize = 100 } = {}) =>
1435
1727
  listAll(this, `/api/v1/users/${userId}/data`, {
@@ -1445,7 +1737,7 @@ class PlaidClient {
1445
1737
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1446
1738
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1447
1739
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1448
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1740
+ * @returns {Promise<Page<UserDataEntry>>}
1449
1741
  */
1450
1742
  listPage: (
1451
1743
  userId,
@@ -1464,6 +1756,7 @@ class PlaidClient {
1464
1756
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1465
1757
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1466
1758
  * @param {number} [opts.pageSize=100] - Entries per request (1..1000)
1759
+ * @returns {AsyncGenerator<UserDataEntry[]>}
1467
1760
  */
1468
1761
  iterPages: (
1469
1762
  userId,
@@ -1474,9 +1767,10 @@ class PlaidClient {
1474
1767
  query: { prefix, pattern, "include-values": includeValues },
1475
1768
  }),
1476
1769
  /**
1477
- * Read one private data entry ({key, updatedAt, value}); 404 if absent.
1770
+ * Read one private data entry ({key, updatedAt, version, value}); 404 if absent.
1478
1771
  * @param {string} userId
1479
1772
  * @param {string} key
1773
+ * @returns {Promise<UserDataEntry & {value: any}>}
1480
1774
  */
1481
1775
  get: (userId, key) =>
1482
1776
  this._request(
@@ -1484,27 +1778,51 @@ class PlaidClient {
1484
1778
  `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1485
1779
  ),
1486
1780
  /**
1487
- * Create or replace one private data entry. `value` is any JSON (up to
1488
- * 1 MB). Not audited, not batchable.
1781
+ * Create or replace one private data entry. `value` is any JSON, up to
1782
+ * the server's `userDataValueBytes` (`server.limits()`, 5 MB by
1783
+ * default), and is refused with 413 over it, after which
1784
+ * `server.limits()` answers the figure the server has now. Not audited,
1785
+ * not batchable.
1489
1786
  *
1490
1787
  * The server stores it verbatim, but this client recases object keys on
1491
1788
  * the way out and back like any other body (`myKey` <-> `my-key`), so a
1492
1789
  * value whose keys are camelCase round-trips unchanged while one keyed
1493
1790
  * by arbitrary strings does not. Put such a map under a `metadata` key,
1494
1791
  * which both clients pass through untouched.
1792
+ *
1793
+ * Every write raises the entry's `version` by one, and reads answer it.
1794
+ * With `version`, the write lands only when the entry is still at that
1795
+ * version (0: only when there is no entry), and is otherwise refused with
1796
+ * 409, `error: "version-mismatch"`, and the stored `version` and
1797
+ * `updatedAt` on the error's `responseData`: read it again and make the
1798
+ * change on what is there.
1495
1799
  * @param {string} userId
1496
1800
  * @param {string} key
1497
1801
  * @param {*} value
1802
+ * @param {object} [opts]
1803
+ * @param {number} [opts.version] - The version this write was made from
1804
+ * @returns {Promise<{key: string, updatedAt: string, version: number}>}
1498
1805
  */
1499
- put: (userId, key, value) =>
1500
- this._request(
1501
- "PUT",
1502
- `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1503
- {
1504
- body: value,
1505
- noBatch: true,
1506
- },
1507
- ),
1806
+ put: async (userId, key, value, { version } = {}) => {
1807
+ try {
1808
+ return await this._request(
1809
+ "PUT",
1810
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1811
+ {
1812
+ body: value,
1813
+ noBatch: true,
1814
+ queryParams: { version },
1815
+ },
1816
+ );
1817
+ } catch (err) {
1818
+ if (err?.status === 413) {
1819
+ // The cap may have changed since it was read: the caller deciding
1820
+ // what to do next reads the server's figure now.
1821
+ await this.server.refresh().catch(() => {});
1822
+ }
1823
+ throw err;
1824
+ }
1825
+ },
1508
1826
  /**
1509
1827
  * Delete one private data entry; 404 if absent.
1510
1828
  * @param {string} userId
@@ -1556,6 +1874,7 @@ class PlaidClient {
1556
1874
  * needs a sign-in token.
1557
1875
  * @param {string} userId - The user ID who will own the token
1558
1876
  * @param {string} name - A human label, e.g. "Stanza parser"
1877
+ * @param {string} [auditMessage] - Audit message for this write
1559
1878
  * @returns {Promise<{id: string, name: string, token: string}>}
1560
1879
  */
1561
1880
  create: (userId, name, auditMessage) =>
@@ -1572,6 +1891,7 @@ class PlaidClient {
1572
1891
  * needs a sign-in token.
1573
1892
  * @param {string} userId - The user ID who owns the token
1574
1893
  * @param {string} tokenId - The token ID to revoke
1894
+ * @param {string} [auditMessage] - Audit message for this write
1575
1895
  */
1576
1896
  revoke: (userId, tokenId, auditMessage) =>
1577
1897
  this._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
@@ -1589,6 +1909,7 @@ class PlaidClient {
1589
1909
  * @param {object} [opts]
1590
1910
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1591
1911
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1912
+ * @returns {Promise<Invite[]>}
1592
1913
  */
1593
1914
  list: ({ projectId, all } = {}) =>
1594
1915
  listAll(this, "/api/v1/invites", {
@@ -1601,7 +1922,7 @@ class PlaidClient {
1601
1922
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1602
1923
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1603
1924
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1604
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1925
+ * @returns {Promise<Page<Invite>>}
1605
1926
  */
1606
1927
  listPage: ({ projectId, all, limit, cursor } = {}) =>
1607
1928
  listPage(this, "/api/v1/invites", {
@@ -1615,7 +1936,7 @@ class PlaidClient {
1615
1936
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1616
1937
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1617
1938
  * @param {number} [opts.pageSize] - Per-request page size
1618
- * @returns {AsyncGenerator<Array>}
1939
+ * @returns {AsyncGenerator<Invite[]>}
1619
1940
  */
1620
1941
  iterPages: ({ projectId, all, pageSize } = {}) =>
1621
1942
  iterPages(this, "/api/v1/invites", {
@@ -1648,7 +1969,8 @@ class PlaidClient {
1648
1969
  * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
1649
1970
  * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
1650
1971
  * @param {string} [opts.note] - Human label shown in your invite list
1651
- * @returns {Promise<{id: string, code: string, kind: string, status: string}>}
1972
+ * @param {string} [auditMessage] - Audit message for this write
1973
+ * @returns {Promise<Invite & {code: string}>}
1652
1974
  */
1653
1975
  create: (
1654
1976
  {
@@ -1681,6 +2003,7 @@ class PlaidClient {
1681
2003
  * Revoke an invite, killing the link immediately. Idempotent. Allowed for
1682
2004
  * the creator, an admin, or a maintainer of the invite's project.
1683
2005
  * @param {string} id - The invite ID
2006
+ * @param {string} [auditMessage] - Audit message for this write
1684
2007
  */
1685
2008
  revoke: (id, auditMessage) =>
1686
2009
  this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
@@ -1693,7 +2016,7 @@ class PlaidClient {
1693
2016
  * configuration and the backups on disk, and the settings an operator
1694
2017
  * gets asked to confirm. Carries no secrets. Runs a count per table and
1695
2018
  * walks the media directory, so open it, do not poll it. Admin only.
1696
- * @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
2019
+ * @returns {Promise<ServerReport>}
1697
2020
  */
1698
2021
  server: () => this._request("GET", "/api/v1/admin/server"),
1699
2022
  /**
@@ -1707,7 +2030,7 @@ class PlaidClient {
1707
2030
  * rather than the client's usual timeout. A proxy in front of the server
1708
2031
  * may still give up first (a 504): the backup goes on, and its file
1709
2032
  * shows in `server().backup.backups` once it is written.
1710
- * @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
2033
+ * @returns {Promise<BackupStatus>}
1711
2034
  *
1712
2035
  * outOfBand, as every admin action on the server itself is: none of them
1713
2036
  * writes project data (see the note at the top of http.js).
@@ -1727,6 +2050,7 @@ class PlaidClient {
1727
2050
  * Drop the lock on a document whoever holds it. Idempotent. For a client
1728
2051
  * that went away without releasing one. Admin only.
1729
2052
  * @param {string} documentId - The document ID
2053
+ * @returns {Promise<{result: string}>}
1730
2054
  */
1731
2055
  releaseLock: (documentId) =>
1732
2056
  this._request("DELETE", `/api/v1/admin/locks/${documentId}`, {
@@ -1736,7 +2060,7 @@ class PlaidClient {
1736
2060
  * Live login and invite rate-limit buckets: the address, the account
1737
2061
  * where there is one, failures inside the window, the limit, and whether
1738
2062
  * it is currently blocking. Admin only.
1739
- * @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
2063
+ * @returns {Promise<RateLimitSnapshot>}
1740
2064
  */
1741
2065
  rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
1742
2066
  /**
@@ -1746,6 +2070,7 @@ class PlaidClient {
1746
2070
  * @param {object} [opts]
1747
2071
  * @param {string} [opts.ip] - Clear only this address
1748
2072
  * @param {string} [opts.userId] - Narrow to one account on that address
2073
+ * @returns {Promise<{result: string}>}
1749
2074
  */
1750
2075
  clearRateLimits: ({ ip, userId } = {}) =>
1751
2076
  this._request("DELETE", "/api/v1/admin/rate-limits", {
@@ -1769,7 +2094,7 @@ class PlaidClient {
1769
2094
  * @param {string} [opts.status] - "2xx".."5xx", an exact code, or "failures"
1770
2095
  * @param {string} [opts.user] - Only requests made by this account
1771
2096
  * @param {string} [opts.method] - Only requests with this HTTP method
1772
- * @returns {Promise<{requests: object, events: object, file: (string|null)}>}
2097
+ * @returns {Promise<LiveLog>}
1773
2098
  */
1774
2099
  logs: ({ limit, q, level, status, user, method } = {}) =>
1775
2100
  this._request("GET", "/api/v1/admin/logs", {
@@ -1803,7 +2128,7 @@ class PlaidClient {
1803
2128
  * @param {string} [opts.prefix] - Only keys starting with this
1804
2129
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1805
2130
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1806
- * @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
2131
+ * @returns {Promise<AdminUserDataEntry[]>}
1807
2132
  */
1808
2133
  userData: ({ prefix, pattern, includeValues } = {}) =>
1809
2134
  listAll(this, "/api/v1/admin/user-data", {
@@ -1822,7 +2147,7 @@ class PlaidClient {
1822
2147
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1823
2148
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1824
2149
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1825
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2150
+ * @returns {Promise<Page<AdminUserDataEntry>>}
1826
2151
  */
1827
2152
  userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
1828
2153
  listPage(this, "/api/v1/admin/user-data", {
@@ -1863,6 +2188,10 @@ class PlaidClient {
1863
2188
  /**
1864
2189
  * Fetch a single page of the instance-wide audit log. Admin only.
1865
2190
  * @param {object} [opts]
2191
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2192
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2193
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2194
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
1866
2195
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
1867
2196
  * is what a feed wants. A cursor belongs to the direction that made it.
1868
2197
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
@@ -1889,6 +2218,12 @@ class PlaidClient {
1889
2218
  }),
1890
2219
  /**
1891
2220
  * Async-iterate the instance-wide audit log page by page. Admin only.
2221
+ * @param {object} [opts]
2222
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2223
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2224
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2225
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
2226
+ * @param {number} [opts.pageSize] - Per-request page size
1892
2227
  * @returns {AsyncGenerator<Array>}
1893
2228
  */
1894
2229
  iterPages: ({ startTime, endTime, opTypes, kinds, pageSize } = {}) =>
@@ -1917,7 +2252,7 @@ class PlaidClient {
1917
2252
  * @param {boolean} [opts.daily] - Also return `byDay`, a list of
1918
2253
  * `{date, changes}` per user, oldest first, at the cost of a second
1919
2254
  * grouped scan
1920
- * @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
2255
+ * @returns {Promise<ActivityTallyRow[]>}
1921
2256
  */
1922
2257
  tally: async ({ projectId, startTime, endTime, daily } = {}) => {
1923
2258
  const path = projectId
@@ -1935,22 +2270,16 @@ class PlaidClient {
1935
2270
  };
1936
2271
 
1937
2272
  this.tokenLayers = {
1938
- /**
1939
- * Shift a token layer's display order.
1940
- * @param {string} tokenLayerId - The token layer ID
1941
- * @param {string} direction - The direction ("up" or "down")
1942
- */
1943
- shift: (tokenLayerId, direction, auditMessage) =>
1944
- this._request("POST", `/api/v1/token-layers/${tokenLayerId}/shift`, {
1945
- auditMessage,
1946
- body: bodyOf({ direction }),
1947
- }),
2273
+ ...layerMethods(this, "token-layers"),
1948
2274
  /**
1949
2275
  * Create a new token layer.
1950
2276
  * @param {string} textLayerId - The text layer ID
1951
2277
  * @param {string} name - The name
1952
2278
  * @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.
1953
2279
  * @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).
2280
+ * @param {string} [auditMessage] - Audit message for this write
2281
+ * @param {object} [opts]
2282
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
1954
2283
  */
1955
2284
  create: (
1956
2285
  textLayerId,
@@ -1960,71 +2289,18 @@ class PlaidClient {
1960
2289
  auditMessage,
1961
2290
  { id } = {},
1962
2291
  ) =>
1963
- this._request("POST", "/api/v1/token-layers", {
1964
- auditMessage,
1965
- body: bodyOf({
1966
- id,
1967
- "text-layer-id": textLayerId,
1968
- name,
1969
- "overlap-mode": overlapMode,
1970
- "parent-token-layer-id": parentTokenLayerId,
1971
- }),
1972
- }),
1973
- /**
1974
- * Set a configuration value for a layer in an editor namespace.
1975
- * @param {string} tokenLayerId - The token layer ID
1976
- * @param {string} namespace - The config namespace
1977
- * @param {string} configKey - The config key
1978
- * @param {any} configValue - Configuration value to set
1979
- * @param {string} [auditMessage] - Audit message for this write
1980
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
1981
- * was absent). The write is then refused with a 409 when the stored value differs.
1982
- */
1983
- setConfig: (tokenLayerId, namespace, configKey, configValue, auditMessage, options) =>
1984
- this._request(
1985
- "PUT",
1986
- `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
1987
- configRequest(auditMessage, configValue, options),
1988
- ),
1989
- /**
1990
- * Remove a configuration value for a layer.
1991
- * @param {string} tokenLayerId - The token layer ID
1992
- * @param {string} namespace - The config namespace
1993
- * @param {string} configKey - The config key
1994
- * @param {string} [auditMessage] - Audit message for this write
1995
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
1996
- */
1997
- deleteConfig: (tokenLayerId, namespace, configKey, auditMessage, options) =>
1998
- this._request(
1999
- "DELETE",
2000
- `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
2001
- configRequest(auditMessage, undefined, options, true),
2002
- ),
2003
- ...constraintMethods(this, "token-layers"),
2004
- /**
2005
- * Get a token layer by ID.
2006
- * @param {string} tokenLayerId - The token layer ID
2007
- */
2008
- get: (tokenLayerId) =>
2009
- this._request("GET", `/api/v1/token-layers/${tokenLayerId}`),
2010
- /**
2011
- * Delete a token layer.
2012
- * @param {string} tokenLayerId - The token layer ID
2013
- */
2014
- delete: (tokenLayerId, auditMessage) =>
2015
- this._request("DELETE", `/api/v1/token-layers/${tokenLayerId}`, {
2016
- auditMessage,
2017
- }),
2018
- /**
2019
- * Update a token layer's name.
2020
- * @param {string} tokenLayerId - The token layer ID
2021
- * @param {string} name - The name
2022
- */
2023
- update: (tokenLayerId, name, auditMessage) =>
2024
- this._request("PATCH", `/api/v1/token-layers/${tokenLayerId}`, {
2292
+ this._request("POST", "/api/v1/token-layers", {
2025
2293
  auditMessage,
2026
- body: bodyOf({ name }),
2294
+ body: bodyOf({
2295
+ id,
2296
+ "text-layer-id": textLayerId,
2297
+ name,
2298
+ "overlap-mode": overlapMode,
2299
+ "parent-token-layer-id": parentTokenLayerId,
2300
+ }),
2027
2301
  }),
2302
+ ...configMethods(this, "token-layers"),
2303
+ ...constraintMethods(this, "token-layers"),
2028
2304
  };
2029
2305
 
2030
2306
  this.documents = {
@@ -2071,6 +2347,7 @@ class PlaidClient {
2071
2347
  * outOfBand, for the same reason as acquireLock.
2072
2348
  * @param {string} documentId - The document ID
2073
2349
  * @param {string} lockId - The `lockId` acquireLock answered with
2350
+ * @param {string} [auditMessage] - Audit message for this write
2074
2351
  */
2075
2352
  renewLock: (documentId, lockId, auditMessage) =>
2076
2353
  this._request("POST", `/api/v1/documents/${documentId}/lock`, {
@@ -2085,6 +2362,7 @@ class PlaidClient {
2085
2362
  * and not released at all if it aborts.
2086
2363
  * @param {string} documentId - The document ID
2087
2364
  * @param {string} lockId - The `lockId` acquireLock answered with
2365
+ * @param {string} [auditMessage] - Audit message for this write
2088
2366
  */
2089
2367
  releaseLock: (documentId, lockId, auditMessage) =>
2090
2368
  this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
@@ -2128,9 +2406,11 @@ class PlaidClient {
2128
2406
  * lost lock is recorded on the CLIENT, like strict mode, so it stops
2129
2407
  * every write the client makes (on any batch of it too) and not only the
2130
2408
  * ones this block makes.
2409
+ * @template T
2131
2410
  * @param {string} documentId - The document ID
2132
- * @param {(lock: DocumentLock) => any} fn - The work to run while holding it
2133
- * @param {object} [options] - `{ keepAlive }`: renew on a timer (default true)
2411
+ * @param {(lock: import("./documentLock.js").DocumentLock) => T | Promise<T>} fn - The work to run while holding it
2412
+ * @param {{keepAlive?: boolean}} [options] - `{ keepAlive }`: renew on a timer (default true)
2413
+ * @returns {Promise<T>}
2134
2414
  */
2135
2415
  locked: (documentId, fn, options) =>
2136
2416
  withDocumentLock(this, documentId, fn, options),
@@ -2139,22 +2419,45 @@ class PlaidClient {
2139
2419
  * no as-of form: the route refuses the parameter. Prefer the document's
2140
2420
  * own `mediaUrl`, which carries the file's version for caching.
2141
2421
  * @param {string} documentId - The document ID
2422
+ * @returns {Promise<ArrayBuffer>}
2142
2423
  */
2143
2424
  getMedia: (documentId) =>
2144
2425
  this._request("GET", `/api/v1/documents/${documentId}/media`, {
2145
2426
  binaryResponse: true,
2146
2427
  }),
2428
+ /**
2429
+ * Get a link that plays the document's recording without an
2430
+ * Authorization header, for an audio or video element's `src`, which
2431
+ * cannot send one. `url` is the document's media URL with a
2432
+ * `media-token` added, resolved against the client's base URL. The token
2433
+ * opens only this recording, for this user, and the server refuses it on
2434
+ * every other route. It expires at `expiresAt` (an ISO-8601 instant, six
2435
+ * hours on by default), and sooner when the user signs out, changes
2436
+ * password or loses access to the project: ask for a new link then. A
2437
+ * document with no recording is a 404. Writes nothing, so it takes no
2438
+ * Idempotency-Key and goes over the wire when made on a batch.
2439
+ * @param {string} documentId - The document ID
2440
+ * @returns {Promise<{url: string, expiresAt: string}>}
2441
+ */
2442
+ mediaLink: async (documentId) => {
2443
+ const link = await this._request(
2444
+ "POST",
2445
+ `/api/v1/documents/${documentId}/media/link`,
2446
+ { outOfBand: true },
2447
+ );
2448
+ return { ...link, url: `${this.baseUrl}${link.url}` };
2449
+ },
2147
2450
  /**
2148
2451
  * Upload a media file for a document. Uses Apache Tika for content validation.
2149
2452
  * A document that already has a recording refuses it with a 409
2150
2453
  * (`media-exists`, and the current `media-url`): delete that one first.
2151
- * A WAV whose samples are not PCM or float (IMA or MS ADPCM, GSM, A-law
2152
- * and the rest), which browsers cannot play, is refused with a 415
2153
- * whose `error` names its coding.
2454
+ * A WAV whose samples are not PCM, 32-bit float, A-law or mu-law (IMA or
2455
+ * MS ADPCM, GSM, 64-bit float and the rest), which browsers cannot play,
2456
+ * is refused with a 415 whose `error` names its coding.
2154
2457
  * @param {string} documentId - The document ID
2155
2458
  * @param {File} file - The file to upload
2156
2459
  * @param {string} [auditMessage] - Custom audit-log message for this write
2157
- * @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.
2460
+ * @param {{onProgress?: (progress: {loaded: number, total: number | null}) => void}} [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.
2158
2461
  */
2159
2462
  uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
2160
2463
  const fd = new FormData();
@@ -2186,37 +2489,14 @@ class PlaidClient {
2186
2489
  auditMessage,
2187
2490
  queryParams: { "media-version": mediaVersion },
2188
2491
  }),
2189
- /**
2190
- * Replace all metadata for a document.
2191
- * @param {string} documentId - The document ID
2192
- * @param {any} body - The request body
2193
- */
2194
- setMetadata: (documentId, body, auditMessage) =>
2195
- this._request("PUT", `/api/v1/documents/${documentId}/metadata`, {
2196
- auditMessage,
2197
- rawBody: body,
2198
- }),
2199
- /**
2200
- * Remove all metadata from a document.
2201
- * @param {string} documentId - The document ID
2202
- */
2203
- deleteMetadata: (documentId, auditMessage) =>
2204
- this._request("DELETE", `/api/v1/documents/${documentId}/metadata`, {
2205
- auditMessage,
2206
- }),
2207
- /**
2208
- * Edit metadata for a document with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
2209
- * @param {string} documentId - The document ID
2210
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
2211
- */
2212
- patchMetadata: (documentId, body, auditMessage) =>
2213
- this._request("PATCH", `/api/v1/documents/${documentId}/metadata`, {
2214
- auditMessage,
2215
- rawBody: body,
2216
- }),
2492
+ ...metadataMethods(this, "documents"),
2217
2493
  /**
2218
2494
  * Get audit log for a document. Transparently follows pagination cursors
2219
- * and returns the full flat array.
2495
+ * and returns the full flat array. Each entry, and each of its `ops`,
2496
+ * says what kind of credential made it as `credential`: `login`,
2497
+ * `named-token` or `delegated` (absent on older operations, `service`
2498
+ * on some from 2026-10-06 to 2026-10-08),
2499
+ * beside `apiToken` (the named token's id and name) when there was one.
2220
2500
  * @param {string} documentId - The document ID
2221
2501
  * @param {string} [startTime] - Start of time range
2222
2502
  * @param {string} [endTime] - End of time range
@@ -2242,6 +2522,10 @@ class PlaidClient {
2242
2522
  * that may be long: audit() walks every page before it resolves.
2243
2523
  * @param {string} documentId - The document ID
2244
2524
  * @param {object} [opts]
2525
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2526
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2527
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2528
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
2245
2529
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2246
2530
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2247
2531
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -2278,7 +2562,7 @@ class PlaidClient {
2278
2562
  * @param {string} documentId - The document ID
2279
2563
  * @param {string} asOf - The moment to go back to (ISO-8601 instant),
2280
2564
  * typically a history entry's `endTime`
2281
- * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
2565
+ * @param {{dryRun?: boolean}} [options] - `{ dryRun }`: with `dryRun` true nothing
2282
2566
  * is written and the summary says what would change
2283
2567
  * @param {string} [auditMessage] - Custom audit message for this operation
2284
2568
  */
@@ -2316,6 +2600,7 @@ class PlaidClient {
2316
2600
  /**
2317
2601
  * Delete a document and all data contained.
2318
2602
  * @param {string} documentId - The document ID
2603
+ * @param {string} [auditMessage] - Audit message for this write
2319
2604
  */
2320
2605
  delete: (documentId, auditMessage) =>
2321
2606
  this._request("DELETE", `/api/v1/documents/${documentId}`, {
@@ -2325,6 +2610,7 @@ class PlaidClient {
2325
2610
  * Update a document's name.
2326
2611
  * @param {string} documentId - The document ID
2327
2612
  * @param {string} name - The name
2613
+ * @param {string} [auditMessage] - Audit message for this write
2328
2614
  */
2329
2615
  update: (documentId, name, auditMessage) =>
2330
2616
  this._request("PATCH", `/api/v1/documents/${documentId}`, {
@@ -2336,6 +2622,9 @@ class PlaidClient {
2336
2622
  * @param {string} projectId - The project ID
2337
2623
  * @param {string} name - The name
2338
2624
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
2625
+ * @param {string} [auditMessage] - Audit message for this write
2626
+ * @param {object} [opts]
2627
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2339
2628
  */
2340
2629
  create: (projectId, name, metadata, auditMessage, { id } = {}) =>
2341
2630
  this._request("POST", "/api/v1/documents", {
@@ -2352,9 +2641,10 @@ class PlaidClient {
2352
2641
  * the source had media the copy could not take with it.
2353
2642
  * @param {string} documentId - The document to copy
2354
2643
  * @param {string} name - The new document's name
2355
- * @param {object} [options] - `{ includeMedia }`: false leaves the media
2644
+ * @param {{includeMedia?: boolean, id?: string}} [options] - `{ includeMedia }`: false leaves the media
2356
2645
  * file behind
2357
2646
  * @param {string} [auditMessage] - Custom audit message for this operation
2647
+ * @returns {Promise<{id: string, mediaError?: string}>}
2358
2648
  */
2359
2649
  copy: (documentId, name, { includeMedia, id } = {}, auditMessage) =>
2360
2650
  this._request("POST", `/api/v1/documents/${documentId}/copy`, {
@@ -2368,6 +2658,7 @@ class PlaidClient {
2368
2658
  * Set a user's access level to read and write for this project.
2369
2659
  * @param {string} id - The resource ID
2370
2660
  * @param {string} userId - The user ID
2661
+ * @param {string} [auditMessage] - Audit message for this write
2371
2662
  */
2372
2663
  addWriter: (id, userId, auditMessage) =>
2373
2664
  this._request("POST", `/api/v1/projects/${id}/writers/${userId}`, {
@@ -2377,6 +2668,7 @@ class PlaidClient {
2377
2668
  * Remove a user's writer privileges for this project.
2378
2669
  * @param {string} id - The resource ID
2379
2670
  * @param {string} userId - The user ID
2671
+ * @param {string} [auditMessage] - Audit message for this write
2380
2672
  */
2381
2673
  removeWriter: (id, userId, auditMessage) =>
2382
2674
  this._request("DELETE", `/api/v1/projects/${id}/writers/${userId}`, {
@@ -2386,6 +2678,7 @@ class PlaidClient {
2386
2678
  * Set a user's access level to read-only for this project.
2387
2679
  * @param {string} id - The resource ID
2388
2680
  * @param {string} userId - The user ID
2681
+ * @param {string} [auditMessage] - Audit message for this write
2389
2682
  */
2390
2683
  addReader: (id, userId, auditMessage) =>
2391
2684
  this._request("POST", `/api/v1/projects/${id}/readers/${userId}`, {
@@ -2395,45 +2688,18 @@ class PlaidClient {
2395
2688
  * Remove a user's reader privileges for this project.
2396
2689
  * @param {string} id - The resource ID
2397
2690
  * @param {string} userId - The user ID
2691
+ * @param {string} [auditMessage] - Audit message for this write
2398
2692
  */
2399
2693
  removeReader: (id, userId, auditMessage) =>
2400
2694
  this._request("DELETE", `/api/v1/projects/${id}/readers/${userId}`, {
2401
2695
  auditMessage,
2402
2696
  }),
2403
- /**
2404
- * Set a configuration value for a project in an editor namespace.
2405
- * @param {string} id - The resource ID
2406
- * @param {string} namespace - The config namespace
2407
- * @param {string} configKey - The config key
2408
- * @param {any} configValue - Configuration value to set
2409
- * @param {string} [auditMessage] - Audit message for this write
2410
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
2411
- * was absent). The write is then refused with a 409 when the stored value differs.
2412
- */
2413
- setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
2414
- this._request(
2415
- "PUT",
2416
- `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
2417
- configRequest(auditMessage, configValue, options),
2418
- ),
2419
- /**
2420
- * Remove a configuration value for a project.
2421
- * @param {string} id - The resource ID
2422
- * @param {string} namespace - The config namespace
2423
- * @param {string} configKey - The config key
2424
- * @param {string} [auditMessage] - Audit message for this write
2425
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2426
- */
2427
- deleteConfig: (id, namespace, configKey, auditMessage, options) =>
2428
- this._request(
2429
- "DELETE",
2430
- `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
2431
- configRequest(auditMessage, undefined, options, true),
2432
- ),
2697
+ ...configMethods(this, "projects"),
2433
2698
  /**
2434
2699
  * Assign a user as a maintainer for this project.
2435
2700
  * @param {string} id - The resource ID
2436
2701
  * @param {string} userId - The user ID
2702
+ * @param {string} [auditMessage] - Audit message for this write
2437
2703
  */
2438
2704
  addMaintainer: (id, userId, auditMessage) =>
2439
2705
  this._request("POST", `/api/v1/projects/${id}/maintainers/${userId}`, {
@@ -2443,6 +2709,7 @@ class PlaidClient {
2443
2709
  * Remove a user's maintainer privileges for this project.
2444
2710
  * @param {string} id - The resource ID
2445
2711
  * @param {string} userId - The user ID
2712
+ * @param {string} [auditMessage] - Audit message for this write
2446
2713
  */
2447
2714
  removeMaintainer: (id, userId, auditMessage) =>
2448
2715
  this._request(
@@ -2452,7 +2719,11 @@ class PlaidClient {
2452
2719
  ),
2453
2720
  /**
2454
2721
  * Get audit log for a project. Transparently follows pagination cursors
2455
- * and returns the full flat array.
2722
+ * and returns the full flat array. Each entry, and each of its `ops`,
2723
+ * says what kind of credential made it as `credential`: `login`,
2724
+ * `named-token` or `delegated` (absent on older operations, `service`
2725
+ * on some from 2026-10-06 to 2026-10-08),
2726
+ * beside `apiToken` (the named token's id and name) when there was one.
2456
2727
  * @param {string} projectId - The project ID
2457
2728
  * @param {string} [startTime] - Start of time range
2458
2729
  * @param {string} [endTime] - End of time range
@@ -2478,6 +2749,10 @@ class PlaidClient {
2478
2749
  * that may be long: audit() walks every page before it resolves.
2479
2750
  * @param {string} projectId - The project ID
2480
2751
  * @param {object} [opts]
2752
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2753
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2754
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2755
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
2481
2756
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2482
2757
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2483
2758
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -2522,6 +2797,7 @@ class PlaidClient {
2522
2797
  * Link a vocabulary to a project.
2523
2798
  * @param {string} id - The resource ID
2524
2799
  * @param {string} vocabId - The vocab layer ID
2800
+ * @param {string} [auditMessage] - Audit message for this write
2525
2801
  */
2526
2802
  linkVocab: (id, vocabId, auditMessage) =>
2527
2803
  this._request("POST", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
@@ -2531,6 +2807,7 @@ class PlaidClient {
2531
2807
  * Unlink a vocabulary from a project.
2532
2808
  * @param {string} id - The resource ID
2533
2809
  * @param {string} vocabId - The vocab layer ID
2810
+ * @param {string} [auditMessage] - Audit message for this write
2534
2811
  */
2535
2812
  unlinkVocab: (id, vocabId, auditMessage) =>
2536
2813
  this._request("DELETE", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
@@ -2596,6 +2873,7 @@ class PlaidClient {
2596
2873
  * Update a project's name.
2597
2874
  * @param {string} id - The resource ID
2598
2875
  * @param {string} name - The name
2876
+ * @param {string} [auditMessage] - Audit message for this write
2599
2877
  */
2600
2878
  update: (id, name, auditMessage) =>
2601
2879
  this._request("PATCH", `/api/v1/projects/${id}`, {
@@ -2632,6 +2910,9 @@ class PlaidClient {
2632
2910
  /**
2633
2911
  * Create a new project. Note: this also registers the user as a maintainer.
2634
2912
  * @param {string} name - The name
2913
+ * @param {string} [auditMessage] - Audit message for this write
2914
+ * @param {object} [opts]
2915
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2635
2916
  */
2636
2917
  create: (name, auditMessage, { id } = {}) =>
2637
2918
  this._request("POST", "/api/v1/projects", {
@@ -2641,74 +2922,15 @@ class PlaidClient {
2641
2922
  };
2642
2923
 
2643
2924
  this.textLayers = {
2644
- /**
2645
- * Set a configuration value for a layer in an editor namespace.
2646
- * @param {string} textLayerId - The text layer ID
2647
- * @param {string} namespace - The config namespace
2648
- * @param {string} configKey - The config key
2649
- * @param {any} configValue - Configuration value to set
2650
- * @param {string} [auditMessage] - Audit message for this write
2651
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
2652
- * was absent). The write is then refused with a 409 when the stored value differs.
2653
- */
2654
- setConfig: (textLayerId, namespace, configKey, configValue, auditMessage, options) =>
2655
- this._request(
2656
- "PUT",
2657
- `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
2658
- configRequest(auditMessage, configValue, options),
2659
- ),
2660
- /**
2661
- * Remove a configuration value for a layer.
2662
- * @param {string} textLayerId - The text layer ID
2663
- * @param {string} namespace - The config namespace
2664
- * @param {string} configKey - The config key
2665
- * @param {string} [auditMessage] - Audit message for this write
2666
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2667
- */
2668
- deleteConfig: (textLayerId, namespace, configKey, auditMessage, options) =>
2669
- this._request(
2670
- "DELETE",
2671
- `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
2672
- configRequest(auditMessage, undefined, options, true),
2673
- ),
2674
- /**
2675
- * Get a text layer by ID.
2676
- * @param {string} textLayerId - The text layer ID
2677
- */
2678
- get: (textLayerId) =>
2679
- this._request("GET", `/api/v1/text-layers/${textLayerId}`),
2680
- /**
2681
- * Delete a text layer.
2682
- * @param {string} textLayerId - The text layer ID
2683
- */
2684
- delete: (textLayerId, auditMessage) =>
2685
- this._request("DELETE", `/api/v1/text-layers/${textLayerId}`, {
2686
- auditMessage,
2687
- }),
2688
- /**
2689
- * Update a text layer's name.
2690
- * @param {string} textLayerId - The text layer ID
2691
- * @param {string} name - The name
2692
- */
2693
- update: (textLayerId, name, auditMessage) =>
2694
- this._request("PATCH", `/api/v1/text-layers/${textLayerId}`, {
2695
- auditMessage,
2696
- body: bodyOf({ name }),
2697
- }),
2698
- /**
2699
- * Shift a text layer's display order within the project.
2700
- * @param {string} textLayerId - The text layer ID
2701
- * @param {string} direction - The direction ("up" or "down")
2702
- */
2703
- shift: (textLayerId, direction, auditMessage) =>
2704
- this._request("POST", `/api/v1/text-layers/${textLayerId}/shift`, {
2705
- auditMessage,
2706
- body: bodyOf({ direction }),
2707
- }),
2925
+ ...configMethods(this, "text-layers"),
2926
+ ...layerMethods(this, "text-layers"),
2708
2927
  /**
2709
2928
  * Create a new text layer for a project.
2710
2929
  * @param {string} projectId - The project ID
2711
2930
  * @param {string} name - The name
2931
+ * @param {string} [auditMessage] - Audit message for this write
2932
+ * @param {object} [opts]
2933
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2712
2934
  */
2713
2935
  create: (projectId, name, auditMessage, { id } = {}) =>
2714
2936
  this._request("POST", "/api/v1/text-layers", {
@@ -2718,39 +2940,15 @@ class PlaidClient {
2718
2940
  };
2719
2941
 
2720
2942
  this.vocabItems = {
2721
- /**
2722
- * Replace all metadata for a vocab item.
2723
- * @param {string} id - The resource ID
2724
- * @param {any} body - The request body
2725
- */
2726
- setMetadata: (id, body, auditMessage) =>
2727
- this._request("PUT", `/api/v1/vocab-items/${id}/metadata`, {
2728
- auditMessage,
2729
- rawBody: body,
2730
- }),
2731
- /**
2732
- * Remove all metadata from a vocab item.
2733
- * @param {string} id - The resource ID
2734
- */
2735
- deleteMetadata: (id, auditMessage) =>
2736
- this._request("DELETE", `/api/v1/vocab-items/${id}/metadata`, {
2737
- auditMessage,
2738
- }),
2739
- /**
2740
- * Edit metadata for a vocab item with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
2741
- * @param {string} id - The resource ID
2742
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
2743
- */
2744
- patchMetadata: (id, body, auditMessage) =>
2745
- this._request("PATCH", `/api/v1/vocab-items/${id}/metadata`, {
2746
- auditMessage,
2747
- rawBody: body,
2748
- }),
2943
+ ...metadataMethods(this, "vocab-items"),
2749
2944
  /**
2750
2945
  * Create a new vocab item
2751
2946
  * @param {string} vocabLayerId - The vocab layer ID
2752
2947
  * @param {string} form - The vocab item form
2753
2948
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
2949
+ * @param {string} [auditMessage] - Audit message for this write
2950
+ * @param {object} [opts]
2951
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2754
2952
  */
2755
2953
  create: (vocabLayerId, form, metadata, auditMessage, { id } = {}) =>
2756
2954
  this._request("POST", "/api/v1/vocab-items", {
@@ -2761,6 +2959,8 @@ class PlaidClient {
2761
2959
  * Create multiple vocab items in a single operation. Entries may target
2762
2960
  * different vocab layers; the user must have write access to each.
2763
2961
  * @param {Array<{vocabLayerId: string, form: string, metadata?: any}>} body - The vocab items to create
2962
+ * @param {string} [auditMessage] - Audit message for this write
2963
+ * @returns {Promise<{ids: string[]}>}
2764
2964
  */
2765
2965
  bulkCreate: (body, auditMessage) =>
2766
2966
  this._request("POST", "/api/v1/vocab-items/bulk", {
@@ -2771,6 +2971,7 @@ class PlaidClient {
2771
2971
  * Update many vocab items in a single operation: set forms and/or patch
2772
2972
  * metadata.
2773
2973
  * @param {Array<{id: string, form?: string, metadata?: Array<object>}>} body - `form` is set only when the key is present; `metadata` is a list of metadata ops, as for patchMetadata. The entries may lie in several vocab layers; the user must have write access to each. An unknown id refuses the whole update, and an id may appear only once. Only an entry whose form really changes restates the documents linking it, and a strict-mode client picks up their new versions from the response.
2974
+ * @param {string} [auditMessage] - Audit message for this write
2774
2975
  * @returns {Promise<{count: number}>} How many vocab items were updated.
2775
2976
  */
2776
2977
  bulkUpdate: (body, auditMessage) =>
@@ -2785,6 +2986,8 @@ class PlaidClient {
2785
2986
  * Every document holding one of those links has its version bumped, and
2786
2987
  * a strict-mode client picks up their new versions from the response.
2787
2988
  * @param {string[]} body - The vocab item IDs to delete
2989
+ * @param {string} [auditMessage] - Audit message for this write
2990
+ * @returns {Promise<void>}
2788
2991
  */
2789
2992
  bulkDelete: (body, auditMessage) =>
2790
2993
  this._request("DELETE", "/api/v1/vocab-items/bulk", {
@@ -2827,6 +3030,7 @@ class PlaidClient {
2827
3030
  * Needs maintainer rights on the vocabulary.
2828
3031
  * @param {string} survivorId - The entry that stays
2829
3032
  * @param {string[]} loserIds - The entries merged into it
3033
+ * @param {string} [auditMessage] - Audit message for this write
2830
3034
  * @returns {Promise<{moved: number, duplicates: number, removed: string[]}>}
2831
3035
  */
2832
3036
  merge: (survivorId, loserIds, auditMessage) =>
@@ -2841,6 +3045,7 @@ class PlaidClient {
2841
3045
  * from the response.
2842
3046
  * @param {string} id - The resource ID
2843
3047
  * @param {string} form - The vocab item form
3048
+ * @param {string} [auditMessage] - Audit message for this write
2844
3049
  */
2845
3050
  update: (id, form, auditMessage) =>
2846
3051
  this._request("PATCH", `/api/v1/vocab-items/${id}`, {
@@ -2850,82 +3055,22 @@ class PlaidClient {
2850
3055
  };
2851
3056
 
2852
3057
  this.relationLayers = {
2853
- /**
2854
- * Shift a relation layer's display order.
2855
- * @param {string} relationLayerId - The relation layer ID
2856
- * @param {string} direction - The direction ("up" or "down")
2857
- */
2858
- shift: (relationLayerId, direction, auditMessage) =>
2859
- this._request(
2860
- "POST",
2861
- `/api/v1/relation-layers/${relationLayerId}/shift`,
2862
- { auditMessage, body: bodyOf({ direction }) },
2863
- ),
3058
+ ...layerMethods(this, "relation-layers"),
2864
3059
  /**
2865
3060
  * Create a new relation layer.
2866
3061
  * @param {string} spanLayerId - The span layer ID
2867
3062
  * @param {string} name - The name
3063
+ * @param {string} [auditMessage] - Audit message for this write
3064
+ * @param {object} [opts]
3065
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2868
3066
  */
2869
3067
  create: (spanLayerId, name, auditMessage, { id } = {}) =>
2870
3068
  this._request("POST", "/api/v1/relation-layers", {
2871
3069
  auditMessage,
2872
3070
  body: bodyOf({ id, "span-layer-id": spanLayerId, name }),
2873
3071
  }),
2874
- /**
2875
- * Set a configuration value for a layer in an editor namespace.
2876
- * @param {string} relationLayerId - The relation layer ID
2877
- * @param {string} namespace - The config namespace
2878
- * @param {string} configKey - The config key
2879
- * @param {any} configValue - Configuration value to set
2880
- * @param {string} [auditMessage] - Audit message for this write
2881
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
2882
- * was absent). The write is then refused with a 409 when the stored value differs.
2883
- */
2884
- setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage, options) =>
2885
- this._request(
2886
- "PUT",
2887
- `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
2888
- configRequest(auditMessage, configValue, options),
2889
- ),
2890
- /**
2891
- * Remove a configuration value for a layer.
2892
- * @param {string} relationLayerId - The relation layer ID
2893
- * @param {string} namespace - The config namespace
2894
- * @param {string} configKey - The config key
2895
- * @param {string} [auditMessage] - Audit message for this write
2896
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2897
- */
2898
- deleteConfig: (relationLayerId, namespace, configKey, auditMessage, options) =>
2899
- this._request(
2900
- "DELETE",
2901
- `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
2902
- configRequest(auditMessage, undefined, options, true),
2903
- ),
3072
+ ...configMethods(this, "relation-layers"),
2904
3073
  ...constraintMethods(this, "relation-layers"),
2905
- /**
2906
- * Get a relation layer by ID.
2907
- * @param {string} relationLayerId - The relation layer ID
2908
- */
2909
- get: (relationLayerId) =>
2910
- this._request("GET", `/api/v1/relation-layers/${relationLayerId}`),
2911
- /**
2912
- * Delete a relation layer.
2913
- * @param {string} relationLayerId - The relation layer ID
2914
- */
2915
- delete: (relationLayerId, auditMessage) =>
2916
- this._request("DELETE", `/api/v1/relation-layers/${relationLayerId}`, {
2917
- auditMessage,
2918
- }),
2919
- /**
2920
- * Update a relation layer's name.
2921
- * @param {string} relationLayerId - The relation layer ID
2922
- * @param {string} name - The name
2923
- */
2924
- update: (relationLayerId, name, auditMessage) =>
2925
- this._request("PATCH", `/api/v1/relation-layers/${relationLayerId}`, {
2926
- auditMessage,
2927
- body: bodyOf({ name }),
2928
- }),
2929
3074
  };
2930
3075
 
2931
3076
  this.tokens = {
@@ -2946,6 +3091,9 @@ class PlaidClient {
2946
3091
  * @param {number} end - End offset, exclusive (Unicode code points)
2947
3092
  * @param {number} [precedence] - Ordering precedence
2948
3093
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
3094
+ * @param {string} [auditMessage] - Audit message for this write
3095
+ * @param {object} [opts]
3096
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2949
3097
  */
2950
3098
  create: (
2951
3099
  tokenLayerId,
@@ -2979,6 +3127,7 @@ class PlaidClient {
2979
3127
  * Delete a token and remove it from any spans. If this causes a span to
2980
3128
  * have no remaining tokens, the span will also be deleted.
2981
3129
  * @param {string} tokenId - The token ID
3130
+ * @param {string} [auditMessage] - Audit message for this write
2982
3131
  */
2983
3132
  delete: (tokenId, auditMessage) =>
2984
3133
  this._request("DELETE", `/api/v1/tokens/${tokenId}`, { auditMessage }),
@@ -2991,6 +3140,7 @@ class PlaidClient {
2991
3140
  * to leave unchanged; pass a number to set; pass null explicitly to
2992
3141
  * CLEAR it (revert to no explicit ordering). bodyOf keeps null but
2993
3142
  * drops undefined, so the three cases map correctly to the server.
3143
+ * @param {string} [auditMessage] - Audit message for this write
2994
3144
  */
2995
3145
  update: (tokenId, begin, end, precedence, auditMessage) =>
2996
3146
  this._request("PATCH", `/api/v1/tokens/${tokenId}`, {
@@ -3000,6 +3150,7 @@ class PlaidClient {
3000
3150
  /**
3001
3151
  * Create multiple tokens in a single operation.
3002
3152
  * @param {Array} body - The request body
3153
+ * @param {string} [auditMessage] - Audit message for this write
3003
3154
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
3004
3155
  */
3005
3156
  bulkCreate: (body, auditMessage) =>
@@ -3007,12 +3158,15 @@ class PlaidClient {
3007
3158
  /**
3008
3159
  * Delete multiple tokens in a single operation. Provide an array of IDs.
3009
3160
  * @param {Array} body - The request body
3161
+ * @param {string} [auditMessage] - Audit message for this write
3162
+ * @returns {Promise<void>}
3010
3163
  */
3011
3164
  bulkDelete: (body, auditMessage) =>
3012
3165
  this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
3013
3166
  /**
3014
3167
  * Update many tokens in a single operation: patch metadata.
3015
3168
  * @param {Array} body - Objects of the shape `{id, metadata}`. `metadata` is a list of metadata ops, as for patchMetadata. The tokens may lie in several documents of one project; every document touched has its version bumped and every new version comes back in `X-Document-Versions` (past fifty documents, only their number, in `X-Document-Versions-Omitted`). A `document-version` precondition is accepted only when every entry lies in one document. An unknown id refuses the whole update.
3169
+ * @param {string} [auditMessage] - Audit message for this write
3016
3170
  * @returns {Promise<{count: number}>} How many tokens were updated.
3017
3171
  */
3018
3172
  bulkUpdate: (body, auditMessage) =>
@@ -3051,6 +3205,7 @@ class PlaidClient {
3051
3205
  * merged extent must not engulf a third token.
3052
3206
  * @param {string} tokenId - The anchor token ID
3053
3207
  * @param {string} otherTokenId - The other token to merge in
3208
+ * @param {string} [auditMessage] - Audit message for this write
3054
3209
  */
3055
3210
  merge: (tokenId, otherTokenId, auditMessage) =>
3056
3211
  this._request("POST", `/api/v1/tokens/${tokenId}/merge`, {
@@ -3064,49 +3219,23 @@ class PlaidClient {
3064
3219
  * @param {string} tokenId - The token ID
3065
3220
  * @param {number} [begin] - New start offset, inclusive (Unicode code points)
3066
3221
  * @param {number} [end] - New end offset, exclusive (Unicode code points)
3222
+ * @param {string} [auditMessage] - Audit message for this write
3067
3223
  */
3068
3224
  shift: (tokenId, begin, end, auditMessage) =>
3069
3225
  this._request("POST", `/api/v1/tokens/${tokenId}/shift`, {
3070
3226
  auditMessage,
3071
3227
  body: bodyOf({ begin, end }),
3072
3228
  }),
3073
- /**
3074
- * Replace all metadata for a token.
3075
- * @param {string} tokenId - The token ID
3076
- * @param {any} body - The request body
3077
- */
3078
- setMetadata: (tokenId, body, auditMessage) =>
3079
- this._request("PUT", `/api/v1/tokens/${tokenId}/metadata`, {
3080
- auditMessage,
3081
- rawBody: body,
3082
- }),
3083
- /**
3084
- * Remove all metadata from a token.
3085
- * @param {string} tokenId - The token ID
3086
- */
3087
- deleteMetadata: (tokenId, auditMessage) =>
3088
- this._request("DELETE", `/api/v1/tokens/${tokenId}/metadata`, {
3089
- auditMessage,
3090
- }),
3091
- /**
3092
- * Edit metadata for a token with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
3093
- * @param {string} tokenId - The token ID
3094
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
3095
- */
3096
- patchMetadata: (tokenId, body, auditMessage) =>
3097
- this._request("PATCH", `/api/v1/tokens/${tokenId}/metadata`, {
3098
- auditMessage,
3099
- rawBody: body,
3100
- }),
3229
+ ...metadataMethods(this, "tokens"),
3101
3230
  };
3102
3231
 
3103
3232
  this.messages = {
3104
3233
  /**
3105
3234
  * Open a Server-Sent Events stream for a project.
3106
3235
  * @param {string} projectId - The UUID of the project to listen to
3107
- * @param {function} onEvent - Callback function that receives (eventType, data). If it returns true, listening will stop.
3236
+ * @param {(eventType: string, data: any) => void | boolean} onEvent - Callback function that receives (eventType, data). If it returns true, listening will stop.
3108
3237
  * @param {string} [path] - Stream path under baseUrl (defaults to the project /listen bus; service channels pass their own).
3109
- * @returns {Object} SSE connection object with .close() and .getStats() methods
3238
+ * @returns {SSEConnection} SSE connection object with .close() and .getStats() methods
3110
3239
  */
3111
3240
  listen: (projectId, onEvent, path) =>
3112
3241
  createSSEConnection(this, projectId, onEvent, path),
@@ -3139,7 +3268,7 @@ class PlaidClient {
3139
3268
  * connected services carry `online: true`; previously-seen offline ones
3140
3269
  * carry `online: false` plus a `lastSeenAt` stamp.
3141
3270
  * @param {string} projectId - The UUID of the project to query
3142
- * @returns {Promise<Array>} Array of discovered service information
3271
+ * @returns {Promise<DiscoveredService[]>} Array of discovered service information
3143
3272
  */
3144
3273
  discoverServices: (projectId) => discoverServices(this, projectId),
3145
3274
 
@@ -3159,14 +3288,14 @@ class PlaidClient {
3159
3288
  * The registration reopens its channel whenever it drops, so a server
3160
3289
  * restart needs no service restart.
3161
3290
  * @param {string} projectId - The UUID of the project to serve
3162
- * @param {Object} serviceInfo - Service information {serviceId, serviceName, description}
3163
- * @param {function} onServiceRequest - Callback (data, responseHelper)
3291
+ * @param {ServiceInfo} serviceInfo - Service information {serviceId, serviceName, description}
3292
+ * @param {(data: any, responseHelper: ResponseHelper) => void} onServiceRequest - Callback (data, responseHelper)
3164
3293
  * @param {Object} [extras] - Optional additional service metadata
3165
- * @param {function} [onStatus] - Optional callback (event, projectId, detail) for
3294
+ * @param {(event: ServiceStatusEvent, projectId: string, detail?: string) => void} [onStatus] - Optional callback (event, projectId, detail) for
3166
3295
  * connection transitions: 'registered', 'reconnected', 'disconnected',
3167
3296
  * and 'stopped' when the server refuses the channel for good, which
3168
3297
  * ends the registration
3169
- * @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
3298
+ * @returns {ServiceRegistration} Service registration with .stop(), .isRunning(), .isConnected()
3170
3299
  */
3171
3300
  serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
3172
3301
  serve(this, projectId, serviceInfo, onServiceRequest, extras, onStatus),
@@ -3187,7 +3316,7 @@ class PlaidClient {
3187
3316
  * @param {number} [timeout=10000] - Timeout in ms
3188
3317
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
3189
3318
  * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
3190
- * @param {Object} [opts] - {requestId, onAccepted, projectIds, noOperation}
3319
+ * @param {{requestId?: string, onAccepted?: (requestId: string) => void, projectIds?: string[], noOperation?: boolean}} [opts] - {requestId, onAccepted, projectIds, noOperation}
3191
3320
  * @returns {Promise<any>} The service's result
3192
3321
  */
3193
3322
  requestService: (
@@ -3332,11 +3461,13 @@ class PlaidClient {
3332
3461
  * A comment outlives its anchor: deleting the entity does not delete
3333
3462
  * the comment. `anchorLabel` is the caption shown once that happens
3334
3463
  * ("Gloss of ktab, sentence 4"), so pass what the comment is about.
3335
- * @param {'document'|'text'|'token'|'span'|'relation'|'vocab-item'} entityType - What kind of thing is being commented on
3464
+ * @param {CommentableType} entityType - What kind of thing is being commented on
3336
3465
  * @param {string} entityId - The commented entity's id
3337
3466
  * @param {string} body - The comment text (1..10000 characters)
3338
3467
  * @param {object} [opts]
3339
3468
  * @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
3469
+ * @param {string} [opts.id] - The new comment's id, minted by the caller (see uuidv7)
3470
+ * @returns {Promise<Comment>}
3340
3471
  */
3341
3472
  create: (entityType, entityId, body, { anchorLabel, id } = {}) =>
3342
3473
  this._request("POST", "/api/v1/comments", {
@@ -3354,6 +3485,7 @@ class PlaidClient {
3354
3485
  /**
3355
3486
  * Read one comment.
3356
3487
  * @param {string} id - The comment id
3488
+ * @returns {Promise<Comment>}
3357
3489
  */
3358
3490
  get: (id) => this._request("GET", `/api/v1/comments/${id}`),
3359
3491
  /**
@@ -3361,6 +3493,7 @@ class PlaidClient {
3361
3493
  * maintainers, not admins. Sets `edited` on the comment.
3362
3494
  * @param {string} id - The comment id
3363
3495
  * @param {string} body - The replacement text
3496
+ * @returns {Promise<Comment>}
3364
3497
  */
3365
3498
  update: (id, body) =>
3366
3499
  this._request("PATCH", `/api/v1/comments/${id}`, {
@@ -3382,6 +3515,7 @@ class PlaidClient {
3382
3515
  * @param {string} [filters.documentId] - Every comment anywhere in one document
3383
3516
  * @param {string} [filters.entityType] - With entityId, one entity's thread
3384
3517
  * @param {string} [filters.entityId] - With entityType, one entity's thread
3518
+ * @returns {Promise<Comment[]>}
3385
3519
  */
3386
3520
  list: (projectId, { documentId, entityType, entityId } = {}) =>
3387
3521
  listAll(this, `/api/v1/projects/${projectId}/comments`, {
@@ -3400,7 +3534,7 @@ class PlaidClient {
3400
3534
  * @param {string} [opts.documentId] - Every comment anywhere in one document
3401
3535
  * @param {string} [opts.entityType] - With entityId, one entity's thread
3402
3536
  * @param {string} [opts.entityId] - With entityType, one entity's thread
3403
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3537
+ * @returns {Promise<Page<Comment>>}
3404
3538
  */
3405
3539
  listPage: (
3406
3540
  projectId,
@@ -3423,7 +3557,7 @@ class PlaidClient {
3423
3557
  * @param {string} [opts.documentId] - Every comment anywhere in one document
3424
3558
  * @param {string} [opts.entityType] - With entityId, one entity's thread
3425
3559
  * @param {string} [opts.entityId] - With entityType, one entity's thread
3426
- * @returns {AsyncGenerator<Array>}
3560
+ * @returns {AsyncGenerator<Comment[]>}
3427
3561
  */
3428
3562
  iterPages: (
3429
3563
  projectId,
@@ -3446,7 +3580,7 @@ class PlaidClient {
3446
3580
  * The response is NOT key-transformed: its keys are entity ids, and
3447
3581
  * camelCasing would mangle the hyphens in a UUID.
3448
3582
  * @param {string} projectId - The project to read
3449
- * @param {object} [filters] - Same filters as list()
3583
+ * @param {{documentId?: string, entityType?: CommentableType, entityId?: string}} [filters] - Same filters as list()
3450
3584
  * @returns {Promise<Object<string, number>>}
3451
3585
  */
3452
3586
  counts: (projectId, { documentId, entityType, entityId } = {}) =>
@@ -3465,6 +3599,7 @@ class PlaidClient {
3465
3599
  * @param {string} vocabId - The vocab layer to read
3466
3600
  * @param {object} [filters]
3467
3601
  * @param {string} [filters.entityId] - One entry's thread
3602
+ * @returns {Promise<Comment[]>}
3468
3603
  */
3469
3604
  listInVocab: (vocabId, { entityId } = {}) =>
3470
3605
  listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3477,7 +3612,7 @@ class PlaidClient {
3477
3612
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
3478
3613
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
3479
3614
  * @param {string} [opts.entityId] - One entry's thread
3480
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3615
+ * @returns {Promise<Page<Comment>>}
3481
3616
  */
3482
3617
  listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
3483
3618
  listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3492,7 +3627,7 @@ class PlaidClient {
3492
3627
  * @param {object} [opts]
3493
3628
  * @param {string} [opts.entityId] - One entry's thread
3494
3629
  * @param {number} [opts.pageSize] - Per-request page size
3495
- * @returns {AsyncGenerator<Array>}
3630
+ * @returns {AsyncGenerator<Comment[]>}
3496
3631
  */
3497
3632
  iterInVocabPages: (vocabId, { entityId, pageSize } = {}) =>
3498
3633
  iterPages(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3520,15 +3655,15 @@ class PlaidClient {
3520
3655
 
3521
3656
  this.events = {
3522
3657
  /**
3523
- * Record one research-telemetry event (core manual, "Research
3524
- * telemetry"). Fire and forget: the event is buffered and sent with
3658
+ * Record one research-telemetry event (plaid-core sql/client_event.clj).
3659
+ * Fire and forget: the event is buffered and sent with
3525
3660
  * others every ten seconds, at fifty events, and when the page is
3526
3661
  * hidden. A batch that fails is dropped, nothing is retried, and
3527
3662
  * nothing is ever thrown. Sends nothing for a project whose switch
3528
3663
  * (`config.plaid.research.telemetry`) is off, which it reads from the
3529
3664
  * project itself. `suggestion.shown` is recorded once per target,
3530
3665
  * field and value in a page session.
3531
- * @param {'suggestion.shown'|'suggestion.adopted'|'suggestion.dismissed'|'plan.opened'} type - The event type
3666
+ * @param {ClientEventType} type - The event type
3532
3667
  * @param {object} fields
3533
3668
  * @param {string} fields.projectId - The project it happened in
3534
3669
  * @param {string} [fields.documentId] - The document it happened in
@@ -3539,6 +3674,7 @@ class PlaidClient {
3539
3674
  record: (type, fields) => recorderFor(this).record(this, type, fields),
3540
3675
  /**
3541
3676
  * Send what the recorder holds now, rather than at its next interval.
3677
+ * @returns {void}
3542
3678
  */
3543
3679
  flush: () => recorderFor(this).flush(),
3544
3680
  /**
@@ -3546,6 +3682,7 @@ class PlaidClient {
3546
3682
  * maintainer changed it. Turning it off drops what is buffered.
3547
3683
  * @param {string} projectId - The project
3548
3684
  * @param {boolean} on - Whether telemetry is on
3685
+ * @returns {void}
3549
3686
  */
3550
3687
  setEnabled: (projectId, on) => recorderFor(this).setEnabled(projectId, on),
3551
3688
  /**
@@ -3570,6 +3707,7 @@ class PlaidClient {
3570
3707
  * @param {string|string[]} [filters.types] - Only these types
3571
3708
  * @param {string} [filters.startTime] - Only events the server stamped at or after this instant
3572
3709
  * @param {string} [filters.endTime] - Only events the server stamped at or before this instant
3710
+ * @returns {Promise<ClientEvent[]>}
3573
3711
  */
3574
3712
  list: (projectId, { types, startTime, endTime } = {}) =>
3575
3713
  listAll(this, `/api/v1/projects/${projectId}/events`, {
@@ -3584,7 +3722,7 @@ class PlaidClient {
3584
3722
  * @param {string|string[]} [opts.types] - Only these types
3585
3723
  * @param {string} [opts.startTime] - Only events at or after this instant
3586
3724
  * @param {string} [opts.endTime] - Only events at or before this instant
3587
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3725
+ * @returns {Promise<Page<ClientEvent>>}
3588
3726
  */
3589
3727
  listPage: (
3590
3728
  projectId,
@@ -3603,7 +3741,7 @@ class PlaidClient {
3603
3741
  * @param {string|string[]} [opts.types] - Only these types
3604
3742
  * @param {string} [opts.startTime] - Only events at or after this instant
3605
3743
  * @param {string} [opts.endTime] - Only events at or before this instant
3606
- * @returns {AsyncGenerator<Array>}
3744
+ * @returns {AsyncGenerator<ClientEvent[]>}
3607
3745
  */
3608
3746
  iterPages: (projectId, { pageSize, types, startTime, endTime } = {}) =>
3609
3747
  iterPages(this, `/api/v1/projects/${projectId}/events`, {
@@ -3624,6 +3762,9 @@ class PlaidClient {
3624
3762
  * @param {object} [opts]
3625
3763
  * @param {string} [opts.body] - The Markdown text (up to 20000 characters; may be empty)
3626
3764
  * @param {boolean} [opts.pinned] - Send this one to the assistant in full on every turn
3765
+ * @param {string} [opts.id] - The new guideline's id, minted by the caller (see uuidv7)
3766
+ * @param {string} [auditMessage] - Audit message for this write
3767
+ * @returns {Promise<{id: string}>}
3627
3768
  */
3628
3769
  create: (projectId, title, { body, pinned, id } = {}, auditMessage) =>
3629
3770
  this._request("POST", `/api/v1/projects/${projectId}/guidelines`, {
@@ -3633,6 +3774,7 @@ class PlaidClient {
3633
3774
  /**
3634
3775
  * Read one guideline, Markdown body included.
3635
3776
  * @param {string} id - The guideline id
3777
+ * @returns {Promise<Guideline>}
3636
3778
  */
3637
3779
  get: (id) => this._request("GET", `/api/v1/guidelines/${id}`),
3638
3780
  /**
@@ -3649,6 +3791,8 @@ class PlaidClient {
3649
3791
  * @param {string} [changes.body] - The new Markdown text
3650
3792
  * @param {boolean} [changes.pinned] - Whether the assistant always gets it in full
3651
3793
  * @param {string} [changes.expectedUpdatedAt] - Write only if this is still the stored updatedAt
3794
+ * @param {string} [auditMessage] - Audit message for this write
3795
+ * @returns {Promise<Guideline>}
3652
3796
  */
3653
3797
  update: (
3654
3798
  id,
@@ -3663,6 +3807,7 @@ class PlaidClient {
3663
3807
  /**
3664
3808
  * Delete a guideline.
3665
3809
  * @param {string} id - The guideline id
3810
+ * @param {string} [auditMessage] - Audit message for this write
3666
3811
  */
3667
3812
  delete: (id, auditMessage) =>
3668
3813
  this._request("DELETE", `/api/v1/guidelines/${id}`, { auditMessage }),
@@ -3677,6 +3822,7 @@ class PlaidClient {
3677
3822
  * @param {string} projectId - The project to read
3678
3823
  * @param {object} [opts]
3679
3824
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3825
+ * @returns {Promise<Guideline[]>}
3680
3826
  */
3681
3827
  list: (projectId, { includeBodies } = {}) =>
3682
3828
  listAll(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3689,7 +3835,7 @@ class PlaidClient {
3689
3835
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
3690
3836
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
3691
3837
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3692
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3838
+ * @returns {Promise<Page<Guideline>>}
3693
3839
  */
3694
3840
  listPage: (projectId, { limit, cursor, includeBodies } = {}) =>
3695
3841
  listPage(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3703,7 +3849,7 @@ class PlaidClient {
3703
3849
  * @param {object} [opts]
3704
3850
  * @param {number} [opts.pageSize] - Per-request page size
3705
3851
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3706
- * @returns {AsyncGenerator<Array>}
3852
+ * @returns {AsyncGenerator<Guideline[]>}
3707
3853
  */
3708
3854
  iterPages: (projectId, { pageSize, includeBodies } = {}) =>
3709
3855
  iterPages(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3715,6 +3861,7 @@ class PlaidClient {
3715
3861
 
3716
3862
  // --- Core methods ---
3717
3863
 
3864
+ /** @private */
3718
3865
  async _request(method, path, options = {}) {
3719
3866
  return makeRequest(this, method, path, options);
3720
3867
  }
@@ -3777,7 +3924,7 @@ class PlaidClient {
3777
3924
  * matching end, and the outer numbering resumes after it.
3778
3925
  *
3779
3926
  * @param {string} message - Human label for the operation.
3780
- * @param {object} [opts] - Optional `{ groupId, kind, ref, keys, minted }`. `groupId` adopts an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically). Python's is `group_id`. `kind`, `ref` and `keys` are described above. `minted` is the ids the operation mints for what it creates: a create refused 409 id-taken for one of them was made by an earlier send of the operation, and answers as made.
3927
+ * @param {{groupId?: string, kind?: OperationKind, ref?: string, keys?: KeySeed, minted?: Iterable<string>}} [opts] - Optional `{ groupId, kind, ref, keys, minted }`. `groupId` adopts an existing group id instead of minting one (a service joining the requester's operation; `requestService` propagates an open operation to the service automatically). Python's is `group_id`. `kind`, `ref` and `keys` are described above. `minted` is the ids the operation mints for what it creates: a create refused 409 id-taken for one of them was made by an earlier send of the operation, and answers as made.
3781
3928
  * @returns {string} The operation's group id.
3782
3929
  */
3783
3930
  beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
@@ -3809,7 +3956,7 @@ class PlaidClient {
3809
3956
  return open.id;
3810
3957
  }
3811
3958
  this.operationGroup = {
3812
- id: groupId ? String(groupId) : crypto.randomUUID(),
3959
+ id: groupId ? String(groupId) : uuidv4(),
3813
3960
  message: message == null ? null : String(message),
3814
3961
  kind: kind == null ? null : String(kind),
3815
3962
  ref: ref == null ? null : String(ref),
@@ -3826,7 +3973,7 @@ class PlaidClient {
3826
3973
  * A seed for the Idempotency-Keys of a logical operation that may be run
3827
3974
  * again from the top (see beginOperation's `keys`). Keep it with the work
3828
3975
  * it belongs to and pass it to every run.
3829
- * @returns {{seed: string, stamps: Map<number, number|null>}}
3976
+ * @returns {KeySeed}
3830
3977
  */
3831
3978
  keySeed() {
3832
3979
  return { seed: uuidv7(), stamps: new Map() };
@@ -3881,10 +4028,11 @@ class PlaidClient {
3881
4028
  *
3882
4029
  * await client.withOperation('Import ELAN corpus', run, { kind: 'import', ref: 'format:elan' });
3883
4030
  *
4031
+ * @template T
3884
4032
  * @param {string} message - Human label for the operation.
3885
- * @param {function} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
3886
- * @param {object} [opts] - Optional `{ kind, ref, groupId, keys, minted }`, as for beginOperation. `kind` is one of `assistant-plan`, `service-run`, `import`, `bulk-edit`, `guess-adoption`, `repair` or `review`, and any other throws before `fn` runs.
3887
- * @returns {Promise<any>} Whatever `fn` resolves to.
4033
+ * @param {(setMessage: (msg: string) => void) => T | Promise<T>} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
4034
+ * @param {{kind?: OperationKind, ref?: string, groupId?: string, keys?: KeySeed, minted?: Iterable<string>}} [opts] - Optional `{ kind, ref, groupId, keys, minted }`, as for beginOperation. `kind` is one of `assistant-plan`, `service-run`, `import`, `bulk-edit`, `guess-adoption`, `repair` or `review`, and any other throws before `fn` runs.
4035
+ * @returns {Promise<T>} Whatever `fn` resolves to.
3888
4036
  */
3889
4037
  async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
3890
4038
  this.beginOperation(message, { kind, ref, groupId, keys, minted });
@@ -3939,7 +4087,7 @@ class PlaidClient {
3939
4087
  * parents created earlier in the same batch, and any op's failure rolls the
3940
4088
  * whole batch back. A batch is not nestable. Prefer `batched()`, which
3941
4089
  * submits or aborts for you.
3942
- * @returns {PlaidClient} the batch view
4090
+ * @returns {PlaidBatch} the batch view
3943
4091
  */
3944
4092
  batch() {
3945
4093
  return openBatch(this);
@@ -3951,6 +4099,7 @@ class PlaidClient {
3951
4099
  * recased: the headers keep the server's spelling. `key` is the request's
3952
4100
  * Idempotency-Key: a batch whose answer is lost (no response, 502, 504) is
3953
4101
  * sent again under it, and answered from the first send if that landed.
4102
+ * @private
3954
4103
  */
3955
4104
  async _postBatch(url, body, { key = uuidv7() } = {}) {
3956
4105
  const fetchOptions = {
@@ -4083,8 +4232,8 @@ class PlaidClient {
4083
4232
  *
4084
4233
  * A write made on `client` inside `fn` is not part of the batch: it goes
4085
4234
  * over the wire at once, as it would anywhere else. See `batch()`.
4086
- * @param {(batch: PlaidClient) => (void | Promise<void>)} fn
4087
- * @returns {Promise<Array>}
4235
+ * @param {(batch: PlaidBatch) => (void | Promise<void>)} fn
4236
+ * @returns {Promise<any[]>}
4088
4237
  */
4089
4238
  async batched(fn) {
4090
4239
  const batch = this.batch();
@@ -4102,20 +4251,22 @@ class PlaidClient {
4102
4251
  * instance — for a launcher or status page checking whether a server is up
4103
4252
  * before anyone logs in.
4104
4253
  * @param {string} baseUrl - The API base URL
4105
- * @param {object} [options]
4254
+ * @param {{timeout?: number | null}} [options]
4255
+ * @returns {Promise<ServerHealth>}
4106
4256
  */
4107
4257
  static async health(baseUrl, options = {}) {
4108
- return anonymousGet(baseUrl, "/health", options);
4258
+ return anonymousRequest("GET", baseUrl, "/health", undefined, options);
4109
4259
  }
4110
4260
 
4111
4261
  /**
4112
4262
  * The limits this server enforces, with NO authentication and no client
4113
4263
  * instance. See the instance method for the shape.
4114
4264
  * @param {string} baseUrl - The API base URL
4115
- * @param {object} [options]
4265
+ * @param {{timeout?: number | null}} [options]
4266
+ * @returns {Promise<{limits: ServerLimits}>}
4116
4267
  */
4117
4268
  static async info(baseUrl, options = {}) {
4118
- return anonymousGet(baseUrl, "/api/v1/info", options);
4269
+ return anonymousRequest("GET", baseUrl, "/api/v1/info", undefined, options);
4119
4270
  }
4120
4271
 
4121
4272
  /**
@@ -4146,7 +4297,7 @@ class PlaidClient {
4146
4297
  * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
4147
4298
  */
4148
4299
  static async lookupInvite(baseUrl, code, options = {}) {
4149
- return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
4300
+ return anonymousRequest("POST", baseUrl, "/api/v1/invites/lookup", { code }, options);
4150
4301
  }
4151
4302
 
4152
4303
  /**
@@ -4175,10 +4326,16 @@ class PlaidClient {
4175
4326
  { email, password, displayName } = {},
4176
4327
  options = {},
4177
4328
  ) {
4178
- const data = await anonymousPost(
4329
+ const data = await anonymousRequest(
4330
+ "POST",
4179
4331
  baseUrl,
4180
4332
  "/api/v1/invites/redeem",
4181
- bodyOf({ code, email, password, "display-name": displayName }),
4333
+ bodyOf({
4334
+ code,
4335
+ email: normalizeUserId(email),
4336
+ password,
4337
+ "display-name": displayName,
4338
+ }),
4182
4339
  options,
4183
4340
  );
4184
4341
  return {
@@ -4203,35 +4360,14 @@ class PlaidClient {
4203
4360
  */
4204
4361
  static async login(baseUrl, userId, password, options = {}) {
4205
4362
  baseUrl = baseUrl.replace(/\/$/, "");
4206
- const url = `${baseUrl}/api/v1/login`;
4207
- try {
4208
- const fetchOptions = {
4209
- method: "POST",
4210
- headers: { "Content-Type": "application/json" },
4211
- body: JSON.stringify({ "user-id": userId, password }),
4212
- };
4213
- const signal = timeoutSignal(
4214
- options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
4215
- );
4216
- if (signal) fetchOptions.signal = signal;
4217
-
4218
- const response = await fetch(url, fetchOptions);
4219
- if (!response.ok) {
4220
- throw makeHttpError(
4221
- response,
4222
- await parseErrorBody(response),
4223
- url,
4224
- "POST",
4225
- );
4226
- }
4227
-
4228
- const data = await response.json();
4229
- const token = data.token || "";
4230
- return new PlaidClient(baseUrl, token, options);
4231
- } catch (error) {
4232
- if (error.status) throw error;
4233
- throw makeNetworkError(error, url, "POST");
4234
- }
4363
+ const data = await anonymousRequest(
4364
+ "POST",
4365
+ baseUrl,
4366
+ "/api/v1/login",
4367
+ { "user-id": normalizeUserId(userId), password },
4368
+ options,
4369
+ );
4370
+ return new PlaidClient(baseUrl, data.token || "", options);
4235
4371
  }
4236
4372
  }
4237
4373
 
@@ -4259,9 +4395,129 @@ function configRequest(auditMessage, value, options, isDelete = false) {
4259
4395
  };
4260
4396
  }
4261
4397
 
4398
+ /**
4399
+ * The three metadata methods of a bundle whose entities carry metadata, on
4400
+ * `/api/v1/<kind>/<id>/metadata`. Metadata is opaque, so no body is re-cased.
4401
+ * @param {PlaidClient} client
4402
+ * @param {string} kind
4403
+ */
4404
+ function metadataMethods(client, kind) {
4405
+ const path = (id) => `/api/v1/${kind}/${id}/metadata`;
4406
+ return {
4407
+ /**
4408
+ * Replace all metadata of an entity. Keys left out of the new map are removed.
4409
+ * @param {string} id - The entity's ID
4410
+ * @param {object} body - The new metadata map
4411
+ * @param {string} [auditMessage] - Audit message for this write
4412
+ */
4413
+ setMetadata: (id, body, auditMessage) =>
4414
+ client._request("PUT", path(id), { auditMessage, rawBody: body }),
4415
+ /**
4416
+ * Remove all metadata from an entity.
4417
+ * @param {string} id - The entity's ID
4418
+ * @param {string} [auditMessage] - Audit message for this write
4419
+ */
4420
+ deleteMetadata: (id, auditMessage) => client._request("DELETE", path(id), { auditMessage }),
4421
+ /**
4422
+ * Edit an entity's metadata with a list of ops applied in order, in one operation. `{op: 'set', path, value}` writes value at path, creating missing objects along it; `{op: 'delete', path}` removes the key at path (a no-op when absent). A path is a non-empty array of keys, the first a top-level key. A path through a non-object is refused (400). See metadataOps and applyMetadataOps.
4423
+ * @param {string} id - The entity's ID
4424
+ * @param {MetadataOp[]} body - The metadata ops
4425
+ * @param {string} [auditMessage] - Audit message for this write
4426
+ */
4427
+ patchMetadata: (id, body, auditMessage) =>
4428
+ client._request("PATCH", path(id), { auditMessage, rawBody: body }),
4429
+ };
4430
+ }
4431
+
4432
+ /**
4433
+ * The two config methods of a bundle whose records carry editor config, on
4434
+ * `/api/v1/<kind>/<id>/config/<namespace>/<key>`.
4435
+ * @param {PlaidClient} client
4436
+ * @param {string} kind
4437
+ */
4438
+ function configMethods(client, kind) {
4439
+ const path = (id, namespace, configKey) => `/api/v1/${kind}/${id}/config/${namespace}/${configKey}`;
4440
+ return {
4441
+ /**
4442
+ * Set a configuration value in an editor namespace.
4443
+ * @param {string} id - The record's ID
4444
+ * @param {string} namespace - The config namespace
4445
+ * @param {string} configKey - The config key
4446
+ * @param {any} configValue - Configuration value to set
4447
+ * @param {string} [auditMessage] - Audit message for this write
4448
+ * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
4449
+ * was absent). The write is then refused with a 409 when the stored value differs.
4450
+ */
4451
+ setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
4452
+ client._request(
4453
+ "PUT",
4454
+ path(id, namespace, configKey),
4455
+ configRequest(auditMessage, configValue, options),
4456
+ ),
4457
+ /**
4458
+ * Remove a configuration value.
4459
+ * @param {string} id - The record's ID
4460
+ * @param {string} namespace - The config namespace
4461
+ * @param {string} configKey - The config key
4462
+ * @param {string} [auditMessage] - Audit message for this write
4463
+ * @param {{expected?: any}} [options] - `expected`, as on setConfig.
4464
+ */
4465
+ deleteConfig: (id, namespace, configKey, auditMessage, options) =>
4466
+ client._request(
4467
+ "DELETE",
4468
+ path(id, namespace, configKey),
4469
+ configRequest(auditMessage, undefined, options, true),
4470
+ ),
4471
+ };
4472
+ }
4473
+
4474
+ /**
4475
+ * The get, delete, update and shift methods of the four layer bundles, on
4476
+ * `/api/v1/<kind>/<layerId>`.
4477
+ * @param {PlaidClient} client
4478
+ * @param {string} kind
4479
+ */
4480
+ function layerMethods(client, kind) {
4481
+ const path = (layerId) => `/api/v1/${kind}/${layerId}`;
4482
+ return {
4483
+ /**
4484
+ * Get a layer by ID.
4485
+ * @param {string} layerId - The layer ID
4486
+ */
4487
+ get: (layerId) => client._request("GET", path(layerId)),
4488
+ /**
4489
+ * Delete a layer.
4490
+ * @param {string} layerId - The layer ID
4491
+ * @param {string} [auditMessage] - Audit message for this write
4492
+ */
4493
+ delete: (layerId, auditMessage) => client._request("DELETE", path(layerId), { auditMessage }),
4494
+ /**
4495
+ * Rename a layer.
4496
+ * @param {string} layerId - The layer ID
4497
+ * @param {string} name - The new name
4498
+ * @param {string} [auditMessage] - Audit message for this write
4499
+ */
4500
+ update: (layerId, name, auditMessage) =>
4501
+ client._request("PATCH", path(layerId), { auditMessage, body: bodyOf({ name }) }),
4502
+ /**
4503
+ * Move a layer one place in its display order.
4504
+ * @param {string} layerId - The layer ID
4505
+ * @param {string} direction - The direction ("up" or "down")
4506
+ * @param {string} [auditMessage] - Audit message for this write
4507
+ */
4508
+ shift: (layerId, direction, auditMessage) =>
4509
+ client._request("POST", `${path(layerId)}/shift`, {
4510
+ auditMessage,
4511
+ body: bodyOf({ direction }),
4512
+ }),
4513
+ };
4514
+ }
4515
+
4262
4516
  /**
4263
4517
  * The four layer-constraint methods of a layer bundle, on `/api/v1/<kind>`.
4264
4518
  * See constraints.js and the core manual, "Layer constraints".
4519
+ * @param {PlaidClient} client
4520
+ * @param {string} kind
4265
4521
  */
4266
4522
  function constraintMethods(client, kind) {
4267
4523
  const base = (layerId) => `/api/v1/${kind}/${layerId}/constraints`;
@@ -4272,12 +4528,12 @@ function constraintMethods(client, kind) {
4272
4528
  * stored data breaks them (see violationsOf).
4273
4529
  * @param {string} layerId - The layer ID
4274
4530
  * @param {string} namespace - The declaring app's namespace, e.g. "igt"
4275
- * @param {Array<object>} constraints - e.g. [{type: "single-span"}, {type: "value-set", values: ["N"]}]
4531
+ * @param {LayerConstraint[]} constraints - e.g. [{type: "single-span"}, {type: "value-set", values: ["N"]}]
4276
4532
  * @param {string} [auditMessage] - Audit message for this write
4277
4533
  * @param {{expected?: Array<object>|null}} [options] - `expected`: the list read for this
4278
4534
  * namespace (null when it was absent). The write is then refused with a 409 when the stored
4279
4535
  * list differs.
4280
- * @returns {Promise<{constraints: object}>} the layer's whole constraint map
4536
+ * @returns {Promise<{constraints: Record<string, LayerConstraint[]>}>} the layer's whole constraint map
4281
4537
  */
4282
4538
  setConstraints: (layerId, namespace, constraints, auditMessage, options) =>
4283
4539
  client._request("PUT", `${base(layerId)}/${namespace}`, {
@@ -4302,8 +4558,8 @@ function constraintMethods(client, kind) {
4302
4558
  * The violations the given constraints would meet in this layer's stored
4303
4559
  * data. Writes nothing.
4304
4560
  * @param {string} layerId - The layer ID
4305
- * @param {Array<object>} constraints - The list to check
4306
- * @returns {Promise<{violations: Array<object>, violationCount: number}>}
4561
+ * @param {LayerConstraint[]} constraints - The list to check
4562
+ * @returns {Promise<{violations: ConstraintViolation[], violationCount: number}>}
4307
4563
  */
4308
4564
  checkConstraints: (layerId, constraints) =>
4309
4565
  client._request("POST", `${base(layerId)}/check`, { body: { constraints } }),
@@ -4315,10 +4571,10 @@ function constraintMethods(client, kind) {
4315
4571
  * ask. A document another user holds the lock on is left as it is and
4316
4572
  * listed under `locked`.
4317
4573
  * @param {string} layerId - The layer ID
4318
- * @param {Array<object>} constraints - The list to repair for
4574
+ * @param {LayerConstraint[]} constraints - The list to repair for
4319
4575
  * @param {string} [auditMessage] - Audit message for this write
4320
4576
  * @param {{document?: string}} [options]
4321
- * @returns {Promise<{repaired: Array<object>, locked: Array<{document: string, lockedBy: string}>, violations: Array<object>, violationCount: number}>}
4577
+ * @returns {Promise<{repaired: ConstraintRepair[], locked: Array<{document: string, lockedBy: string}>, violations: ConstraintViolation[], violationCount: number}>}
4322
4578
  */
4323
4579
  repairConstraints: (layerId, constraints, auditMessage, options) =>
4324
4580
  client._request("POST", `${base(layerId)}/repair`, {
@@ -4340,6 +4596,7 @@ export {
4340
4596
  cpSlicer,
4341
4597
  utf16ToCp,
4342
4598
  cpIndexOf,
4599
+ composeText,
4343
4600
  } from "./codepoint.js";
4344
4601
  export { composeTextEdits, gapsToOps, applyTextOps } from "./textEdits.js";
4345
4602
  export {
@@ -4412,7 +4669,8 @@ export {
4412
4669
  // result. See ./created.js.
4413
4670
  export { createdId, createdIds } from "./created.js";
4414
4671
  // UUIDv7 ids for what a client creates. See ./ids.js and the manual,
4415
- // "Retrying a write".
4416
- export { uuidv7 } from "./ids.js";
4672
+ // "Retrying a write". uuidv4 works where crypto.randomUUID does not (a page
4673
+ // served over plain HTTP from any host but localhost).
4674
+ export { uuidv4, uuidv7, normalizeUserId } from "./ids.js";
4417
4675
  export { wasReplayed } from "./replayed.js";
4418
4676
  export { MAX_BATCH_OPS };