@larc-iu/plaid-client 0.2.0-alpha.23 → 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 { uuidv4, 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,48 +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 = {}) {
77
- const base = baseUrl.replace(/\/$/, "");
78
- const url = `${base}${path}`;
79
- try {
80
- const fetchOptions = {
81
- method: "POST",
82
- headers: { "Content-Type": "application/json" },
83
- body: JSON.stringify(body),
84
- };
85
- const signal = timeoutSignal(
86
- options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
87
- );
88
- if (signal) fetchOptions.signal = signal;
89
-
90
- const response = await fetch(url, fetchOptions);
91
- if (!response.ok) {
92
- throw makeHttpError(
93
- response,
94
- await parseErrorBody(response),
95
- url,
96
- "POST",
97
- );
98
- }
99
- return transformResponse(await response.json());
100
- } catch (error) {
101
- 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 = {}) {
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 = {}) {
111
366
  const base = baseUrl.replace(/\/$/, "");
112
367
  const url = `${base}${path}`;
113
368
  try {
114
- const fetchOptions = { method: "GET" };
369
+ const fetchOptions =
370
+ method === "GET"
371
+ ? { method }
372
+ : {
373
+ method,
374
+ headers: { "Content-Type": "application/json" },
375
+ body: JSON.stringify(body),
376
+ };
115
377
  const signal = timeoutSignal(
116
378
  options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
117
379
  );
@@ -119,12 +381,12 @@ async function anonymousGet(baseUrl, path, options = {}) {
119
381
 
120
382
  const response = await fetch(url, fetchOptions);
121
383
  if (!response.ok) {
122
- throw makeHttpError(response, await parseErrorBody(response), url, "GET");
384
+ throw makeHttpError(response, await parseErrorBody(response), url, method);
123
385
  }
124
386
  return transformResponse(await response.json());
125
387
  } catch (error) {
126
388
  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,
@@ -1477,6 +1770,7 @@ class PlaidClient {
1477
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,8 +1778,11 @@ 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
@@ -1506,16 +1803,26 @@ class PlaidClient {
1506
1803
  * @param {number} [opts.version] - The version this write was made from
1507
1804
  * @returns {Promise<{key: string, updatedAt: string, version: number}>}
1508
1805
  */
1509
- put: (userId, key, value, { version } = {}) =>
1510
- this._request(
1511
- "PUT",
1512
- `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1513
- {
1514
- body: value,
1515
- noBatch: true,
1516
- queryParams: { version },
1517
- },
1518
- ),
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
+ },
1519
1826
  /**
1520
1827
  * Delete one private data entry; 404 if absent.
1521
1828
  * @param {string} userId
@@ -1567,6 +1874,7 @@ class PlaidClient {
1567
1874
  * needs a sign-in token.
1568
1875
  * @param {string} userId - The user ID who will own the token
1569
1876
  * @param {string} name - A human label, e.g. "Stanza parser"
1877
+ * @param {string} [auditMessage] - Audit message for this write
1570
1878
  * @returns {Promise<{id: string, name: string, token: string}>}
1571
1879
  */
1572
1880
  create: (userId, name, auditMessage) =>
@@ -1583,6 +1891,7 @@ class PlaidClient {
1583
1891
  * needs a sign-in token.
1584
1892
  * @param {string} userId - The user ID who owns the token
1585
1893
  * @param {string} tokenId - The token ID to revoke
1894
+ * @param {string} [auditMessage] - Audit message for this write
1586
1895
  */
1587
1896
  revoke: (userId, tokenId, auditMessage) =>
1588
1897
  this._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
@@ -1600,6 +1909,7 @@ class PlaidClient {
1600
1909
  * @param {object} [opts]
1601
1910
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1602
1911
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1912
+ * @returns {Promise<Invite[]>}
1603
1913
  */
1604
1914
  list: ({ projectId, all } = {}) =>
1605
1915
  listAll(this, "/api/v1/invites", {
@@ -1612,7 +1922,7 @@ class PlaidClient {
1612
1922
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1613
1923
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1614
1924
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1615
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1925
+ * @returns {Promise<Page<Invite>>}
1616
1926
  */
1617
1927
  listPage: ({ projectId, all, limit, cursor } = {}) =>
1618
1928
  listPage(this, "/api/v1/invites", {
@@ -1626,7 +1936,7 @@ class PlaidClient {
1626
1936
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1627
1937
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1628
1938
  * @param {number} [opts.pageSize] - Per-request page size
1629
- * @returns {AsyncGenerator<Array>}
1939
+ * @returns {AsyncGenerator<Invite[]>}
1630
1940
  */
1631
1941
  iterPages: ({ projectId, all, pageSize } = {}) =>
1632
1942
  iterPages(this, "/api/v1/invites", {
@@ -1659,7 +1969,8 @@ class PlaidClient {
1659
1969
  * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
1660
1970
  * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
1661
1971
  * @param {string} [opts.note] - Human label shown in your invite list
1662
- * @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}>}
1663
1974
  */
1664
1975
  create: (
1665
1976
  {
@@ -1692,6 +2003,7 @@ class PlaidClient {
1692
2003
  * Revoke an invite, killing the link immediately. Idempotent. Allowed for
1693
2004
  * the creator, an admin, or a maintainer of the invite's project.
1694
2005
  * @param {string} id - The invite ID
2006
+ * @param {string} [auditMessage] - Audit message for this write
1695
2007
  */
1696
2008
  revoke: (id, auditMessage) =>
1697
2009
  this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
@@ -1704,7 +2016,7 @@ class PlaidClient {
1704
2016
  * configuration and the backups on disk, and the settings an operator
1705
2017
  * gets asked to confirm. Carries no secrets. Runs a count per table and
1706
2018
  * walks the media directory, so open it, do not poll it. Admin only.
1707
- * @returns {Promise<{version: string, jvm: object, database: object, media: object, backup: object, settings: object}>}
2019
+ * @returns {Promise<ServerReport>}
1708
2020
  */
1709
2021
  server: () => this._request("GET", "/api/v1/admin/server"),
1710
2022
  /**
@@ -1718,7 +2030,7 @@ class PlaidClient {
1718
2030
  * rather than the client's usual timeout. A proxy in front of the server
1719
2031
  * may still give up first (a 504): the backup goes on, and its file
1720
2032
  * shows in `server().backup.backups` once it is written.
1721
- * @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
2033
+ * @returns {Promise<BackupStatus>}
1722
2034
  *
1723
2035
  * outOfBand, as every admin action on the server itself is: none of them
1724
2036
  * writes project data (see the note at the top of http.js).
@@ -1738,6 +2050,7 @@ class PlaidClient {
1738
2050
  * Drop the lock on a document whoever holds it. Idempotent. For a client
1739
2051
  * that went away without releasing one. Admin only.
1740
2052
  * @param {string} documentId - The document ID
2053
+ * @returns {Promise<{result: string}>}
1741
2054
  */
1742
2055
  releaseLock: (documentId) =>
1743
2056
  this._request("DELETE", `/api/v1/admin/locks/${documentId}`, {
@@ -1747,7 +2060,7 @@ class PlaidClient {
1747
2060
  * Live login and invite rate-limit buckets: the address, the account
1748
2061
  * where there is one, failures inside the window, the limit, and whether
1749
2062
  * it is currently blocking. Admin only.
1750
- * @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
2063
+ * @returns {Promise<RateLimitSnapshot>}
1751
2064
  */
1752
2065
  rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
1753
2066
  /**
@@ -1757,6 +2070,7 @@ class PlaidClient {
1757
2070
  * @param {object} [opts]
1758
2071
  * @param {string} [opts.ip] - Clear only this address
1759
2072
  * @param {string} [opts.userId] - Narrow to one account on that address
2073
+ * @returns {Promise<{result: string}>}
1760
2074
  */
1761
2075
  clearRateLimits: ({ ip, userId } = {}) =>
1762
2076
  this._request("DELETE", "/api/v1/admin/rate-limits", {
@@ -1780,7 +2094,7 @@ class PlaidClient {
1780
2094
  * @param {string} [opts.status] - "2xx".."5xx", an exact code, or "failures"
1781
2095
  * @param {string} [opts.user] - Only requests made by this account
1782
2096
  * @param {string} [opts.method] - Only requests with this HTTP method
1783
- * @returns {Promise<{requests: object, events: object, file: (string|null)}>}
2097
+ * @returns {Promise<LiveLog>}
1784
2098
  */
1785
2099
  logs: ({ limit, q, level, status, user, method } = {}) =>
1786
2100
  this._request("GET", "/api/v1/admin/logs", {
@@ -1814,7 +2128,7 @@ class PlaidClient {
1814
2128
  * @param {string} [opts.prefix] - Only keys starting with this
1815
2129
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1816
2130
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1817
- * @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
2131
+ * @returns {Promise<AdminUserDataEntry[]>}
1818
2132
  */
1819
2133
  userData: ({ prefix, pattern, includeValues } = {}) =>
1820
2134
  listAll(this, "/api/v1/admin/user-data", {
@@ -1833,7 +2147,7 @@ class PlaidClient {
1833
2147
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1834
2148
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1835
2149
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1836
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2150
+ * @returns {Promise<Page<AdminUserDataEntry>>}
1837
2151
  */
1838
2152
  userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
1839
2153
  listPage(this, "/api/v1/admin/user-data", {
@@ -1874,6 +2188,10 @@ class PlaidClient {
1874
2188
  /**
1875
2189
  * Fetch a single page of the instance-wide audit log. Admin only.
1876
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
1877
2195
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
1878
2196
  * is what a feed wants. A cursor belongs to the direction that made it.
1879
2197
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
@@ -1900,6 +2218,12 @@ class PlaidClient {
1900
2218
  }),
1901
2219
  /**
1902
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
1903
2227
  * @returns {AsyncGenerator<Array>}
1904
2228
  */
1905
2229
  iterPages: ({ startTime, endTime, opTypes, kinds, pageSize } = {}) =>
@@ -1928,7 +2252,7 @@ class PlaidClient {
1928
2252
  * @param {boolean} [opts.daily] - Also return `byDay`, a list of
1929
2253
  * `{date, changes}` per user, oldest first, at the cost of a second
1930
2254
  * grouped scan
1931
- * @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
2255
+ * @returns {Promise<ActivityTallyRow[]>}
1932
2256
  */
1933
2257
  tally: async ({ projectId, startTime, endTime, daily } = {}) => {
1934
2258
  const path = projectId
@@ -1946,22 +2270,16 @@ class PlaidClient {
1946
2270
  };
1947
2271
 
1948
2272
  this.tokenLayers = {
1949
- /**
1950
- * Shift a token layer's display order.
1951
- * @param {string} tokenLayerId - The token layer ID
1952
- * @param {string} direction - The direction ("up" or "down")
1953
- */
1954
- shift: (tokenLayerId, direction, auditMessage) =>
1955
- this._request("POST", `/api/v1/token-layers/${tokenLayerId}/shift`, {
1956
- auditMessage,
1957
- body: bodyOf({ direction }),
1958
- }),
2273
+ ...layerMethods(this, "token-layers"),
1959
2274
  /**
1960
2275
  * Create a new token layer.
1961
2276
  * @param {string} textLayerId - The text layer ID
1962
2277
  * @param {string} name - The name
1963
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.
1964
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)
1965
2283
  */
1966
2284
  create: (
1967
2285
  textLayerId,
@@ -1971,71 +2289,18 @@ class PlaidClient {
1971
2289
  auditMessage,
1972
2290
  { id } = {},
1973
2291
  ) =>
1974
- this._request("POST", "/api/v1/token-layers", {
1975
- auditMessage,
1976
- body: bodyOf({
1977
- id,
1978
- "text-layer-id": textLayerId,
1979
- name,
1980
- "overlap-mode": overlapMode,
1981
- "parent-token-layer-id": parentTokenLayerId,
1982
- }),
1983
- }),
1984
- /**
1985
- * Set a configuration value for a layer in an editor namespace.
1986
- * @param {string} tokenLayerId - The token layer ID
1987
- * @param {string} namespace - The config namespace
1988
- * @param {string} configKey - The config key
1989
- * @param {any} configValue - Configuration value to set
1990
- * @param {string} [auditMessage] - Audit message for this write
1991
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
1992
- * was absent). The write is then refused with a 409 when the stored value differs.
1993
- */
1994
- setConfig: (tokenLayerId, namespace, configKey, configValue, auditMessage, options) =>
1995
- this._request(
1996
- "PUT",
1997
- `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
1998
- configRequest(auditMessage, configValue, options),
1999
- ),
2000
- /**
2001
- * Remove a configuration value for a layer.
2002
- * @param {string} tokenLayerId - The token layer ID
2003
- * @param {string} namespace - The config namespace
2004
- * @param {string} configKey - The config key
2005
- * @param {string} [auditMessage] - Audit message for this write
2006
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2007
- */
2008
- deleteConfig: (tokenLayerId, namespace, configKey, auditMessage, options) =>
2009
- this._request(
2010
- "DELETE",
2011
- `/api/v1/token-layers/${tokenLayerId}/config/${namespace}/${configKey}`,
2012
- configRequest(auditMessage, undefined, options, true),
2013
- ),
2014
- ...constraintMethods(this, "token-layers"),
2015
- /**
2016
- * Get a token layer by ID.
2017
- * @param {string} tokenLayerId - The token layer ID
2018
- */
2019
- get: (tokenLayerId) =>
2020
- this._request("GET", `/api/v1/token-layers/${tokenLayerId}`),
2021
- /**
2022
- * Delete a token layer.
2023
- * @param {string} tokenLayerId - The token layer ID
2024
- */
2025
- delete: (tokenLayerId, auditMessage) =>
2026
- this._request("DELETE", `/api/v1/token-layers/${tokenLayerId}`, {
2027
- auditMessage,
2028
- }),
2029
- /**
2030
- * Update a token layer's name.
2031
- * @param {string} tokenLayerId - The token layer ID
2032
- * @param {string} name - The name
2033
- */
2034
- update: (tokenLayerId, name, auditMessage) =>
2035
- this._request("PATCH", `/api/v1/token-layers/${tokenLayerId}`, {
2292
+ this._request("POST", "/api/v1/token-layers", {
2036
2293
  auditMessage,
2037
- 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
+ }),
2038
2301
  }),
2302
+ ...configMethods(this, "token-layers"),
2303
+ ...constraintMethods(this, "token-layers"),
2039
2304
  };
2040
2305
 
2041
2306
  this.documents = {
@@ -2082,6 +2347,7 @@ class PlaidClient {
2082
2347
  * outOfBand, for the same reason as acquireLock.
2083
2348
  * @param {string} documentId - The document ID
2084
2349
  * @param {string} lockId - The `lockId` acquireLock answered with
2350
+ * @param {string} [auditMessage] - Audit message for this write
2085
2351
  */
2086
2352
  renewLock: (documentId, lockId, auditMessage) =>
2087
2353
  this._request("POST", `/api/v1/documents/${documentId}/lock`, {
@@ -2096,6 +2362,7 @@ class PlaidClient {
2096
2362
  * and not released at all if it aborts.
2097
2363
  * @param {string} documentId - The document ID
2098
2364
  * @param {string} lockId - The `lockId` acquireLock answered with
2365
+ * @param {string} [auditMessage] - Audit message for this write
2099
2366
  */
2100
2367
  releaseLock: (documentId, lockId, auditMessage) =>
2101
2368
  this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
@@ -2139,9 +2406,11 @@ class PlaidClient {
2139
2406
  * lost lock is recorded on the CLIENT, like strict mode, so it stops
2140
2407
  * every write the client makes (on any batch of it too) and not only the
2141
2408
  * ones this block makes.
2409
+ * @template T
2142
2410
  * @param {string} documentId - The document ID
2143
- * @param {(lock: DocumentLock) => any} fn - The work to run while holding it
2144
- * @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>}
2145
2414
  */
2146
2415
  locked: (documentId, fn, options) =>
2147
2416
  withDocumentLock(this, documentId, fn, options),
@@ -2150,11 +2419,34 @@ class PlaidClient {
2150
2419
  * no as-of form: the route refuses the parameter. Prefer the document's
2151
2420
  * own `mediaUrl`, which carries the file's version for caching.
2152
2421
  * @param {string} documentId - The document ID
2422
+ * @returns {Promise<ArrayBuffer>}
2153
2423
  */
2154
2424
  getMedia: (documentId) =>
2155
2425
  this._request("GET", `/api/v1/documents/${documentId}/media`, {
2156
2426
  binaryResponse: true,
2157
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
+ },
2158
2450
  /**
2159
2451
  * Upload a media file for a document. Uses Apache Tika for content validation.
2160
2452
  * A document that already has a recording refuses it with a 409
@@ -2165,7 +2457,7 @@ class PlaidClient {
2165
2457
  * @param {string} documentId - The document ID
2166
2458
  * @param {File} file - The file to upload
2167
2459
  * @param {string} [auditMessage] - Custom audit-log message for this write
2168
- * @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.
2169
2461
  */
2170
2462
  uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
2171
2463
  const fd = new FormData();
@@ -2197,39 +2489,13 @@ class PlaidClient {
2197
2489
  auditMessage,
2198
2490
  queryParams: { "media-version": mediaVersion },
2199
2491
  }),
2200
- /**
2201
- * Replace all metadata for a document.
2202
- * @param {string} documentId - The document ID
2203
- * @param {any} body - The request body
2204
- */
2205
- setMetadata: (documentId, body, auditMessage) =>
2206
- this._request("PUT", `/api/v1/documents/${documentId}/metadata`, {
2207
- auditMessage,
2208
- rawBody: body,
2209
- }),
2210
- /**
2211
- * Remove all metadata from a document.
2212
- * @param {string} documentId - The document ID
2213
- */
2214
- deleteMetadata: (documentId, auditMessage) =>
2215
- this._request("DELETE", `/api/v1/documents/${documentId}/metadata`, {
2216
- auditMessage,
2217
- }),
2218
- /**
2219
- * 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.
2220
- * @param {string} documentId - The document ID
2221
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
2222
- */
2223
- patchMetadata: (documentId, body, auditMessage) =>
2224
- this._request("PATCH", `/api/v1/documents/${documentId}/metadata`, {
2225
- auditMessage,
2226
- rawBody: body,
2227
- }),
2492
+ ...metadataMethods(this, "documents"),
2228
2493
  /**
2229
2494
  * Get audit log for a document. Transparently follows pagination cursors
2230
2495
  * and returns the full flat array. Each entry, and each of its `ops`,
2231
2496
  * says what kind of credential made it as `credential`: `login`,
2232
- * `named-token`, `service` or `delegated` (absent on older operations),
2497
+ * `named-token` or `delegated` (absent on older operations, `service`
2498
+ * on some from 2026-10-06 to 2026-10-08),
2233
2499
  * beside `apiToken` (the named token's id and name) when there was one.
2234
2500
  * @param {string} documentId - The document ID
2235
2501
  * @param {string} [startTime] - Start of time range
@@ -2256,6 +2522,10 @@ class PlaidClient {
2256
2522
  * that may be long: audit() walks every page before it resolves.
2257
2523
  * @param {string} documentId - The document ID
2258
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
2259
2529
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2260
2530
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2261
2531
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -2292,7 +2562,7 @@ class PlaidClient {
2292
2562
  * @param {string} documentId - The document ID
2293
2563
  * @param {string} asOf - The moment to go back to (ISO-8601 instant),
2294
2564
  * typically a history entry's `endTime`
2295
- * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
2565
+ * @param {{dryRun?: boolean}} [options] - `{ dryRun }`: with `dryRun` true nothing
2296
2566
  * is written and the summary says what would change
2297
2567
  * @param {string} [auditMessage] - Custom audit message for this operation
2298
2568
  */
@@ -2330,6 +2600,7 @@ class PlaidClient {
2330
2600
  /**
2331
2601
  * Delete a document and all data contained.
2332
2602
  * @param {string} documentId - The document ID
2603
+ * @param {string} [auditMessage] - Audit message for this write
2333
2604
  */
2334
2605
  delete: (documentId, auditMessage) =>
2335
2606
  this._request("DELETE", `/api/v1/documents/${documentId}`, {
@@ -2339,6 +2610,7 @@ class PlaidClient {
2339
2610
  * Update a document's name.
2340
2611
  * @param {string} documentId - The document ID
2341
2612
  * @param {string} name - The name
2613
+ * @param {string} [auditMessage] - Audit message for this write
2342
2614
  */
2343
2615
  update: (documentId, name, auditMessage) =>
2344
2616
  this._request("PATCH", `/api/v1/documents/${documentId}`, {
@@ -2350,6 +2622,9 @@ class PlaidClient {
2350
2622
  * @param {string} projectId - The project ID
2351
2623
  * @param {string} name - The name
2352
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)
2353
2628
  */
2354
2629
  create: (projectId, name, metadata, auditMessage, { id } = {}) =>
2355
2630
  this._request("POST", "/api/v1/documents", {
@@ -2366,9 +2641,10 @@ class PlaidClient {
2366
2641
  * the source had media the copy could not take with it.
2367
2642
  * @param {string} documentId - The document to copy
2368
2643
  * @param {string} name - The new document's name
2369
- * @param {object} [options] - `{ includeMedia }`: false leaves the media
2644
+ * @param {{includeMedia?: boolean, id?: string}} [options] - `{ includeMedia }`: false leaves the media
2370
2645
  * file behind
2371
2646
  * @param {string} [auditMessage] - Custom audit message for this operation
2647
+ * @returns {Promise<{id: string, mediaError?: string}>}
2372
2648
  */
2373
2649
  copy: (documentId, name, { includeMedia, id } = {}, auditMessage) =>
2374
2650
  this._request("POST", `/api/v1/documents/${documentId}/copy`, {
@@ -2382,6 +2658,7 @@ class PlaidClient {
2382
2658
  * Set a user's access level to read and write for this project.
2383
2659
  * @param {string} id - The resource ID
2384
2660
  * @param {string} userId - The user ID
2661
+ * @param {string} [auditMessage] - Audit message for this write
2385
2662
  */
2386
2663
  addWriter: (id, userId, auditMessage) =>
2387
2664
  this._request("POST", `/api/v1/projects/${id}/writers/${userId}`, {
@@ -2391,6 +2668,7 @@ class PlaidClient {
2391
2668
  * Remove a user's writer privileges for this project.
2392
2669
  * @param {string} id - The resource ID
2393
2670
  * @param {string} userId - The user ID
2671
+ * @param {string} [auditMessage] - Audit message for this write
2394
2672
  */
2395
2673
  removeWriter: (id, userId, auditMessage) =>
2396
2674
  this._request("DELETE", `/api/v1/projects/${id}/writers/${userId}`, {
@@ -2400,6 +2678,7 @@ class PlaidClient {
2400
2678
  * Set a user's access level to read-only for this project.
2401
2679
  * @param {string} id - The resource ID
2402
2680
  * @param {string} userId - The user ID
2681
+ * @param {string} [auditMessage] - Audit message for this write
2403
2682
  */
2404
2683
  addReader: (id, userId, auditMessage) =>
2405
2684
  this._request("POST", `/api/v1/projects/${id}/readers/${userId}`, {
@@ -2409,45 +2688,18 @@ class PlaidClient {
2409
2688
  * Remove a user's reader privileges for this project.
2410
2689
  * @param {string} id - The resource ID
2411
2690
  * @param {string} userId - The user ID
2691
+ * @param {string} [auditMessage] - Audit message for this write
2412
2692
  */
2413
2693
  removeReader: (id, userId, auditMessage) =>
2414
2694
  this._request("DELETE", `/api/v1/projects/${id}/readers/${userId}`, {
2415
2695
  auditMessage,
2416
2696
  }),
2417
- /**
2418
- * Set a configuration value for a project in an editor namespace.
2419
- * @param {string} id - The resource ID
2420
- * @param {string} namespace - The config namespace
2421
- * @param {string} configKey - The config key
2422
- * @param {any} configValue - Configuration value to set
2423
- * @param {string} [auditMessage] - Audit message for this write
2424
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
2425
- * was absent). The write is then refused with a 409 when the stored value differs.
2426
- */
2427
- setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
2428
- this._request(
2429
- "PUT",
2430
- `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
2431
- configRequest(auditMessage, configValue, options),
2432
- ),
2433
- /**
2434
- * Remove a configuration value for a project.
2435
- * @param {string} id - The resource ID
2436
- * @param {string} namespace - The config namespace
2437
- * @param {string} configKey - The config key
2438
- * @param {string} [auditMessage] - Audit message for this write
2439
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2440
- */
2441
- deleteConfig: (id, namespace, configKey, auditMessage, options) =>
2442
- this._request(
2443
- "DELETE",
2444
- `/api/v1/projects/${id}/config/${namespace}/${configKey}`,
2445
- configRequest(auditMessage, undefined, options, true),
2446
- ),
2697
+ ...configMethods(this, "projects"),
2447
2698
  /**
2448
2699
  * Assign a user as a maintainer for this project.
2449
2700
  * @param {string} id - The resource ID
2450
2701
  * @param {string} userId - The user ID
2702
+ * @param {string} [auditMessage] - Audit message for this write
2451
2703
  */
2452
2704
  addMaintainer: (id, userId, auditMessage) =>
2453
2705
  this._request("POST", `/api/v1/projects/${id}/maintainers/${userId}`, {
@@ -2457,6 +2709,7 @@ class PlaidClient {
2457
2709
  * Remove a user's maintainer privileges for this project.
2458
2710
  * @param {string} id - The resource ID
2459
2711
  * @param {string} userId - The user ID
2712
+ * @param {string} [auditMessage] - Audit message for this write
2460
2713
  */
2461
2714
  removeMaintainer: (id, userId, auditMessage) =>
2462
2715
  this._request(
@@ -2468,7 +2721,8 @@ class PlaidClient {
2468
2721
  * Get audit log for a project. Transparently follows pagination cursors
2469
2722
  * and returns the full flat array. Each entry, and each of its `ops`,
2470
2723
  * says what kind of credential made it as `credential`: `login`,
2471
- * `named-token`, `service` or `delegated` (absent on older operations),
2724
+ * `named-token` or `delegated` (absent on older operations, `service`
2725
+ * on some from 2026-10-06 to 2026-10-08),
2472
2726
  * beside `apiToken` (the named token's id and name) when there was one.
2473
2727
  * @param {string} projectId - The project ID
2474
2728
  * @param {string} [startTime] - Start of time range
@@ -2495,6 +2749,10 @@ class PlaidClient {
2495
2749
  * that may be long: audit() walks every page before it resolves.
2496
2750
  * @param {string} projectId - The project ID
2497
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
2498
2756
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2499
2757
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2500
2758
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -2539,6 +2797,7 @@ class PlaidClient {
2539
2797
  * Link a vocabulary to a project.
2540
2798
  * @param {string} id - The resource ID
2541
2799
  * @param {string} vocabId - The vocab layer ID
2800
+ * @param {string} [auditMessage] - Audit message for this write
2542
2801
  */
2543
2802
  linkVocab: (id, vocabId, auditMessage) =>
2544
2803
  this._request("POST", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
@@ -2548,6 +2807,7 @@ class PlaidClient {
2548
2807
  * Unlink a vocabulary from a project.
2549
2808
  * @param {string} id - The resource ID
2550
2809
  * @param {string} vocabId - The vocab layer ID
2810
+ * @param {string} [auditMessage] - Audit message for this write
2551
2811
  */
2552
2812
  unlinkVocab: (id, vocabId, auditMessage) =>
2553
2813
  this._request("DELETE", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
@@ -2613,6 +2873,7 @@ class PlaidClient {
2613
2873
  * Update a project's name.
2614
2874
  * @param {string} id - The resource ID
2615
2875
  * @param {string} name - The name
2876
+ * @param {string} [auditMessage] - Audit message for this write
2616
2877
  */
2617
2878
  update: (id, name, auditMessage) =>
2618
2879
  this._request("PATCH", `/api/v1/projects/${id}`, {
@@ -2649,6 +2910,9 @@ class PlaidClient {
2649
2910
  /**
2650
2911
  * Create a new project. Note: this also registers the user as a maintainer.
2651
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)
2652
2916
  */
2653
2917
  create: (name, auditMessage, { id } = {}) =>
2654
2918
  this._request("POST", "/api/v1/projects", {
@@ -2658,74 +2922,15 @@ class PlaidClient {
2658
2922
  };
2659
2923
 
2660
2924
  this.textLayers = {
2661
- /**
2662
- * Set a configuration value for a layer in an editor namespace.
2663
- * @param {string} textLayerId - The text layer ID
2664
- * @param {string} namespace - The config namespace
2665
- * @param {string} configKey - The config key
2666
- * @param {any} configValue - Configuration value to set
2667
- * @param {string} [auditMessage] - Audit message for this write
2668
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
2669
- * was absent). The write is then refused with a 409 when the stored value differs.
2670
- */
2671
- setConfig: (textLayerId, namespace, configKey, configValue, auditMessage, options) =>
2672
- this._request(
2673
- "PUT",
2674
- `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
2675
- configRequest(auditMessage, configValue, options),
2676
- ),
2677
- /**
2678
- * Remove a configuration value for a layer.
2679
- * @param {string} textLayerId - The text layer ID
2680
- * @param {string} namespace - The config namespace
2681
- * @param {string} configKey - The config key
2682
- * @param {string} [auditMessage] - Audit message for this write
2683
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2684
- */
2685
- deleteConfig: (textLayerId, namespace, configKey, auditMessage, options) =>
2686
- this._request(
2687
- "DELETE",
2688
- `/api/v1/text-layers/${textLayerId}/config/${namespace}/${configKey}`,
2689
- configRequest(auditMessage, undefined, options, true),
2690
- ),
2691
- /**
2692
- * Get a text layer by ID.
2693
- * @param {string} textLayerId - The text layer ID
2694
- */
2695
- get: (textLayerId) =>
2696
- this._request("GET", `/api/v1/text-layers/${textLayerId}`),
2697
- /**
2698
- * Delete a text layer.
2699
- * @param {string} textLayerId - The text layer ID
2700
- */
2701
- delete: (textLayerId, auditMessage) =>
2702
- this._request("DELETE", `/api/v1/text-layers/${textLayerId}`, {
2703
- auditMessage,
2704
- }),
2705
- /**
2706
- * Update a text layer's name.
2707
- * @param {string} textLayerId - The text layer ID
2708
- * @param {string} name - The name
2709
- */
2710
- update: (textLayerId, name, auditMessage) =>
2711
- this._request("PATCH", `/api/v1/text-layers/${textLayerId}`, {
2712
- auditMessage,
2713
- body: bodyOf({ name }),
2714
- }),
2715
- /**
2716
- * Shift a text layer's display order within the project.
2717
- * @param {string} textLayerId - The text layer ID
2718
- * @param {string} direction - The direction ("up" or "down")
2719
- */
2720
- shift: (textLayerId, direction, auditMessage) =>
2721
- this._request("POST", `/api/v1/text-layers/${textLayerId}/shift`, {
2722
- auditMessage,
2723
- body: bodyOf({ direction }),
2724
- }),
2925
+ ...configMethods(this, "text-layers"),
2926
+ ...layerMethods(this, "text-layers"),
2725
2927
  /**
2726
2928
  * Create a new text layer for a project.
2727
2929
  * @param {string} projectId - The project ID
2728
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)
2729
2934
  */
2730
2935
  create: (projectId, name, auditMessage, { id } = {}) =>
2731
2936
  this._request("POST", "/api/v1/text-layers", {
@@ -2735,39 +2940,15 @@ class PlaidClient {
2735
2940
  };
2736
2941
 
2737
2942
  this.vocabItems = {
2738
- /**
2739
- * Replace all metadata for a vocab item.
2740
- * @param {string} id - The resource ID
2741
- * @param {any} body - The request body
2742
- */
2743
- setMetadata: (id, body, auditMessage) =>
2744
- this._request("PUT", `/api/v1/vocab-items/${id}/metadata`, {
2745
- auditMessage,
2746
- rawBody: body,
2747
- }),
2748
- /**
2749
- * Remove all metadata from a vocab item.
2750
- * @param {string} id - The resource ID
2751
- */
2752
- deleteMetadata: (id, auditMessage) =>
2753
- this._request("DELETE", `/api/v1/vocab-items/${id}/metadata`, {
2754
- auditMessage,
2755
- }),
2756
- /**
2757
- * 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.
2758
- * @param {string} id - The resource ID
2759
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
2760
- */
2761
- patchMetadata: (id, body, auditMessage) =>
2762
- this._request("PATCH", `/api/v1/vocab-items/${id}/metadata`, {
2763
- auditMessage,
2764
- rawBody: body,
2765
- }),
2943
+ ...metadataMethods(this, "vocab-items"),
2766
2944
  /**
2767
2945
  * Create a new vocab item
2768
2946
  * @param {string} vocabLayerId - The vocab layer ID
2769
2947
  * @param {string} form - The vocab item form
2770
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)
2771
2952
  */
2772
2953
  create: (vocabLayerId, form, metadata, auditMessage, { id } = {}) =>
2773
2954
  this._request("POST", "/api/v1/vocab-items", {
@@ -2778,6 +2959,8 @@ class PlaidClient {
2778
2959
  * Create multiple vocab items in a single operation. Entries may target
2779
2960
  * different vocab layers; the user must have write access to each.
2780
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[]}>}
2781
2964
  */
2782
2965
  bulkCreate: (body, auditMessage) =>
2783
2966
  this._request("POST", "/api/v1/vocab-items/bulk", {
@@ -2788,6 +2971,7 @@ class PlaidClient {
2788
2971
  * Update many vocab items in a single operation: set forms and/or patch
2789
2972
  * metadata.
2790
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
2791
2975
  * @returns {Promise<{count: number}>} How many vocab items were updated.
2792
2976
  */
2793
2977
  bulkUpdate: (body, auditMessage) =>
@@ -2802,6 +2986,8 @@ class PlaidClient {
2802
2986
  * Every document holding one of those links has its version bumped, and
2803
2987
  * a strict-mode client picks up their new versions from the response.
2804
2988
  * @param {string[]} body - The vocab item IDs to delete
2989
+ * @param {string} [auditMessage] - Audit message for this write
2990
+ * @returns {Promise<void>}
2805
2991
  */
2806
2992
  bulkDelete: (body, auditMessage) =>
2807
2993
  this._request("DELETE", "/api/v1/vocab-items/bulk", {
@@ -2844,6 +3030,7 @@ class PlaidClient {
2844
3030
  * Needs maintainer rights on the vocabulary.
2845
3031
  * @param {string} survivorId - The entry that stays
2846
3032
  * @param {string[]} loserIds - The entries merged into it
3033
+ * @param {string} [auditMessage] - Audit message for this write
2847
3034
  * @returns {Promise<{moved: number, duplicates: number, removed: string[]}>}
2848
3035
  */
2849
3036
  merge: (survivorId, loserIds, auditMessage) =>
@@ -2858,6 +3045,7 @@ class PlaidClient {
2858
3045
  * from the response.
2859
3046
  * @param {string} id - The resource ID
2860
3047
  * @param {string} form - The vocab item form
3048
+ * @param {string} [auditMessage] - Audit message for this write
2861
3049
  */
2862
3050
  update: (id, form, auditMessage) =>
2863
3051
  this._request("PATCH", `/api/v1/vocab-items/${id}`, {
@@ -2867,82 +3055,22 @@ class PlaidClient {
2867
3055
  };
2868
3056
 
2869
3057
  this.relationLayers = {
2870
- /**
2871
- * Shift a relation layer's display order.
2872
- * @param {string} relationLayerId - The relation layer ID
2873
- * @param {string} direction - The direction ("up" or "down")
2874
- */
2875
- shift: (relationLayerId, direction, auditMessage) =>
2876
- this._request(
2877
- "POST",
2878
- `/api/v1/relation-layers/${relationLayerId}/shift`,
2879
- { auditMessage, body: bodyOf({ direction }) },
2880
- ),
3058
+ ...layerMethods(this, "relation-layers"),
2881
3059
  /**
2882
3060
  * Create a new relation layer.
2883
3061
  * @param {string} spanLayerId - The span layer ID
2884
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)
2885
3066
  */
2886
3067
  create: (spanLayerId, name, auditMessage, { id } = {}) =>
2887
3068
  this._request("POST", "/api/v1/relation-layers", {
2888
3069
  auditMessage,
2889
3070
  body: bodyOf({ id, "span-layer-id": spanLayerId, name }),
2890
3071
  }),
2891
- /**
2892
- * Set a configuration value for a layer in an editor namespace.
2893
- * @param {string} relationLayerId - The relation layer ID
2894
- * @param {string} namespace - The config namespace
2895
- * @param {string} configKey - The config key
2896
- * @param {any} configValue - Configuration value to set
2897
- * @param {string} [auditMessage] - Audit message for this write
2898
- * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
2899
- * was absent). The write is then refused with a 409 when the stored value differs.
2900
- */
2901
- setConfig: (relationLayerId, namespace, configKey, configValue, auditMessage, options) =>
2902
- this._request(
2903
- "PUT",
2904
- `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
2905
- configRequest(auditMessage, configValue, options),
2906
- ),
2907
- /**
2908
- * Remove a configuration value for a layer.
2909
- * @param {string} relationLayerId - The relation layer ID
2910
- * @param {string} namespace - The config namespace
2911
- * @param {string} configKey - The config key
2912
- * @param {string} [auditMessage] - Audit message for this write
2913
- * @param {{expected?: any}} [options] - `expected`, as on setConfig.
2914
- */
2915
- deleteConfig: (relationLayerId, namespace, configKey, auditMessage, options) =>
2916
- this._request(
2917
- "DELETE",
2918
- `/api/v1/relation-layers/${relationLayerId}/config/${namespace}/${configKey}`,
2919
- configRequest(auditMessage, undefined, options, true),
2920
- ),
3072
+ ...configMethods(this, "relation-layers"),
2921
3073
  ...constraintMethods(this, "relation-layers"),
2922
- /**
2923
- * Get a relation layer by ID.
2924
- * @param {string} relationLayerId - The relation layer ID
2925
- */
2926
- get: (relationLayerId) =>
2927
- this._request("GET", `/api/v1/relation-layers/${relationLayerId}`),
2928
- /**
2929
- * Delete a relation layer.
2930
- * @param {string} relationLayerId - The relation layer ID
2931
- */
2932
- delete: (relationLayerId, auditMessage) =>
2933
- this._request("DELETE", `/api/v1/relation-layers/${relationLayerId}`, {
2934
- auditMessage,
2935
- }),
2936
- /**
2937
- * Update a relation layer's name.
2938
- * @param {string} relationLayerId - The relation layer ID
2939
- * @param {string} name - The name
2940
- */
2941
- update: (relationLayerId, name, auditMessage) =>
2942
- this._request("PATCH", `/api/v1/relation-layers/${relationLayerId}`, {
2943
- auditMessage,
2944
- body: bodyOf({ name }),
2945
- }),
2946
3074
  };
2947
3075
 
2948
3076
  this.tokens = {
@@ -2963,6 +3091,9 @@ class PlaidClient {
2963
3091
  * @param {number} end - End offset, exclusive (Unicode code points)
2964
3092
  * @param {number} [precedence] - Ordering precedence
2965
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)
2966
3097
  */
2967
3098
  create: (
2968
3099
  tokenLayerId,
@@ -2996,6 +3127,7 @@ class PlaidClient {
2996
3127
  * Delete a token and remove it from any spans. If this causes a span to
2997
3128
  * have no remaining tokens, the span will also be deleted.
2998
3129
  * @param {string} tokenId - The token ID
3130
+ * @param {string} [auditMessage] - Audit message for this write
2999
3131
  */
3000
3132
  delete: (tokenId, auditMessage) =>
3001
3133
  this._request("DELETE", `/api/v1/tokens/${tokenId}`, { auditMessage }),
@@ -3008,6 +3140,7 @@ class PlaidClient {
3008
3140
  * to leave unchanged; pass a number to set; pass null explicitly to
3009
3141
  * CLEAR it (revert to no explicit ordering). bodyOf keeps null but
3010
3142
  * drops undefined, so the three cases map correctly to the server.
3143
+ * @param {string} [auditMessage] - Audit message for this write
3011
3144
  */
3012
3145
  update: (tokenId, begin, end, precedence, auditMessage) =>
3013
3146
  this._request("PATCH", `/api/v1/tokens/${tokenId}`, {
@@ -3017,6 +3150,7 @@ class PlaidClient {
3017
3150
  /**
3018
3151
  * Create multiple tokens in a single operation.
3019
3152
  * @param {Array} body - The request body
3153
+ * @param {string} [auditMessage] - Audit message for this write
3020
3154
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
3021
3155
  */
3022
3156
  bulkCreate: (body, auditMessage) =>
@@ -3024,12 +3158,15 @@ class PlaidClient {
3024
3158
  /**
3025
3159
  * Delete multiple tokens in a single operation. Provide an array of IDs.
3026
3160
  * @param {Array} body - The request body
3161
+ * @param {string} [auditMessage] - Audit message for this write
3162
+ * @returns {Promise<void>}
3027
3163
  */
3028
3164
  bulkDelete: (body, auditMessage) =>
3029
3165
  this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
3030
3166
  /**
3031
3167
  * Update many tokens in a single operation: patch metadata.
3032
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
3033
3170
  * @returns {Promise<{count: number}>} How many tokens were updated.
3034
3171
  */
3035
3172
  bulkUpdate: (body, auditMessage) =>
@@ -3068,6 +3205,7 @@ class PlaidClient {
3068
3205
  * merged extent must not engulf a third token.
3069
3206
  * @param {string} tokenId - The anchor token ID
3070
3207
  * @param {string} otherTokenId - The other token to merge in
3208
+ * @param {string} [auditMessage] - Audit message for this write
3071
3209
  */
3072
3210
  merge: (tokenId, otherTokenId, auditMessage) =>
3073
3211
  this._request("POST", `/api/v1/tokens/${tokenId}/merge`, {
@@ -3081,49 +3219,23 @@ class PlaidClient {
3081
3219
  * @param {string} tokenId - The token ID
3082
3220
  * @param {number} [begin] - New start offset, inclusive (Unicode code points)
3083
3221
  * @param {number} [end] - New end offset, exclusive (Unicode code points)
3222
+ * @param {string} [auditMessage] - Audit message for this write
3084
3223
  */
3085
3224
  shift: (tokenId, begin, end, auditMessage) =>
3086
3225
  this._request("POST", `/api/v1/tokens/${tokenId}/shift`, {
3087
3226
  auditMessage,
3088
3227
  body: bodyOf({ begin, end }),
3089
3228
  }),
3090
- /**
3091
- * Replace all metadata for a token.
3092
- * @param {string} tokenId - The token ID
3093
- * @param {any} body - The request body
3094
- */
3095
- setMetadata: (tokenId, body, auditMessage) =>
3096
- this._request("PUT", `/api/v1/tokens/${tokenId}/metadata`, {
3097
- auditMessage,
3098
- rawBody: body,
3099
- }),
3100
- /**
3101
- * Remove all metadata from a token.
3102
- * @param {string} tokenId - The token ID
3103
- */
3104
- deleteMetadata: (tokenId, auditMessage) =>
3105
- this._request("DELETE", `/api/v1/tokens/${tokenId}/metadata`, {
3106
- auditMessage,
3107
- }),
3108
- /**
3109
- * 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.
3110
- * @param {string} tokenId - The token ID
3111
- * @param {Array<{op: string, path: string[], value?: any}>} body - The metadata ops
3112
- */
3113
- patchMetadata: (tokenId, body, auditMessage) =>
3114
- this._request("PATCH", `/api/v1/tokens/${tokenId}/metadata`, {
3115
- auditMessage,
3116
- rawBody: body,
3117
- }),
3229
+ ...metadataMethods(this, "tokens"),
3118
3230
  };
3119
3231
 
3120
3232
  this.messages = {
3121
3233
  /**
3122
3234
  * Open a Server-Sent Events stream for a project.
3123
3235
  * @param {string} projectId - The UUID of the project to listen to
3124
- * @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.
3125
3237
  * @param {string} [path] - Stream path under baseUrl (defaults to the project /listen bus; service channels pass their own).
3126
- * @returns {Object} SSE connection object with .close() and .getStats() methods
3238
+ * @returns {SSEConnection} SSE connection object with .close() and .getStats() methods
3127
3239
  */
3128
3240
  listen: (projectId, onEvent, path) =>
3129
3241
  createSSEConnection(this, projectId, onEvent, path),
@@ -3156,7 +3268,7 @@ class PlaidClient {
3156
3268
  * connected services carry `online: true`; previously-seen offline ones
3157
3269
  * carry `online: false` plus a `lastSeenAt` stamp.
3158
3270
  * @param {string} projectId - The UUID of the project to query
3159
- * @returns {Promise<Array>} Array of discovered service information
3271
+ * @returns {Promise<DiscoveredService[]>} Array of discovered service information
3160
3272
  */
3161
3273
  discoverServices: (projectId) => discoverServices(this, projectId),
3162
3274
 
@@ -3176,14 +3288,14 @@ class PlaidClient {
3176
3288
  * The registration reopens its channel whenever it drops, so a server
3177
3289
  * restart needs no service restart.
3178
3290
  * @param {string} projectId - The UUID of the project to serve
3179
- * @param {Object} serviceInfo - Service information {serviceId, serviceName, description}
3180
- * @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)
3181
3293
  * @param {Object} [extras] - Optional additional service metadata
3182
- * @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
3183
3295
  * connection transitions: 'registered', 'reconnected', 'disconnected',
3184
3296
  * and 'stopped' when the server refuses the channel for good, which
3185
3297
  * ends the registration
3186
- * @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
3298
+ * @returns {ServiceRegistration} Service registration with .stop(), .isRunning(), .isConnected()
3187
3299
  */
3188
3300
  serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
3189
3301
  serve(this, projectId, serviceInfo, onServiceRequest, extras, onStatus),
@@ -3204,7 +3316,7 @@ class PlaidClient {
3204
3316
  * @param {number} [timeout=10000] - Timeout in ms
3205
3317
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
3206
3318
  * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
3207
- * @param {Object} [opts] - {requestId, onAccepted, projectIds, noOperation}
3319
+ * @param {{requestId?: string, onAccepted?: (requestId: string) => void, projectIds?: string[], noOperation?: boolean}} [opts] - {requestId, onAccepted, projectIds, noOperation}
3208
3320
  * @returns {Promise<any>} The service's result
3209
3321
  */
3210
3322
  requestService: (
@@ -3349,11 +3461,13 @@ class PlaidClient {
3349
3461
  * A comment outlives its anchor: deleting the entity does not delete
3350
3462
  * the comment. `anchorLabel` is the caption shown once that happens
3351
3463
  * ("Gloss of ktab, sentence 4"), so pass what the comment is about.
3352
- * @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
3353
3465
  * @param {string} entityId - The commented entity's id
3354
3466
  * @param {string} body - The comment text (1..10000 characters)
3355
3467
  * @param {object} [opts]
3356
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>}
3357
3471
  */
3358
3472
  create: (entityType, entityId, body, { anchorLabel, id } = {}) =>
3359
3473
  this._request("POST", "/api/v1/comments", {
@@ -3371,6 +3485,7 @@ class PlaidClient {
3371
3485
  /**
3372
3486
  * Read one comment.
3373
3487
  * @param {string} id - The comment id
3488
+ * @returns {Promise<Comment>}
3374
3489
  */
3375
3490
  get: (id) => this._request("GET", `/api/v1/comments/${id}`),
3376
3491
  /**
@@ -3378,6 +3493,7 @@ class PlaidClient {
3378
3493
  * maintainers, not admins. Sets `edited` on the comment.
3379
3494
  * @param {string} id - The comment id
3380
3495
  * @param {string} body - The replacement text
3496
+ * @returns {Promise<Comment>}
3381
3497
  */
3382
3498
  update: (id, body) =>
3383
3499
  this._request("PATCH", `/api/v1/comments/${id}`, {
@@ -3399,6 +3515,7 @@ class PlaidClient {
3399
3515
  * @param {string} [filters.documentId] - Every comment anywhere in one document
3400
3516
  * @param {string} [filters.entityType] - With entityId, one entity's thread
3401
3517
  * @param {string} [filters.entityId] - With entityType, one entity's thread
3518
+ * @returns {Promise<Comment[]>}
3402
3519
  */
3403
3520
  list: (projectId, { documentId, entityType, entityId } = {}) =>
3404
3521
  listAll(this, `/api/v1/projects/${projectId}/comments`, {
@@ -3417,7 +3534,7 @@ class PlaidClient {
3417
3534
  * @param {string} [opts.documentId] - Every comment anywhere in one document
3418
3535
  * @param {string} [opts.entityType] - With entityId, one entity's thread
3419
3536
  * @param {string} [opts.entityId] - With entityType, one entity's thread
3420
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3537
+ * @returns {Promise<Page<Comment>>}
3421
3538
  */
3422
3539
  listPage: (
3423
3540
  projectId,
@@ -3440,7 +3557,7 @@ class PlaidClient {
3440
3557
  * @param {string} [opts.documentId] - Every comment anywhere in one document
3441
3558
  * @param {string} [opts.entityType] - With entityId, one entity's thread
3442
3559
  * @param {string} [opts.entityId] - With entityType, one entity's thread
3443
- * @returns {AsyncGenerator<Array>}
3560
+ * @returns {AsyncGenerator<Comment[]>}
3444
3561
  */
3445
3562
  iterPages: (
3446
3563
  projectId,
@@ -3463,7 +3580,7 @@ class PlaidClient {
3463
3580
  * The response is NOT key-transformed: its keys are entity ids, and
3464
3581
  * camelCasing would mangle the hyphens in a UUID.
3465
3582
  * @param {string} projectId - The project to read
3466
- * @param {object} [filters] - Same filters as list()
3583
+ * @param {{documentId?: string, entityType?: CommentableType, entityId?: string}} [filters] - Same filters as list()
3467
3584
  * @returns {Promise<Object<string, number>>}
3468
3585
  */
3469
3586
  counts: (projectId, { documentId, entityType, entityId } = {}) =>
@@ -3482,6 +3599,7 @@ class PlaidClient {
3482
3599
  * @param {string} vocabId - The vocab layer to read
3483
3600
  * @param {object} [filters]
3484
3601
  * @param {string} [filters.entityId] - One entry's thread
3602
+ * @returns {Promise<Comment[]>}
3485
3603
  */
3486
3604
  listInVocab: (vocabId, { entityId } = {}) =>
3487
3605
  listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3494,7 +3612,7 @@ class PlaidClient {
3494
3612
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
3495
3613
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
3496
3614
  * @param {string} [opts.entityId] - One entry's thread
3497
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3615
+ * @returns {Promise<Page<Comment>>}
3498
3616
  */
3499
3617
  listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
3500
3618
  listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3509,7 +3627,7 @@ class PlaidClient {
3509
3627
  * @param {object} [opts]
3510
3628
  * @param {string} [opts.entityId] - One entry's thread
3511
3629
  * @param {number} [opts.pageSize] - Per-request page size
3512
- * @returns {AsyncGenerator<Array>}
3630
+ * @returns {AsyncGenerator<Comment[]>}
3513
3631
  */
3514
3632
  iterInVocabPages: (vocabId, { entityId, pageSize } = {}) =>
3515
3633
  iterPages(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3537,15 +3655,15 @@ class PlaidClient {
3537
3655
 
3538
3656
  this.events = {
3539
3657
  /**
3540
- * Record one research-telemetry event (core manual, "Research
3541
- * 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
3542
3660
  * others every ten seconds, at fifty events, and when the page is
3543
3661
  * hidden. A batch that fails is dropped, nothing is retried, and
3544
3662
  * nothing is ever thrown. Sends nothing for a project whose switch
3545
3663
  * (`config.plaid.research.telemetry`) is off, which it reads from the
3546
3664
  * project itself. `suggestion.shown` is recorded once per target,
3547
3665
  * field and value in a page session.
3548
- * @param {'suggestion.shown'|'suggestion.adopted'|'suggestion.dismissed'|'plan.opened'} type - The event type
3666
+ * @param {ClientEventType} type - The event type
3549
3667
  * @param {object} fields
3550
3668
  * @param {string} fields.projectId - The project it happened in
3551
3669
  * @param {string} [fields.documentId] - The document it happened in
@@ -3556,6 +3674,7 @@ class PlaidClient {
3556
3674
  record: (type, fields) => recorderFor(this).record(this, type, fields),
3557
3675
  /**
3558
3676
  * Send what the recorder holds now, rather than at its next interval.
3677
+ * @returns {void}
3559
3678
  */
3560
3679
  flush: () => recorderFor(this).flush(),
3561
3680
  /**
@@ -3563,6 +3682,7 @@ class PlaidClient {
3563
3682
  * maintainer changed it. Turning it off drops what is buffered.
3564
3683
  * @param {string} projectId - The project
3565
3684
  * @param {boolean} on - Whether telemetry is on
3685
+ * @returns {void}
3566
3686
  */
3567
3687
  setEnabled: (projectId, on) => recorderFor(this).setEnabled(projectId, on),
3568
3688
  /**
@@ -3587,6 +3707,7 @@ class PlaidClient {
3587
3707
  * @param {string|string[]} [filters.types] - Only these types
3588
3708
  * @param {string} [filters.startTime] - Only events the server stamped at or after this instant
3589
3709
  * @param {string} [filters.endTime] - Only events the server stamped at or before this instant
3710
+ * @returns {Promise<ClientEvent[]>}
3590
3711
  */
3591
3712
  list: (projectId, { types, startTime, endTime } = {}) =>
3592
3713
  listAll(this, `/api/v1/projects/${projectId}/events`, {
@@ -3601,7 +3722,7 @@ class PlaidClient {
3601
3722
  * @param {string|string[]} [opts.types] - Only these types
3602
3723
  * @param {string} [opts.startTime] - Only events at or after this instant
3603
3724
  * @param {string} [opts.endTime] - Only events at or before this instant
3604
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3725
+ * @returns {Promise<Page<ClientEvent>>}
3605
3726
  */
3606
3727
  listPage: (
3607
3728
  projectId,
@@ -3620,7 +3741,7 @@ class PlaidClient {
3620
3741
  * @param {string|string[]} [opts.types] - Only these types
3621
3742
  * @param {string} [opts.startTime] - Only events at or after this instant
3622
3743
  * @param {string} [opts.endTime] - Only events at or before this instant
3623
- * @returns {AsyncGenerator<Array>}
3744
+ * @returns {AsyncGenerator<ClientEvent[]>}
3624
3745
  */
3625
3746
  iterPages: (projectId, { pageSize, types, startTime, endTime } = {}) =>
3626
3747
  iterPages(this, `/api/v1/projects/${projectId}/events`, {
@@ -3641,6 +3762,9 @@ class PlaidClient {
3641
3762
  * @param {object} [opts]
3642
3763
  * @param {string} [opts.body] - The Markdown text (up to 20000 characters; may be empty)
3643
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}>}
3644
3768
  */
3645
3769
  create: (projectId, title, { body, pinned, id } = {}, auditMessage) =>
3646
3770
  this._request("POST", `/api/v1/projects/${projectId}/guidelines`, {
@@ -3650,6 +3774,7 @@ class PlaidClient {
3650
3774
  /**
3651
3775
  * Read one guideline, Markdown body included.
3652
3776
  * @param {string} id - The guideline id
3777
+ * @returns {Promise<Guideline>}
3653
3778
  */
3654
3779
  get: (id) => this._request("GET", `/api/v1/guidelines/${id}`),
3655
3780
  /**
@@ -3666,6 +3791,8 @@ class PlaidClient {
3666
3791
  * @param {string} [changes.body] - The new Markdown text
3667
3792
  * @param {boolean} [changes.pinned] - Whether the assistant always gets it in full
3668
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>}
3669
3796
  */
3670
3797
  update: (
3671
3798
  id,
@@ -3680,6 +3807,7 @@ class PlaidClient {
3680
3807
  /**
3681
3808
  * Delete a guideline.
3682
3809
  * @param {string} id - The guideline id
3810
+ * @param {string} [auditMessage] - Audit message for this write
3683
3811
  */
3684
3812
  delete: (id, auditMessage) =>
3685
3813
  this._request("DELETE", `/api/v1/guidelines/${id}`, { auditMessage }),
@@ -3694,6 +3822,7 @@ class PlaidClient {
3694
3822
  * @param {string} projectId - The project to read
3695
3823
  * @param {object} [opts]
3696
3824
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3825
+ * @returns {Promise<Guideline[]>}
3697
3826
  */
3698
3827
  list: (projectId, { includeBodies } = {}) =>
3699
3828
  listAll(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3706,7 +3835,7 @@ class PlaidClient {
3706
3835
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
3707
3836
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
3708
3837
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3709
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3838
+ * @returns {Promise<Page<Guideline>>}
3710
3839
  */
3711
3840
  listPage: (projectId, { limit, cursor, includeBodies } = {}) =>
3712
3841
  listPage(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3720,7 +3849,7 @@ class PlaidClient {
3720
3849
  * @param {object} [opts]
3721
3850
  * @param {number} [opts.pageSize] - Per-request page size
3722
3851
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3723
- * @returns {AsyncGenerator<Array>}
3852
+ * @returns {AsyncGenerator<Guideline[]>}
3724
3853
  */
3725
3854
  iterPages: (projectId, { pageSize, includeBodies } = {}) =>
3726
3855
  iterPages(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3732,6 +3861,7 @@ class PlaidClient {
3732
3861
 
3733
3862
  // --- Core methods ---
3734
3863
 
3864
+ /** @private */
3735
3865
  async _request(method, path, options = {}) {
3736
3866
  return makeRequest(this, method, path, options);
3737
3867
  }
@@ -3794,7 +3924,7 @@ class PlaidClient {
3794
3924
  * matching end, and the outer numbering resumes after it.
3795
3925
  *
3796
3926
  * @param {string} message - Human label for the operation.
3797
- * @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.
3798
3928
  * @returns {string} The operation's group id.
3799
3929
  */
3800
3930
  beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
@@ -3843,7 +3973,7 @@ class PlaidClient {
3843
3973
  * A seed for the Idempotency-Keys of a logical operation that may be run
3844
3974
  * again from the top (see beginOperation's `keys`). Keep it with the work
3845
3975
  * it belongs to and pass it to every run.
3846
- * @returns {{seed: string, stamps: Map<number, number|null>}}
3976
+ * @returns {KeySeed}
3847
3977
  */
3848
3978
  keySeed() {
3849
3979
  return { seed: uuidv7(), stamps: new Map() };
@@ -3898,10 +4028,11 @@ class PlaidClient {
3898
4028
  *
3899
4029
  * await client.withOperation('Import ELAN corpus', run, { kind: 'import', ref: 'format:elan' });
3900
4030
  *
4031
+ * @template T
3901
4032
  * @param {string} message - Human label for the operation.
3902
- * @param {function} fn - The work to run; receives `setMessage(msg)` to refine the label once the outcome is known.
3903
- * @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.
3904
- * @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.
3905
4036
  */
3906
4037
  async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
3907
4038
  this.beginOperation(message, { kind, ref, groupId, keys, minted });
@@ -3956,7 +4087,7 @@ class PlaidClient {
3956
4087
  * parents created earlier in the same batch, and any op's failure rolls the
3957
4088
  * whole batch back. A batch is not nestable. Prefer `batched()`, which
3958
4089
  * submits or aborts for you.
3959
- * @returns {PlaidClient} the batch view
4090
+ * @returns {PlaidBatch} the batch view
3960
4091
  */
3961
4092
  batch() {
3962
4093
  return openBatch(this);
@@ -3968,6 +4099,7 @@ class PlaidClient {
3968
4099
  * recased: the headers keep the server's spelling. `key` is the request's
3969
4100
  * Idempotency-Key: a batch whose answer is lost (no response, 502, 504) is
3970
4101
  * sent again under it, and answered from the first send if that landed.
4102
+ * @private
3971
4103
  */
3972
4104
  async _postBatch(url, body, { key = uuidv7() } = {}) {
3973
4105
  const fetchOptions = {
@@ -4100,8 +4232,8 @@ class PlaidClient {
4100
4232
  *
4101
4233
  * A write made on `client` inside `fn` is not part of the batch: it goes
4102
4234
  * over the wire at once, as it would anywhere else. See `batch()`.
4103
- * @param {(batch: PlaidClient) => (void | Promise<void>)} fn
4104
- * @returns {Promise<Array>}
4235
+ * @param {(batch: PlaidBatch) => (void | Promise<void>)} fn
4236
+ * @returns {Promise<any[]>}
4105
4237
  */
4106
4238
  async batched(fn) {
4107
4239
  const batch = this.batch();
@@ -4119,20 +4251,22 @@ class PlaidClient {
4119
4251
  * instance — for a launcher or status page checking whether a server is up
4120
4252
  * before anyone logs in.
4121
4253
  * @param {string} baseUrl - The API base URL
4122
- * @param {object} [options]
4254
+ * @param {{timeout?: number | null}} [options]
4255
+ * @returns {Promise<ServerHealth>}
4123
4256
  */
4124
4257
  static async health(baseUrl, options = {}) {
4125
- return anonymousGet(baseUrl, "/health", options);
4258
+ return anonymousRequest("GET", baseUrl, "/health", undefined, options);
4126
4259
  }
4127
4260
 
4128
4261
  /**
4129
4262
  * The limits this server enforces, with NO authentication and no client
4130
4263
  * instance. See the instance method for the shape.
4131
4264
  * @param {string} baseUrl - The API base URL
4132
- * @param {object} [options]
4265
+ * @param {{timeout?: number | null}} [options]
4266
+ * @returns {Promise<{limits: ServerLimits}>}
4133
4267
  */
4134
4268
  static async info(baseUrl, options = {}) {
4135
- return anonymousGet(baseUrl, "/api/v1/info", options);
4269
+ return anonymousRequest("GET", baseUrl, "/api/v1/info", undefined, options);
4136
4270
  }
4137
4271
 
4138
4272
  /**
@@ -4163,7 +4297,7 @@ class PlaidClient {
4163
4297
  * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
4164
4298
  */
4165
4299
  static async lookupInvite(baseUrl, code, options = {}) {
4166
- return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
4300
+ return anonymousRequest("POST", baseUrl, "/api/v1/invites/lookup", { code }, options);
4167
4301
  }
4168
4302
 
4169
4303
  /**
@@ -4192,10 +4326,16 @@ class PlaidClient {
4192
4326
  { email, password, displayName } = {},
4193
4327
  options = {},
4194
4328
  ) {
4195
- const data = await anonymousPost(
4329
+ const data = await anonymousRequest(
4330
+ "POST",
4196
4331
  baseUrl,
4197
4332
  "/api/v1/invites/redeem",
4198
- bodyOf({ code, email, password, "display-name": displayName }),
4333
+ bodyOf({
4334
+ code,
4335
+ email: normalizeUserId(email),
4336
+ password,
4337
+ "display-name": displayName,
4338
+ }),
4199
4339
  options,
4200
4340
  );
4201
4341
  return {
@@ -4220,35 +4360,14 @@ class PlaidClient {
4220
4360
  */
4221
4361
  static async login(baseUrl, userId, password, options = {}) {
4222
4362
  baseUrl = baseUrl.replace(/\/$/, "");
4223
- const url = `${baseUrl}/api/v1/login`;
4224
- try {
4225
- const fetchOptions = {
4226
- method: "POST",
4227
- headers: { "Content-Type": "application/json" },
4228
- body: JSON.stringify({ "user-id": userId, password }),
4229
- };
4230
- const signal = timeoutSignal(
4231
- options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
4232
- );
4233
- if (signal) fetchOptions.signal = signal;
4234
-
4235
- const response = await fetch(url, fetchOptions);
4236
- if (!response.ok) {
4237
- throw makeHttpError(
4238
- response,
4239
- await parseErrorBody(response),
4240
- url,
4241
- "POST",
4242
- );
4243
- }
4244
-
4245
- const data = await response.json();
4246
- const token = data.token || "";
4247
- return new PlaidClient(baseUrl, token, options);
4248
- } catch (error) {
4249
- if (error.status) throw error;
4250
- throw makeNetworkError(error, url, "POST");
4251
- }
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);
4252
4371
  }
4253
4372
  }
4254
4373
 
@@ -4276,9 +4395,129 @@ function configRequest(auditMessage, value, options, isDelete = false) {
4276
4395
  };
4277
4396
  }
4278
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
+
4279
4516
  /**
4280
4517
  * The four layer-constraint methods of a layer bundle, on `/api/v1/<kind>`.
4281
4518
  * See constraints.js and the core manual, "Layer constraints".
4519
+ * @param {PlaidClient} client
4520
+ * @param {string} kind
4282
4521
  */
4283
4522
  function constraintMethods(client, kind) {
4284
4523
  const base = (layerId) => `/api/v1/${kind}/${layerId}/constraints`;
@@ -4289,12 +4528,12 @@ function constraintMethods(client, kind) {
4289
4528
  * stored data breaks them (see violationsOf).
4290
4529
  * @param {string} layerId - The layer ID
4291
4530
  * @param {string} namespace - The declaring app's namespace, e.g. "igt"
4292
- * @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"]}]
4293
4532
  * @param {string} [auditMessage] - Audit message for this write
4294
4533
  * @param {{expected?: Array<object>|null}} [options] - `expected`: the list read for this
4295
4534
  * namespace (null when it was absent). The write is then refused with a 409 when the stored
4296
4535
  * list differs.
4297
- * @returns {Promise<{constraints: object}>} the layer's whole constraint map
4536
+ * @returns {Promise<{constraints: Record<string, LayerConstraint[]>}>} the layer's whole constraint map
4298
4537
  */
4299
4538
  setConstraints: (layerId, namespace, constraints, auditMessage, options) =>
4300
4539
  client._request("PUT", `${base(layerId)}/${namespace}`, {
@@ -4319,8 +4558,8 @@ function constraintMethods(client, kind) {
4319
4558
  * The violations the given constraints would meet in this layer's stored
4320
4559
  * data. Writes nothing.
4321
4560
  * @param {string} layerId - The layer ID
4322
- * @param {Array<object>} constraints - The list to check
4323
- * @returns {Promise<{violations: Array<object>, violationCount: number}>}
4561
+ * @param {LayerConstraint[]} constraints - The list to check
4562
+ * @returns {Promise<{violations: ConstraintViolation[], violationCount: number}>}
4324
4563
  */
4325
4564
  checkConstraints: (layerId, constraints) =>
4326
4565
  client._request("POST", `${base(layerId)}/check`, { body: { constraints } }),
@@ -4332,10 +4571,10 @@ function constraintMethods(client, kind) {
4332
4571
  * ask. A document another user holds the lock on is left as it is and
4333
4572
  * listed under `locked`.
4334
4573
  * @param {string} layerId - The layer ID
4335
- * @param {Array<object>} constraints - The list to repair for
4574
+ * @param {LayerConstraint[]} constraints - The list to repair for
4336
4575
  * @param {string} [auditMessage] - Audit message for this write
4337
4576
  * @param {{document?: string}} [options]
4338
- * @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}>}
4339
4578
  */
4340
4579
  repairConstraints: (layerId, constraints, auditMessage, options) =>
4341
4580
  client._request("POST", `${base(layerId)}/repair`, {
@@ -4357,6 +4596,7 @@ export {
4357
4596
  cpSlicer,
4358
4597
  utf16ToCp,
4359
4598
  cpIndexOf,
4599
+ composeText,
4360
4600
  } from "./codepoint.js";
4361
4601
  export { composeTextEdits, gapsToOps, applyTextOps } from "./textEdits.js";
4362
4602
  export {
@@ -4431,6 +4671,6 @@ export { createdId, createdIds } from "./created.js";
4431
4671
  // UUIDv7 ids for what a client creates. See ./ids.js and the manual,
4432
4672
  // "Retrying a write". uuidv4 works where crypto.randomUUID does not (a page
4433
4673
  // served over plain HTTP from any host but localhost).
4434
- export { uuidv4, uuidv7 } from "./ids.js";
4674
+ export { uuidv4, uuidv7, normalizeUserId } from "./ids.js";
4435
4675
  export { wasReplayed } from "./replayed.js";
4436
4676
  export { MAX_BATCH_OPS };