@larc-iu/plaid-client 0.2.0-alpha.23 → 0.2.0-alpha.29

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,662 @@ 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
+ * A value-set rule as `refusedValues` takes it: the list, the characters a
166
+ * value is split on, and whether every part (`"all"`, the default) or only
167
+ * the first must be listed.
168
+ * @typedef {{values: string[], delimiters?: string, parts?: "all" | "first"}} ValueSetRule
169
+ */
170
+
171
+ /**
172
+ * What a value-set rule would refuse in a layer's stored values: how many,
173
+ * how many distinct, and the most frequent with the parts not listed.
174
+ * @typedef {object} RefusedValues
175
+ * @property {number} count
176
+ * @property {number} valueCount
177
+ * @property {Array<{value: any, parts: string[], count: number}>} values
178
+ */
179
+
180
+ /**
181
+ * Entities that can carry a comment.
182
+ * @typedef {"document" | "text" | "token" | "span" | "relation" | "vocab-item"} CommentableType
183
+ */
184
+
185
+ /**
186
+ * One comment. `projectId` and `documentId` are null for a comment on a
187
+ * vocabulary entry, and `vocabLayerId` is set only for one. `anchorLabel` is
188
+ * what the comment is about, in words, captured when it was posted: a
189
+ * comment outlives its anchor. `edited` is true once the body has been
190
+ * rewritten since it was posted.
191
+ * @typedef {object} Comment
192
+ * @property {string} id
193
+ * @property {string | null} projectId
194
+ * @property {string | null} documentId
195
+ * @property {string | null} vocabLayerId
196
+ * @property {CommentableType} entityType
197
+ * @property {string} entityId
198
+ * @property {string | null} anchorLabel
199
+ * @property {string} authorId
200
+ * @property {string} body
201
+ * @property {string} createdAt
202
+ * @property {string} updatedAt
203
+ * @property {boolean} edited
204
+ */
205
+
206
+ /**
207
+ * One entry in a project's annotation manual. `body` is present on a single
208
+ * read and on a list made with `includeBodies`, `bodyChars` on a list made
209
+ * without. `pinned`: the assistant is given this one in full on every turn.
210
+ * @typedef {object} Guideline
211
+ * @property {string} id
212
+ * @property {string} projectId
213
+ * @property {string} title
214
+ * @property {string} [body]
215
+ * @property {number} [bodyChars]
216
+ * @property {boolean} pinned
217
+ * @property {string} createdAt
218
+ * @property {string} updatedAt
219
+ */
220
+
221
+ /**
222
+ * A research-telemetry event type.
223
+ * @typedef {"suggestion.shown" | "suggestion.adopted" | "suggestion.dismissed" | "plan.opened"} ClientEventType
224
+ */
225
+
226
+ /**
227
+ * One research-telemetry event as the server stores it.
228
+ * @typedef {object} ClientEvent
229
+ * @property {number} id
230
+ * @property {string} projectId
231
+ * @property {string | null} documentId
232
+ * @property {string} userId
233
+ * @property {ClientEventType} type
234
+ * @property {string | null} targetId
235
+ * @property {Record<string, any> | null} data
236
+ * @property {string | null} clientTs
237
+ * @property {string} ts
238
+ */
239
+
240
+ /**
241
+ * An invite. `kind` is "signup" or "password-reset", `status` "active",
242
+ * "used", "expired", "revoked" or "inactive".
243
+ * @typedef {object} Invite
244
+ * @property {string} id
245
+ * @property {string} kind
246
+ * @property {string} status
247
+ * @property {string} createdBy
248
+ * @property {string} createdAt
249
+ * @property {string} expiresAt
250
+ * @property {number} maxUses
251
+ * @property {number} uses
252
+ * @property {string | null} revokedAt
253
+ * @property {string | null} note
254
+ * @property {string | null} targetUserId
255
+ * @property {boolean} grantAdmin
256
+ * @property {string | null} projectId
257
+ * @property {string | null} projectRole
258
+ */
259
+
260
+ /**
261
+ * Server limits, as reported by /api/v1/info. An unset limit is absent.
262
+ * `passwordMinLength` is the shortest password the server accepts on any
263
+ * path, `lockExpirationMs` how long a document lock is held before it lapses.
264
+ * @typedef {object} ServerLimits
265
+ * @property {number} [mediaFileBytes]
266
+ * @property {number} [jsonBodyBytes]
267
+ * @property {number} [batchOperations]
268
+ * @property {number} [metadataDepth]
269
+ * @property {number} [metadataKeyCount]
270
+ * @property {number} [metadataStringLength]
271
+ * @property {number} [metadataTotalBytes]
272
+ * @property {number} [userDataValueBytes]
273
+ * @property {number} [guidelineTitleLength]
274
+ * @property {number} [guidelineBodyLength]
275
+ * @property {number} [commentBodyLength]
276
+ * @property {number} [passwordMinLength]
277
+ * @property {number} [lockExpirationMs]
278
+ */
279
+
280
+ /**
281
+ * @typedef {object} ServerHealth
282
+ * @property {boolean} ok
283
+ * @property {string} version
284
+ * @property {number} uptimeMs
285
+ * @property {{dbSizeMb?: number, auditRows?: number, error?: string}} audit
286
+ */
287
+
288
+ /**
289
+ * @typedef {{ip: string, userId?: string, failures: number, limit: number, blocked: boolean}} RateLimitBucket
290
+ */
291
+
292
+ /**
293
+ * @typedef {object} RateLimitSnapshot
294
+ * @property {number} windowMs
295
+ * @property {RateLimitBucket[]} logins
296
+ * @property {RateLimitBucket[]} ips
297
+ * @property {RateLimitBucket[]} invites
298
+ */
299
+
300
+ /**
301
+ * @typedef {object} BackupStatus
302
+ * @property {boolean} enabled
303
+ * @property {string} directory
304
+ * @property {number} retention
305
+ * @property {string} time
306
+ * @property {Array<{name: string, bytes: number, modified: string}>} backups
307
+ * @property {boolean} [ok]
308
+ */
309
+
310
+ /**
311
+ * @typedef {object} ServerReport
312
+ * @property {string} version
313
+ * @property {{uptimeMs: number, startedAt: string, java: string, heapUsed: number, heapMax: number}} jvm
314
+ * @property {{path: string | null, bytes: number | null, walBytes: number | null,
315
+ * slowQueryThresholdMs?: number, journalMode?: string, maxPoolSize?: number,
316
+ * tables: Record<string, number | null>}} database
317
+ * @property {{directory: string, files: number, bytes: number, orphans: number, orphanBytes: number}} media
318
+ * @property {BackupStatus} backup
319
+ * @property {Record<string, any>} settings
320
+ */
321
+
322
+ /**
323
+ * The live log. Requests and events are buffered separately, so a burst of
324
+ * requests cannot evict an error. In each, `matched` is how many passed the
325
+ * filters (which may exceed what `entries` holds) and `held` how many are
326
+ * buffered in total. `file` is the configured log file's absolute path, or
327
+ * null when logging to stdout.
328
+ * @typedef {object} LiveLog
329
+ * @property {{entries: object[], matched: number, held: number, capacity: number,
330
+ * stats: Record<string, number | null>}} requests
331
+ * @property {{entries: object[], matched: number, held: number, capacity: number,
332
+ * byLevel: Record<string, number>}} events
333
+ * @property {string | null} file
334
+ */
335
+
336
+ /**
337
+ * An open server-sent-events stream. Once CLOSED, `error` says why: null when
338
+ * this side closed it. A refused stream's error carries the HTTP `status`,
339
+ * and one the server ended is named "StreamClosed".
340
+ * @typedef {object} SSEConnection
341
+ * @property {() => void} close
342
+ * @property {() => SSEStats} getStats
343
+ * @property {number} readyState
344
+ * @property {(Error & {status?: number}) | null} error
345
+ */
346
+
347
+ /**
348
+ * How an SSE connection has fared so far. `events` counts what arrived by
349
+ * event type ("audit-log", "message", "heartbeat", "connected", "other").
350
+ * @typedef {object} SSEStats
351
+ * @property {number} durationSeconds
352
+ * @property {boolean} isConnected
353
+ * @property {boolean} isClosed
354
+ * @property {string | null} clientId
355
+ * @property {Record<string, number>} events
356
+ * @property {number} readyState
357
+ */
358
+
359
+ /**
360
+ * A value a span or relation holds: a JSON scalar.
361
+ * @typedef {string | number | boolean | null} AtomicValue
362
+ */
363
+
364
+ /**
365
+ * What a create answers: the new record's id.
366
+ * @typedef {{id: string}} Created
367
+ */
368
+
369
+ /**
370
+ * A project. `textLayers` nests its layers in their display order, each
371
+ * with the layers built on it, and `vocabs` lists the vocabularies linked to
372
+ * it.
373
+ * @typedef {object} Project
374
+ * @property {string} id
375
+ * @property {string} name
376
+ * @property {Record<string, any>} config
377
+ * @property {string[]} readers
378
+ * @property {string[]} writers
379
+ * @property {string[]} maintainers
380
+ * @property {ProjectTextLayer[]} textLayers
381
+ * @property {ProjectVocab[]} vocabs
382
+ */
383
+
384
+ /**
385
+ * @typedef {{id: string, name: string, config: Record<string, any>, tokenLayers: ProjectTokenLayer[]}} ProjectTextLayer
386
+ * @typedef {{id: string, name: string, config: Record<string, any>,
387
+ * constraints: Record<string, LayerConstraint[]>, spanLayers: ProjectSpanLayer[]}} ProjectTokenLayer
388
+ * @typedef {{id: string, name: string, config: Record<string, any>,
389
+ * constraints: Record<string, LayerConstraint[]>, relationLayers: ProjectRelationLayer[]}} ProjectSpanLayer
390
+ * @typedef {{id: string, name: string, config: Record<string, any>,
391
+ * constraints: Record<string, LayerConstraint[]>}} ProjectRelationLayer
392
+ * @typedef {{id: string, name: string, maintainers: string[], config: Record<string, any>}} ProjectVocab
393
+ */
394
+
395
+ /**
396
+ * A document. `version` rises by one on every write to it or its layers.
397
+ * A read with `includeBody` also carries `textLayers`, the document's
398
+ * annotation nested under each layer.
399
+ * @typedef {object} PlaidDocument
400
+ * @property {string} id
401
+ * @property {string} name
402
+ * @property {string} project
403
+ * @property {number} version
404
+ * @property {string} timeCreated
405
+ * @property {string} timeModified
406
+ * @property {Record<string, any>} [metadata]
407
+ * @property {DocumentTextLayer[]} [textLayers]
408
+ */
409
+
410
+ /**
411
+ * A text layer as a document read nests it: `text` is null where the
412
+ * document has no text in this layer.
413
+ * @typedef {object} DocumentTextLayer
414
+ * @property {string} id
415
+ * @property {string} name
416
+ * @property {Record<string, any>} config
417
+ * @property {{id: string, document: string, body: string, digest: string,
418
+ * metadata?: Record<string, any>} | null} text
419
+ * @property {DocumentTokenLayer[]} tokenLayers
420
+ */
421
+
422
+ /**
423
+ * @typedef {object} DocumentTokenLayer
424
+ * @property {string} id
425
+ * @property {string} name
426
+ * @property {Record<string, any>} config
427
+ * @property {Record<string, LayerConstraint[]>} constraints
428
+ * @property {string} overlapMode
429
+ * @property {string | null} parentTokenLayer
430
+ * @property {Array<{id: string, begin: number, end: number, precedence: number | null,
431
+ * metadata?: Record<string, any>}>} tokens
432
+ * @property {DocumentSpanLayer[]} spanLayers
433
+ * @property {Array<ProjectVocab & {vocabLinks: Array<{id: string, vocabItem: VocabItem,
434
+ * tokens: string[], metadata?: Record<string, any>}>}>} vocabs
435
+ */
436
+
437
+ /**
438
+ * @typedef {object} DocumentSpanLayer
439
+ * @property {string} id
440
+ * @property {string} name
441
+ * @property {Record<string, any>} config
442
+ * @property {Record<string, LayerConstraint[]>} constraints
443
+ * @property {Array<{id: string, value: AtomicValue, tokens: string[],
444
+ * metadata?: Record<string, any>}>} spans
445
+ * @property {Array<ProjectRelationLayer & {relations: Array<{id: string, source: string,
446
+ * target: string, value: AtomicValue, metadata?: Record<string, any>}>}>} relationLayers
447
+ */
448
+
449
+ /**
450
+ * A text: one document's body in one text layer. `digest` names the body,
451
+ * as an edit's `base`.
452
+ * @typedef {object} PlaidText
453
+ * @property {string} id
454
+ * @property {string} body
455
+ * @property {string} document
456
+ * @property {string} layer
457
+ * @property {string} digest
458
+ * @property {Record<string, any>} [metadata]
459
+ */
460
+
461
+ /**
462
+ * What a text write changed besides the body: the tokens whose extent
463
+ * moved (with `layer` and `text` on one the write made), the spans and
464
+ * vocabulary links whose tokens or value changed, and the ids of what it
465
+ * deleted.
466
+ * @typedef {object} TextReshape
467
+ * @property {Array<{id: string, begin: number, end: number, layer?: string, text?: string}>} tokens
468
+ * @property {Array<{id: string, tokens?: string[], value?: AtomicValue}>} spans
469
+ * @property {Array<{id: string, tokens: string[]}>} vocabLinks
470
+ * @property {{tokens: string[], spans: string[], relations: string[], vocabLinks: string[]}} deleted
471
+ */
472
+
473
+ /**
474
+ * A token. `value` is the text it covers.
475
+ * @typedef {object} Token
476
+ * @property {string} id
477
+ * @property {string} text
478
+ * @property {number} begin
479
+ * @property {number} end
480
+ * @property {string} layer
481
+ * @property {string} document
482
+ * @property {number | null} precedence
483
+ * @property {string} value
484
+ * @property {Record<string, any>} [metadata]
485
+ */
486
+
487
+ /**
488
+ * @typedef {object} Span
489
+ * @property {string} id
490
+ * @property {string} layer
491
+ * @property {string} document
492
+ * @property {AtomicValue} value
493
+ * @property {string[]} tokens
494
+ * @property {Record<string, any>} [metadata]
495
+ */
496
+
497
+ /**
498
+ * A relation, from the span `source` to the span `target`.
499
+ * @typedef {object} Relation
500
+ * @property {string} id
501
+ * @property {string} layer
502
+ * @property {string} document
503
+ * @property {string} source
504
+ * @property {string} target
505
+ * @property {AtomicValue} value
506
+ * @property {Record<string, any>} [metadata]
507
+ */
508
+
509
+ /**
510
+ * @typedef {object} VocabItem
511
+ * @property {string} id
512
+ * @property {string} layer
513
+ * @property {string} form
514
+ * @property {Record<string, any>} [metadata]
515
+ */
516
+
517
+ /**
518
+ * A link from a vocabulary entry to the tokens of one occurrence.
519
+ * @typedef {object} VocabLink
520
+ * @property {string} id
521
+ * @property {string} vocabItem
522
+ * @property {string} document
523
+ * @property {string[]} tokens
524
+ * @property {Record<string, any>} [metadata]
525
+ */
526
+
527
+ /**
528
+ * A vocabulary. `items` is there on a read made with `includeItems`.
529
+ * @typedef {object} VocabLayer
530
+ * @property {string} id
531
+ * @property {string} name
532
+ * @property {string} timeCreated
533
+ * @property {string} timeModified
534
+ * @property {Record<string, any>} config
535
+ * @property {string[]} maintainers
536
+ * @property {VocabItem[]} [items]
537
+ */
538
+
539
+ /**
540
+ * @typedef {{id: string, name: string, project: string, config: Record<string, any>}} TextLayer
541
+ * @typedef {{id: string, name: string, textLayer: string, project: string, overlapMode: string,
542
+ * parentTokenLayer: string | null, config: Record<string, any>,
543
+ * constraints: Record<string, LayerConstraint[]>}} TokenLayer
544
+ * @typedef {{id: string, name: string, tokenLayer: string, project: string, config: Record<string, any>,
545
+ * constraints: Record<string, LayerConstraint[]>}} SpanLayer
546
+ * @typedef {{id: string, name: string, spanLayer: string, project: string, config: Record<string, any>,
547
+ * constraints: Record<string, LayerConstraint[]>}} RelationLayer
548
+ */
549
+
550
+ /**
551
+ * The records with metadata, by the path segment of their routes.
552
+ * @typedef {{documents: PlaidDocument, texts: PlaidText, tokens: Token, spans: Span,
553
+ * relations: Relation, "vocab-items": VocabItem, "vocab-links": VocabLink}} MetadataRecords
554
+ */
555
+
556
+ /**
557
+ * The four layer kinds, by the path segment of their routes.
558
+ * @typedef {{"text-layers": TextLayer, "token-layers": TokenLayer, "span-layers": SpanLayer,
559
+ * "relation-layers": RelationLayer}} LayerRecords
560
+ */
561
+
562
+ /**
563
+ * @typedef {object} User
564
+ * @property {string} id
565
+ * @property {string} displayName
566
+ * @property {boolean} isAdmin
567
+ * @property {string | null} deactivatedAt
568
+ * @property {string | null} avatarHash
569
+ */
570
+
571
+ /**
572
+ * A project as the project list gives it: the project, with how many
573
+ * documents it has and when the newest of them last changed.
574
+ * @typedef {Project & {documentCount: number, lastModified: string | null}} ProjectListEntry
575
+ */
576
+
577
+ /**
578
+ * A document as a project's document list gives it.
579
+ * @typedef {object} DocumentListEntry
580
+ * @property {string} id
581
+ * @property {string} name
582
+ * @property {number} version
583
+ * @property {string} timeCreated
584
+ * @property {string} timeModified
585
+ */
586
+
587
+ /**
588
+ * A named API token, without the signed token itself, which only `create`
589
+ * returns.
590
+ * @typedef {object} ApiToken
591
+ * @property {string} id
592
+ * @property {string} userId
593
+ * @property {string} name
594
+ * @property {string} createdAt
595
+ * @property {string | null} lastUsedAt
596
+ * @property {string | null} revokedAt
597
+ */
598
+
599
+ /**
600
+ * Who made an operation, as the audit log names them.
601
+ * @typedef {{id: string, displayName: string, avatarHash: string | null}} AuditUser
602
+ */
603
+
604
+ /**
605
+ * A project or a document an audit entry names, while it exists.
606
+ * @typedef {{id: string, name: string}} AuditTarget
607
+ */
608
+
609
+ /**
610
+ * One operation of an audit entry.
611
+ * @typedef {object} AuditOp
612
+ * @property {string} id
613
+ * @property {string} type The kind of write, such as "document/update".
614
+ * @property {string} description
615
+ * @property {string} time
616
+ * @property {string} endTime The time to read at to see the operation done.
617
+ * @property {AuditUser | null} user
618
+ * @property {string} [credential]
619
+ * @property {AuditTarget} [project]
620
+ * @property {AuditTarget} [document]
621
+ * @property {string} [batchId]
622
+ */
623
+
624
+ /**
625
+ * One entry of an audit log: a logical operation, an atomic batch or a lone
626
+ * write, with its operations oldest first (`ops`, cut to `opsLimit` when one
627
+ * was asked for, while `opCount` counts them all).
628
+ * @typedef {object} AuditEntry
629
+ * @property {string} id
630
+ * @property {string} time
631
+ * @property {string} endTime
632
+ * @property {AuditUser | null} user
633
+ * @property {AuditTarget[]} projects
634
+ * @property {AuditTarget[]} documents
635
+ * @property {AuditOp[]} ops
636
+ * @property {number} opCount
637
+ * @property {string} [groupId]
638
+ * @property {string} [message]
639
+ * @property {OperationKind} [kind]
640
+ * @property {string} [ref]
641
+ * @property {string} [batchId]
642
+ * @property {{id: string, name: string}} [apiToken]
643
+ * @property {string} [credential]
644
+ */
645
+
646
+ /**
647
+ * A document lock as its holder sees it. `expiresAt` is in epoch
648
+ * milliseconds.
649
+ * @typedef {{lockId: string, userId: string, expiresAt: number}} LockGrant
650
+ */
651
+
652
+ /**
653
+ * What `documents.restore` did, or with `dryRun` would do: per kind of
654
+ * record, how many come back, are set back and are removed (per layer for
655
+ * the layered kinds), and what could not come back, with why.
656
+ * @typedef {object} DocumentRestoreSummary
657
+ * @property {boolean} name
658
+ * @property {boolean} documentMetadata
659
+ * @property {RestoreCounts} texts
660
+ * @property {RestoreCounts & {byLayer: Array<RestoreCounts & {layerId: string}>}} tokens
661
+ * @property {RestoreCounts & {byLayer: Array<RestoreCounts & {layerId: string}>}} spans
662
+ * @property {RestoreCounts & {byLayer: Array<RestoreCounts & {layerId: string}>}} relations
663
+ * @property {RestoreCounts} vocabLinks
664
+ * @property {Array<{kind: string, count: number, reason: string}>} skipped
665
+ * @property {{name: string, taken: string}} [nameKept] When the name the
666
+ * document had then belongs to another document now: the name it keeps,
667
+ * and that other document's name.
668
+ * @property {number} total 0 when nothing changes.
669
+ */
670
+
671
+ /** @typedef {{inserted: number, updated: number, deleted: number}} RestoreCounts */
672
+
673
+ /**
674
+ * A logical-operation group: its label, who made it and when, and the kind
675
+ * and reference of the operation when it has them.
676
+ * @typedef {object} OperationGroup
677
+ * @property {string} id
678
+ * @property {string | null} message
679
+ * @property {string} user
680
+ * @property {string} createdAt
681
+ * @property {string} [kind]
682
+ * @property {string} [ref]
683
+ */
684
+
685
+ /**
686
+ * A query's answer. For "ids" and "entities", one row of `results` per
687
+ * match, a cell per `columns` entry: an id, or the entity as its GET
688
+ * answers it. For "count", only the count. `truncated`: the rows or the
689
+ * count stopped at the server's cap.
690
+ * @typedef {object} QueryResult
691
+ * @property {string} return
692
+ * @property {number} count
693
+ * @property {boolean} truncated
694
+ * @property {string[]} [columns]
695
+ * @property {any[][]} [results]
696
+ */
697
+
42
698
  // Helper: normalize a document-read `layers` filter to the wire's
43
699
  // comma-separated form. Accepts an array of layer ids or a ready-made string;
44
700
  // anything empty becomes undefined so no `?layers=` is sent at all.
@@ -70,48 +726,22 @@ function bodyOf(obj) {
70
726
  return result;
71
727
  }
72
728
 
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 = {}) {
729
+ // A request with no Authorization header: login, invite lookup and redemption
730
+ // (POST, with a body), /health and /api/v1/info (GET). Deliberately does not
731
+ // go through PlaidClient._request: these are called before any client exists,
732
+ // which is the whole point.
733
+ async function anonymousRequest(method, baseUrl, path, body, options = {}) {
111
734
  const base = baseUrl.replace(/\/$/, "");
112
735
  const url = `${base}${path}`;
113
736
  try {
114
- const fetchOptions = { method: "GET" };
737
+ const fetchOptions =
738
+ method === "GET"
739
+ ? { method }
740
+ : {
741
+ method,
742
+ headers: { "Content-Type": "application/json" },
743
+ body: JSON.stringify(body),
744
+ };
115
745
  const signal = timeoutSignal(
116
746
  options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS,
117
747
  );
@@ -119,12 +749,12 @@ async function anonymousGet(baseUrl, path, options = {}) {
119
749
 
120
750
  const response = await fetch(url, fetchOptions);
121
751
  if (!response.ok) {
122
- throw makeHttpError(response, await parseErrorBody(response), url, "GET");
752
+ throw makeHttpError(response, await parseErrorBody(response), url, method);
123
753
  }
124
754
  return transformResponse(await response.json());
125
755
  } catch (error) {
126
756
  if (error.status) throw error;
127
- throw makeNetworkError(error, url, "GET");
757
+ throw makeNetworkError(error, url, method);
128
758
  }
129
759
  }
130
760
 
@@ -155,15 +785,24 @@ const OPERATION_KINDS = [
155
785
  */
156
786
  export const BACKUP_TIMEOUT_MS = 30 * 60 * 1000;
157
787
 
788
+ /** An avatar token is minted again once less than this is left of it. */
789
+ const AVATAR_TOKEN_RENEW_MS = 10 * 60 * 1000;
790
+ // An avatar token minted this recently is the renewal a caller asking for one
791
+ // wanted.
792
+ const AVATAR_TOKEN_FRESH_MS = 5 * 1000;
793
+
158
794
  /**
159
795
  * A batch is the client with a different `_request` (see `queueRequest`) and
160
796
  * its own copy of the bundles. Everything else resolves to the client through
161
797
  * the prototype: the token and base URL, strict mode and the document versions
162
798
  * it tracks, the open logical operation.
163
799
  */
164
- function openBatch(client) {
800
+ function openBatch(client, { timeout } = {}) {
165
801
  const batch = Object.create(client);
166
802
  batch.client = client;
803
+ // This batch's own timeout per request, in place of the client's
804
+ // `batchTimeout` (undefined: the client's).
805
+ batch.submitTimeout = timeout;
167
806
  batch.operations = [];
168
807
  // The document each queued op's strict-mode stamp is for (null for none),
169
808
  // and the logical operation it joined (null for none), index for index with
@@ -226,7 +865,15 @@ async function submitBatch(batch) {
226
865
  // needs to know. The same holds for `committedResults`.
227
866
  const results = [];
228
867
  try {
229
- await sendChunks(client, url, ops, stamps, results, groups);
868
+ await sendChunks(
869
+ client,
870
+ url,
871
+ ops,
872
+ stamps,
873
+ results,
874
+ groups,
875
+ batch.submitTimeout,
876
+ );
230
877
  } catch (error) {
231
878
  if (error && typeof error === "object") {
232
879
  error.committed = results.length;
@@ -243,7 +890,15 @@ function claimedVersion(path) {
243
890
  return v == null ? null : Number(v);
244
891
  }
245
892
 
246
- async function sendChunks(client, url, ops, stamps, results, groups = []) {
893
+ async function sendChunks(
894
+ client,
895
+ url,
896
+ ops,
897
+ stamps,
898
+ results,
899
+ groups = [],
900
+ timeout = undefined,
901
+ ) {
247
902
  // Every chunk's refs are checked before the first request goes, so a batch
248
903
  // whose refs cannot resolve writes nothing.
249
904
  const chunks = [];
@@ -281,7 +936,7 @@ async function sendChunks(client, url, ops, stamps, results, groups = []) {
281
936
  // its path now carries it.
282
937
  const j = stamps.slice(i, i + chunk.length).findIndex(Boolean);
283
938
  record(j < 0 ? null : claimedVersion(chunk[j].path));
284
- const answered = await client._postBatch(url, chunk, { key });
939
+ const answered = await client._postBatch(url, chunk, { key, timeout });
285
940
  results.push(...answered);
286
941
  if (wasReplayed(answered)) markReplayed(results);
287
942
  // The server took the chunk, so each operation it joined exists.
@@ -316,13 +971,12 @@ class PlaidClient {
316
971
  * Create a new PlaidClient instance
317
972
  * @param {string} baseUrl - The base URL for the API
318
973
  * @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)
974
+ * @param {PlaidClientOptions} [options] - Client options
322
975
  */
323
976
  constructor(baseUrl, token, options = {}) {
324
977
  this.baseUrl = baseUrl.replace(/\/$/, "");
325
978
  this.token = token;
979
+ /** @type {number | null} */
326
980
  this.timeout =
327
981
  options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT_MS;
328
982
  // Batch submissions get a longer budget than a single request. Aborting a
@@ -330,6 +984,7 @@ class PlaidClient {
330
984
  // holding the single SQLite write lock, so a client that gives up early
331
985
  // just adds a retry on top of a write that is still in progress. Falls
332
986
  // back to an explicit `timeout` when one was given.
987
+ /** @type {number | null} */
333
988
  this.batchTimeout =
334
989
  options.batchTimeout !== undefined
335
990
  ? options.batchTimeout
@@ -338,33 +993,90 @@ class PlaidClient {
338
993
  : DEFAULT_BATCH_TIMEOUT_MS;
339
994
  // Delays (ms) before sending a keyed write again when its answer was lost
340
995
  // (no response, 502, 504). See the Idempotency-Key note in http.js.
996
+ /** @type {number[] | undefined} */
341
997
  this.retryDelaysMs = options.retryDelaysMs;
998
+ /**
999
+ * The latest version seen for each document written through this client,
1000
+ * keyed by document id.
1001
+ * @type {Record<string, number>}
1002
+ */
342
1003
  this.documentVersions = {};
1004
+ /**
1005
+ * True once a response left its version list out
1006
+ * (X-Document-Versions-Omitted, past fifty documents). Every version held
1007
+ * was then forgotten, and the strict-mode document's is read again before
1008
+ * the next write.
1009
+ * @type {boolean | undefined}
1010
+ */
1011
+ this.documentVersionsOmitted = undefined;
343
1012
  // The server's clock minus this machine's, in ms, from the last response
344
1013
  // with a Date header (null before one). See serverNow().
1014
+ /** @type {number | null} */
345
1015
  this.serverClockOffsetMs = null;
1016
+ /**
1017
+ * The document strict mode is entered for, or null.
1018
+ * @type {string | null}
1019
+ */
346
1020
  this.strictModeDocumentId = null;
347
1021
  // Set to a DocumentLockLost while a `documents.locked()` block's keep-alive
348
1022
  // has failed. Every write throws it until the block exits; see
349
1023
  // documentLock.js.
1024
+ /** @type {import("./documentLock.js").DocumentLockLost | null} */
350
1025
  this.documentLockLost = null;
351
1026
  // The open logical operation (audit-log group), or null. While set, every
352
1027
  // write is stamped with `?group-id=` (+ `group-message`) so the audit log
353
1028
  // folds them into ONE expandable entry. See beginOperation / withOperation.
354
1029
  // Shape: { id, message, depth, written, refined, frames }, where each of
355
1030
  // `frames` is { keys, count, minted, depth, owned } and the last numbers.
1031
+ /**
1032
+ * @type {{id: string, message: string | null, kind: OperationKind | null,
1033
+ * ref: string | null, depth: number, written: boolean, refined: any,
1034
+ * frames: object[]} | null}
1035
+ */
356
1036
  this.operationGroup = null;
357
1037
  // Optional callback fired once (per client) when any request returns HTTP
358
1038
  // 401 — i.e. the token is missing/expired/invalid. Apps use it to discard
359
1039
  // the stored token and route back to login. See makeRequest in http.js.
1040
+ /** @type {((error: Error) => void) | null} */
360
1041
  this.onAuthError = options.onAuthError || null;
1042
+ /** @private */
361
1043
  this._authErrorFired = false;
362
1044
  // Who hears that a request is being sent again (onRetry).
1045
+ /** @private */
363
1046
  this._retryListeners = new Set();
1047
+ // GET /info as last read (`server.info()`), and whether a stream of this
1048
+ // client dropped since.
1049
+ /** @private */
1050
+ this._serverFacts = { info: null, stale: false };
1051
+ // The key frame the last beginOperation opened, or null (withOperation).
1052
+ /** @private @type {object | null} */
1053
+ this._openedFrame = null;
364
1054
 
365
1055
  this._installResources();
366
1056
  }
367
1057
 
1058
+ /**
1059
+ * A stream of this client ended without being closed here: the server may
1060
+ * have restarted, with other limits.
1061
+ * @private
1062
+ */
1063
+ _noteStreamDropped() {
1064
+ this._serverFacts.stale = true;
1065
+ }
1066
+
1067
+ /**
1068
+ * A stream of this client is open. After a drop the limits are read again,
1069
+ * once however many streams reopen.
1070
+ * @private
1071
+ */
1072
+ _noteStreamOpened() {
1073
+ if (!this._serverFacts.stale) return;
1074
+ this.server.refresh().catch(() => {
1075
+ // The figures known before stand, and the next open asks again.
1076
+ this._serverFacts.stale = true;
1077
+ });
1078
+ }
1079
+
368
1080
  /**
369
1081
  * Hear every time a request is sent again: after a 503 (the database was
370
1082
  * busy), or for a keyed write after its answer was lost (no response, 502,
@@ -379,7 +1091,10 @@ class PlaidClient {
379
1091
  return () => this._retryListeners.delete(listener);
380
1092
  }
381
1093
 
382
- /** Tell every `onRetry` listener. A listener that throws is passed over. */
1094
+ /**
1095
+ * Tell every `onRetry` listener. A listener that throws is passed over.
1096
+ * @private
1097
+ */
383
1098
  _noteRetry(info) {
384
1099
  for (const listener of this._retryListeners) {
385
1100
  try {
@@ -406,6 +1121,7 @@ class PlaidClient {
406
1121
  * the client, and again on every batch it opens (see `batch()`): a batch is
407
1122
  * a view of the client with its own `_request`, so the same bundles built
408
1123
  * on it queue instead of sending.
1124
+ * @private
409
1125
  */
410
1126
  _installResources() {
411
1127
  this.vocabLinks = {
@@ -414,6 +1130,10 @@ class PlaidClient {
414
1130
  * @param {string} vocabItem - The vocab item to link
415
1131
  * @param {Array} tokens - The tokens to link
416
1132
  * @param {any} [metadata] - Metadata for the link. Omit to leave unset; pass null to send JSON null.
1133
+ * @param {string} [auditMessage] - Audit message for this write
1134
+ * @param {object} [opts]
1135
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
1136
+ * @returns {Promise<Created>}
417
1137
  */
418
1138
  create: (vocabItem, tokens, metadata, auditMessage, { id } = {}) =>
419
1139
  this._request("POST", "/api/v1/vocab-links", {
@@ -425,6 +1145,7 @@ class PlaidClient {
425
1145
  * reference different vocab items, but all tokens across the call must
426
1146
  * belong to one document.
427
1147
  * @param {Array<{vocabItem: string, tokens: string[], metadata?: any}>} body - The vocab links to create
1148
+ * @param {string} [auditMessage] - Audit message for this write
428
1149
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
429
1150
  */
430
1151
  bulkCreate: (body, auditMessage) =>
@@ -435,49 +1156,27 @@ class PlaidClient {
435
1156
  /**
436
1157
  * Delete multiple vocab links in a single operation. Provide an array of IDs.
437
1158
  * @param {string[]} body - The vocab link IDs to delete
1159
+ * @param {string} [auditMessage] - Audit message for this write
1160
+ * @returns {Promise<void>}
438
1161
  */
439
1162
  bulkDelete: (body, auditMessage) =>
440
1163
  this._request("DELETE", "/api/v1/vocab-links/bulk", {
441
1164
  auditMessage,
442
1165
  body,
443
1166
  }),
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
- }),
1167
+ ...metadataMethods(this, "vocab-links"),
472
1168
  /**
473
1169
  * Get a vocab link by ID
474
1170
  * @param {string} id - The resource ID
1171
+ * @returns {Promise<VocabLink>}
475
1172
  */
476
1173
  get: (id) =>
477
1174
  this._request("GET", `/api/v1/vocab-links/${id}`),
478
1175
  /**
479
1176
  * Delete a vocab link
480
1177
  * @param {string} id - The resource ID
1178
+ * @param {string} [auditMessage] - Audit message for this write
1179
+ * @returns {Promise<void>}
481
1180
  */
482
1181
  delete: (id, auditMessage) =>
483
1182
  this._request("DELETE", `/api/v1/vocab-links/${id}`, { auditMessage }),
@@ -492,6 +1191,7 @@ class PlaidClient {
492
1191
  * @param {string} id - The resource ID
493
1192
  * @param {boolean} [includeItems] - Include vocab items
494
1193
  * @param {string} [asOf] - The moment to read at (ISO-8601 instant)
1194
+ * @returns {Promise<VocabLayer>}
495
1195
  */
496
1196
  get: (id, includeItems, asOf) =>
497
1197
  this._request("GET", `/api/v1/vocab-layers/${id}`, {
@@ -504,6 +1204,7 @@ class PlaidClient {
504
1204
  * @param {string} id - The vocabulary ID
505
1205
  * @param {string} itemId - The entry ID
506
1206
  * @param {string} asOf - The moment to read at (ISO-8601 instant)
1207
+ * @returns {Promise<VocabItem>}
507
1208
  */
508
1209
  getItemAt: (id, itemId, asOf) =>
509
1210
  this._request("GET", `/api/v1/vocab-layers/${id}/items/${itemId}`, {
@@ -524,6 +1225,7 @@ class PlaidClient {
524
1225
  * each with only its operations that did
525
1226
  * @param {string[]|string} [kinds] - Only the entries of operations of
526
1227
  * these kinds (e.g. `['review', 'guess-adoption']`), each whole
1228
+ * @returns {Promise<AuditEntry[]>}
527
1229
  */
528
1230
  audit: (id, startTime, endTime, opTypes, itemId, kinds) =>
529
1231
  listAll(this, `/api/v1/vocab-layers/${id}/audit`, {
@@ -541,6 +1243,10 @@ class PlaidClient {
541
1243
  * that may be long: audit() walks every page before it resolves.
542
1244
  * @param {string} id - The vocabulary ID
543
1245
  * @param {object} [opts]
1246
+ * @param {string} [opts.startTime] - Only entries at or after this instant
1247
+ * @param {string} [opts.endTime] - Only entries at or before this instant
1248
+ * @param {string[]|string} [opts.opTypes] - Only these op types
1249
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
544
1250
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
545
1251
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
546
1252
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -550,7 +1256,7 @@ class PlaidClient {
550
1256
  * @param {string} [opts.entryId] - Read only this entry, not a page
551
1257
  * @param {string} [opts.itemId] - Only the changes that wrote this one
552
1258
  * entry, each with only its operations that did
553
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1259
+ * @returns {Promise<Page<AuditEntry>>}
554
1260
  */
555
1261
  auditPage: (
556
1262
  id,
@@ -586,9 +1292,10 @@ class PlaidClient {
586
1292
  * @param {string} itemId - The entry ID
587
1293
  * @param {string} asOf - The moment to go back to (ISO-8601 instant),
588
1294
  * typically a history entry's `endTime`
589
- * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
1295
+ * @param {{dryRun?: boolean}} [options] - `{ dryRun }`: with `dryRun` true nothing
590
1296
  * is written and the summary says what would change
591
1297
  * @param {string} [auditMessage] - Custom audit message for this operation
1298
+ * @returns {Promise<VocabItemRestoreSummary>}
592
1299
  */
593
1300
  restoreItem: (id, itemId, asOf, { dryRun } = {}, auditMessage) =>
594
1301
  this._request(
@@ -605,6 +1312,8 @@ class PlaidClient {
605
1312
  /**
606
1313
  * Delete a vocab layer.
607
1314
  * @param {string} id - The resource ID
1315
+ * @param {string} [auditMessage] - Audit message for this write
1316
+ * @returns {Promise<void>}
608
1317
  */
609
1318
  delete: (id, auditMessage) =>
610
1319
  this._request("DELETE", `/api/v1/vocab-layers/${id}`, { auditMessage }),
@@ -612,45 +1321,19 @@ class PlaidClient {
612
1321
  * Update a vocab layer's name.
613
1322
  * @param {string} id - The resource ID
614
1323
  * @param {string} name - The name
1324
+ * @param {string} [auditMessage] - Audit message for this write
1325
+ * @returns {Promise<VocabLayer>}
615
1326
  */
616
1327
  update: (id, name, auditMessage) =>
617
1328
  this._request("PATCH", `/api/v1/vocab-layers/${id}`, {
618
1329
  auditMessage,
619
1330
  body: bodyOf({ name }),
620
1331
  }),
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
- ),
1332
+ ...configMethods(this, "vocab-layers"),
651
1333
  /**
652
1334
  * List all vocab layers accessible to user. Transparently follows
653
1335
  * pagination cursors and returns the full flat array.
1336
+ * @returns {Promise<VocabLayer[]>}
654
1337
  */
655
1338
  list: () => listAll(this, "/api/v1/vocab-layers"),
656
1339
  /**
@@ -658,7 +1341,7 @@ class PlaidClient {
658
1341
  * @param {object} [opts]
659
1342
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
660
1343
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
661
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1344
+ * @returns {Promise<Page<VocabLayer>>}
662
1345
  */
663
1346
  listPage: ({ limit, cursor } = {}) =>
664
1347
  listPage(this, "/api/v1/vocab-layers", {
@@ -669,7 +1352,7 @@ class PlaidClient {
669
1352
  * Async-iterate vocab layers page by page; yields each page's entries array.
670
1353
  * @param {object} [opts]
671
1354
  * @param {number} [opts.pageSize] - Per-request page size
672
- * @returns {AsyncGenerator<Array>}
1355
+ * @returns {AsyncGenerator<VocabLayer[]>}
673
1356
  */
674
1357
  iterPages: ({ pageSize } = {}) =>
675
1358
  iterPages(this, "/api/v1/vocab-layers", {
@@ -678,6 +1361,10 @@ class PlaidClient {
678
1361
  /**
679
1362
  * Create a new vocab layer. Note: this also registers the user as a maintainer.
680
1363
  * @param {string} name - The name
1364
+ * @param {string} [auditMessage] - Audit message for this write
1365
+ * @param {object} [opts]
1366
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
1367
+ * @returns {Promise<Created>}
681
1368
  */
682
1369
  create: (name, auditMessage, { id } = {}) =>
683
1370
  this._request("POST", "/api/v1/vocab-layers", {
@@ -688,6 +1375,8 @@ class PlaidClient {
688
1375
  * Assign a user as a maintainer for this vocab layer.
689
1376
  * @param {string} id - The resource ID
690
1377
  * @param {string} userId - The user ID
1378
+ * @param {string} [auditMessage] - Audit message for this write
1379
+ * @returns {Promise<void>}
691
1380
  */
692
1381
  addMaintainer: (id, userId, auditMessage) =>
693
1382
  this._request(
@@ -699,6 +1388,8 @@ class PlaidClient {
699
1388
  * Remove a user's maintainer privileges for this vocab layer.
700
1389
  * @param {string} id - The resource ID
701
1390
  * @param {string} userId - The user ID
1391
+ * @param {string} [auditMessage] - Audit message for this write
1392
+ * @returns {Promise<void>}
702
1393
  */
703
1394
  removeMaintainer: (id, userId, auditMessage) =>
704
1395
  this._request(
@@ -709,38 +1400,13 @@ class PlaidClient {
709
1400
  };
710
1401
 
711
1402
  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
- }),
1403
+ ...metadataMethods(this, "relations"),
740
1404
  /**
741
1405
  * Update the target span of a relation.
742
1406
  * @param {string} relationId - The relation ID
743
1407
  * @param {string} spanId - The span ID
1408
+ * @param {string} [auditMessage] - Audit message for this write
1409
+ * @returns {Promise<Relation>}
744
1410
  */
745
1411
  setTarget: (relationId, spanId, auditMessage) =>
746
1412
  this._request("PUT", `/api/v1/relations/${relationId}/target`, {
@@ -750,12 +1416,15 @@ class PlaidClient {
750
1416
  /**
751
1417
  * Get a relation by ID.
752
1418
  * @param {string} relationId - The relation ID
1419
+ * @returns {Promise<Relation>}
753
1420
  */
754
1421
  get: (relationId) =>
755
1422
  this._request("GET", `/api/v1/relations/${relationId}`),
756
1423
  /**
757
1424
  * Delete a relation.
758
1425
  * @param {string} relationId - The relation ID
1426
+ * @param {string} [auditMessage] - Audit message for this write
1427
+ * @returns {Promise<void>}
759
1428
  */
760
1429
  delete: (relationId, auditMessage) =>
761
1430
  this._request("DELETE", `/api/v1/relations/${relationId}`, {
@@ -765,6 +1434,8 @@ class PlaidClient {
765
1434
  * Update a relation's value.
766
1435
  * @param {string} relationId - The relation ID
767
1436
  * @param {any} value - The value
1437
+ * @param {string} [auditMessage] - Audit message for this write
1438
+ * @returns {Promise<Relation>}
768
1439
  */
769
1440
  update: (relationId, value, auditMessage) =>
770
1441
  this._request("PATCH", `/api/v1/relations/${relationId}`, {
@@ -775,6 +1446,8 @@ class PlaidClient {
775
1446
  * Update the source span of a relation.
776
1447
  * @param {string} relationId - The relation ID
777
1448
  * @param {string} spanId - The span ID
1449
+ * @param {string} [auditMessage] - Audit message for this write
1450
+ * @returns {Promise<Relation>}
778
1451
  */
779
1452
  setSource: (relationId, spanId, auditMessage) =>
780
1453
  this._request("PUT", `/api/v1/relations/${relationId}/source`, {
@@ -785,16 +1458,20 @@ class PlaidClient {
785
1458
  * Create a new relation. A relation is a directed edge between two spans
786
1459
  * with a value, useful for expressing phenomena such as syntactic or
787
1460
  * semantic relations.
788
- * @param {string} layerId - The relation layer ID
789
- * @param {string} sourceId - The source span ID
790
- * @param {string} targetId - The target span ID
1461
+ * @param {string} relationLayerId - The relation layer ID
1462
+ * @param {string} source - The source span ID
1463
+ * @param {string} target - The target span ID
791
1464
  * @param {any} value - The value
792
1465
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1466
+ * @param {string} [auditMessage] - Audit message for this write
1467
+ * @param {object} [opts]
1468
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
1469
+ * @returns {Promise<Created>}
793
1470
  */
794
1471
  create: (
795
- layerId,
796
- sourceId,
797
- targetId,
1472
+ relationLayerId,
1473
+ source,
1474
+ target,
798
1475
  value,
799
1476
  metadata,
800
1477
  auditMessage,
@@ -804,9 +1481,9 @@ class PlaidClient {
804
1481
  auditMessage,
805
1482
  body: bodyOf({
806
1483
  id,
807
- "layer-id": layerId,
808
- "source-id": sourceId,
809
- "target-id": targetId,
1484
+ "relation-layer-id": relationLayerId,
1485
+ source,
1486
+ target,
810
1487
  value,
811
1488
  metadata,
812
1489
  }),
@@ -814,6 +1491,7 @@ class PlaidClient {
814
1491
  /**
815
1492
  * Create multiple relations in a single operation.
816
1493
  * @param {Array} body - The request body
1494
+ * @param {string} [auditMessage] - Audit message for this write
817
1495
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
818
1496
  */
819
1497
  bulkCreate: (body, auditMessage) =>
@@ -821,6 +1499,8 @@ class PlaidClient {
821
1499
  /**
822
1500
  * Delete multiple relations in a single operation. Provide an array of IDs.
823
1501
  * @param {Array} body - The request body
1502
+ * @param {string} [auditMessage] - Audit message for this write
1503
+ * @returns {Promise<void>}
824
1504
  */
825
1505
  bulkDelete: (body, auditMessage) =>
826
1506
  this._request("DELETE", "/api/v1/relations/bulk", {
@@ -830,6 +1510,7 @@ class PlaidClient {
830
1510
  /**
831
1511
  * Update many relations in a single operation: set values and/or patch metadata.
832
1512
  * @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.
1513
+ * @param {string} [auditMessage] - Audit message for this write
833
1514
  * @returns {Promise<{count: number}>} How many relations were updated.
834
1515
  */
835
1516
  bulkUpdate: (body, auditMessage) =>
@@ -840,81 +1521,24 @@ class PlaidClient {
840
1521
  };
841
1522
 
842
1523
  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
- ),
1524
+ ...configMethods(this, "span-layers"),
873
1525
  ...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
- }),
1526
+ ...valueSetMethods(this, "span-layers"),
1527
+ ...layerMethods(this, "span-layers"),
898
1528
  /**
899
1529
  * Create a new span layer.
900
1530
  * @param {string} tokenLayerId - The token layer ID
901
1531
  * @param {string} name - The name
1532
+ * @param {string} [auditMessage] - Audit message for this write
1533
+ * @param {object} [opts]
1534
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
1535
+ * @returns {Promise<Created>}
902
1536
  */
903
1537
  create: (tokenLayerId, name, auditMessage, { id } = {}) =>
904
1538
  this._request("POST", "/api/v1/span-layers", {
905
1539
  auditMessage,
906
1540
  body: bodyOf({ id, "token-layer-id": tokenLayerId, name }),
907
1541
  }),
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
1542
  };
919
1543
 
920
1544
  this.spans = {
@@ -922,6 +1546,8 @@ class PlaidClient {
922
1546
  * Replace tokens for a span.
923
1547
  * @param {string} spanId - The span ID
924
1548
  * @param {Array} tokens - The tokens
1549
+ * @param {string} [auditMessage] - Audit message for this write
1550
+ * @returns {Promise<Span>}
925
1551
  */
926
1552
  setTokens: (spanId, tokens, auditMessage) =>
927
1553
  this._request("PUT", `/api/v1/spans/${spanId}/tokens`, {
@@ -935,6 +1561,10 @@ class PlaidClient {
935
1561
  * @param {Array} tokens - The tokens
936
1562
  * @param {any} value - The value
937
1563
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1564
+ * @param {string} [auditMessage] - Audit message for this write
1565
+ * @param {object} [opts]
1566
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
1567
+ * @returns {Promise<Created>}
938
1568
  */
939
1569
  create: (spanLayerId, tokens, value, metadata, auditMessage, { id } = {}) =>
940
1570
  this._request("POST", "/api/v1/spans", {
@@ -950,12 +1580,15 @@ class PlaidClient {
950
1580
  /**
951
1581
  * Get a span by ID.
952
1582
  * @param {string} spanId - The span ID
1583
+ * @returns {Promise<Span>}
953
1584
  */
954
1585
  get: (spanId) =>
955
1586
  this._request("GET", `/api/v1/spans/${spanId}`),
956
1587
  /**
957
1588
  * Delete a span.
958
1589
  * @param {string} spanId - The span ID
1590
+ * @param {string} [auditMessage] - Audit message for this write
1591
+ * @returns {Promise<void>}
959
1592
  */
960
1593
  delete: (spanId, auditMessage) =>
961
1594
  this._request("DELETE", `/api/v1/spans/${spanId}`, { auditMessage }),
@@ -963,6 +1596,8 @@ class PlaidClient {
963
1596
  * Update a span's value.
964
1597
  * @param {string} spanId - The span ID
965
1598
  * @param {any} value - The value
1599
+ * @param {string} [auditMessage] - Audit message for this write
1600
+ * @returns {Promise<Span>}
966
1601
  */
967
1602
  update: (spanId, value, auditMessage) =>
968
1603
  this._request("PATCH", `/api/v1/spans/${spanId}`, {
@@ -972,6 +1607,7 @@ class PlaidClient {
972
1607
  /**
973
1608
  * Create multiple spans in a single operation.
974
1609
  * @param {Array} body - The request body
1610
+ * @param {string} [auditMessage] - Audit message for this write
975
1611
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
976
1612
  */
977
1613
  bulkCreate: (body, auditMessage) =>
@@ -979,12 +1615,15 @@ class PlaidClient {
979
1615
  /**
980
1616
  * Delete multiple spans in a single operation. Provide an array of IDs.
981
1617
  * @param {Array} body - The request body
1618
+ * @param {string} [auditMessage] - Audit message for this write
1619
+ * @returns {Promise<void>}
982
1620
  */
983
1621
  bulkDelete: (body, auditMessage) =>
984
1622
  this._request("DELETE", "/api/v1/spans/bulk", { auditMessage, body }),
985
1623
  /**
986
1624
  * Update many spans in a single operation: set values and/or patch metadata.
987
1625
  * @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.
1626
+ * @param {string} [auditMessage] - Audit message for this write
988
1627
  * @returns {Promise<{count: number}>} How many spans were updated.
989
1628
  */
990
1629
  bulkUpdate: (body, auditMessage) =>
@@ -992,64 +1631,71 @@ class PlaidClient {
992
1631
  auditMessage,
993
1632
  body,
994
1633
  }),
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
- }),
1634
+ ...metadataMethods(this, "spans"),
1023
1635
  };
1024
1636
 
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;
1637
+ // Server-level facts. Read once, and again only when they may have
1638
+ // changed: after this client's connection to the server was lost and came
1639
+ // back (a restart, the only way the limits change, always drops it), or
1640
+ // after the server refused a private data write as too large. A caller
1641
+ // asking "will this file be accepted" does not pay a round trip to find
1642
+ // out. The facts live on the client (`_serverFacts`), so a batch opened
1643
+ // on it reads the same.
1029
1644
  this.server = {
1030
1645
  /**
1031
1646
  * This server's version and the limits it enforces, e.g.
1032
1647
  * `{ limits: { mediaFileBytes, jsonBodyBytes, batchOperations, … } }`.
1033
1648
  * Sizes are in bytes. Unauthenticated.
1649
+ * @returns {Promise<{limits: ServerLimits}>}
1034
1650
  */
1035
1651
  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
1652
+ const facts = this._serverFacts;
1653
+ if (!facts.info) {
1654
+ const asked = this._request("GET", "/api/v1/info").catch((err) => {
1655
+ // A failure must not be kept: a client that starts before the
1039
1656
  // server is up would never see the limits at all.
1040
- infoPromise = null;
1657
+ if (facts.info === asked) facts.info = null;
1041
1658
  throw err;
1042
1659
  });
1660
+ facts.info = asked;
1043
1661
  }
1044
- return infoPromise;
1662
+ return facts.info;
1663
+ },
1664
+ /**
1665
+ * Read `GET /info` again and resolve to it. Until it answers, a reader
1666
+ * waits for it. The facts known before are kept when the server does
1667
+ * not answer, and the error is thrown.
1668
+ * @returns {Promise<{limits: ServerLimits}>}
1669
+ */
1670
+ refresh: () => {
1671
+ const facts = this._serverFacts;
1672
+ const before = facts.info;
1673
+ facts.stale = false;
1674
+ const fresh = this._request("GET", "/api/v1/info");
1675
+ const held = fresh.catch((err) => {
1676
+ if (facts.info === held) facts.info = before;
1677
+ if (before) return before;
1678
+ throw err;
1679
+ });
1680
+ held.catch(() => {});
1681
+ facts.info = held;
1682
+ return fresh;
1683
+ },
1684
+ /**
1685
+ * Just the limits, which is what a caller almost always wants.
1686
+ * @returns {Promise<ServerLimits>}
1687
+ */
1688
+ limits: async () => {
1689
+ // Through `self`: tsc types a bundle that names itself as
1690
+ // `this.server` in its own methods `any` in the declarations.
1691
+ const self = this;
1692
+ return (await self.server.info()).limits;
1045
1693
  },
1046
- /** Just the limits, which is what a caller almost always wants. */
1047
- limits: async () => (await this.server.info()).limits,
1048
1694
  /**
1049
1695
  * Liveness, version and database size. Unauthenticated, and served
1050
1696
  * outside the REST router at /health, so it answers even while the API
1051
1697
  * is refusing requests. Never cached — the point is that it is current.
1052
- * @returns {Promise<{ok: boolean, version: string, uptimeMs: number, audit: object}>}
1698
+ * @returns {Promise<ServerHealth>}
1053
1699
  */
1054
1700
  health: () => PlaidClient.health(this.baseUrl, { timeout: this.timeout }),
1055
1701
  };
@@ -1073,34 +1719,7 @@ class PlaidClient {
1073
1719
  };
1074
1720
 
1075
1721
  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
- }),
1722
+ ...metadataMethods(this, "texts"),
1104
1723
  /**
1105
1724
  * Create a new text in a document's text layer. A text is a container for
1106
1725
  * one long string in `body` for a given layer.
@@ -1108,6 +1727,14 @@ class PlaidClient {
1108
1727
  * @param {string} documentId - The document ID
1109
1728
  * @param {string} body - The request body
1110
1729
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
1730
+ * @param {string} [auditMessage]
1731
+ * @param {{id?: string, tokenEdges?: number[], tokenizeLayer?: string}} [options] - `id`: the
1732
+ * new text's id, a UUIDv7 this client minted. `tokenEdges`: the code-point offsets where
1733
+ * the tokens made next on this text begin and end. The body is stored composed, except a
1734
+ * character one of them falls inside, which stays as sent (see composeText).
1735
+ * `tokenizeLayer`: a word layer of the text, which gets the body's words, cut by the
1736
+ * server ("Tokenize new text").
1737
+ * @returns {Promise<Created>}
1111
1738
  */
1112
1739
  create: (
1113
1740
  textLayerId,
@@ -1115,7 +1742,7 @@ class PlaidClient {
1115
1742
  body,
1116
1743
  metadata,
1117
1744
  auditMessage,
1118
- { id } = {},
1745
+ { id, tokenEdges, tokenizeLayer } = {},
1119
1746
  ) =>
1120
1747
  this._request("POST", "/api/v1/texts", {
1121
1748
  auditMessage,
@@ -1124,18 +1751,23 @@ class PlaidClient {
1124
1751
  "text-layer-id": textLayerId,
1125
1752
  "document-id": documentId,
1126
1753
  body,
1754
+ "token-edges": tokenEdges,
1755
+ "tokenize-layer": tokenizeLayer,
1127
1756
  metadata,
1128
1757
  }),
1129
1758
  }),
1130
1759
  /**
1131
1760
  * Get a text.
1132
1761
  * @param {string} textId - The text ID
1762
+ * @returns {Promise<PlaidText>}
1133
1763
  */
1134
1764
  get: (textId) =>
1135
1765
  this._request("GET", `/api/v1/texts/${textId}`),
1136
1766
  /**
1137
1767
  * Delete a text and all dependent data.
1138
1768
  * @param {string} textId - The text ID
1769
+ * @param {string} [auditMessage] - Audit message for this write
1770
+ * @returns {Promise<void>}
1139
1771
  */
1140
1772
  delete: (textId, auditMessage) =>
1141
1773
  this._request("DELETE", `/api/v1/texts/${textId}`, { auditMessage }),
@@ -1157,12 +1789,16 @@ class PlaidClient {
1157
1789
  * @param {string} textId - The text ID
1158
1790
  * @param {any} body - The request body
1159
1791
  * @param {string} [auditMessage]
1160
- * @param {{base?: string, versioned?: boolean}} [options]
1792
+ * `tokenizeLayer`: a word layer of the text, whose words the server makes for the text the
1793
+ * update types, after it places the old tokens ("Tokenize new text"). They come in the
1794
+ * answer's `reshape`.
1795
+ * @param {{base?: string, versioned?: boolean, tokenizeLayer?: string}} [options]
1796
+ * @returns {Promise<PlaidText & {reshape: TextReshape}>}
1161
1797
  */
1162
- update: (textId, body, auditMessage, { base, versioned } = {}) =>
1798
+ update: (textId, body, auditMessage, { base, versioned, tokenizeLayer } = {}) =>
1163
1799
  this._request("PATCH", `/api/v1/texts/${textId}`, {
1164
1800
  auditMessage,
1165
- body: bodyOf({ body, base }),
1801
+ body: bodyOf({ body, base, "tokenize-layer": tokenizeLayer }),
1166
1802
  versioned: versioned ?? (base === undefined || base === null),
1167
1803
  }),
1168
1804
  /**
@@ -1176,16 +1812,17 @@ class PlaidClient {
1176
1812
  * `base`, the digest of the body the edits were made on, the edit
1177
1813
  * applies only to that body and is refused with 409 and `text-changed`
1178
1814
  * otherwise, and strict mode does not stamp it unless `versioned` is
1179
- * true. See composeTextEdits and gapsToOps.
1815
+ * true. `tokenizeLayer` is as `update` takes it. See composeTextEdits and gapsToOps.
1180
1816
  * @param {string} textId - The text ID
1181
1817
  * @param {Array<object>} edits - The edit directives
1182
1818
  * @param {string} [auditMessage]
1183
- * @param {{base?: string, versioned?: boolean}} [options]
1819
+ * @param {{base?: string, versioned?: boolean, tokenizeLayer?: string}} [options]
1820
+ * @returns {Promise<PlaidText & {reshape: TextReshape}>}
1184
1821
  */
1185
- edit: (textId, edits, auditMessage, { base, versioned } = {}) =>
1822
+ edit: (textId, edits, auditMessage, { base, versioned, tokenizeLayer } = {}) =>
1186
1823
  this._request("PATCH", `/api/v1/texts/${textId}`, {
1187
1824
  auditMessage,
1188
- body: bodyOf({ edits, base }),
1825
+ body: bodyOf({ edits, base, "tokenize-layer": tokenizeLayer }),
1189
1826
  versioned: versioned ?? (base === undefined || base === null),
1190
1827
  }),
1191
1828
  };
@@ -1195,17 +1832,18 @@ class PlaidClient {
1195
1832
  * List (or search) users. Transparently follows pagination cursors and
1196
1833
  * returns the full flat array. Admin-or-maintainer only.
1197
1834
  * @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
1835
+ * @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
1836
+ * @returns {Promise<User[]>}
1199
1837
  */
1200
1838
  list: ({ q } = {}) =>
1201
1839
  listAll(this, "/api/v1/users", { query: { q } }),
1202
1840
  /**
1203
1841
  * Fetch a single page of users (optionally filtered by `q`).
1204
1842
  * @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
1843
+ * @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
1844
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1207
1845
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1208
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1846
+ * @returns {Promise<Page<User>>}
1209
1847
  */
1210
1848
  listPage: ({ q, limit, cursor } = {}) =>
1211
1849
  listPage(this, "/api/v1/users", {
@@ -1216,9 +1854,9 @@ class PlaidClient {
1216
1854
  /**
1217
1855
  * Async-iterate users page by page; yields each page's entries array.
1218
1856
  * @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
1857
+ * @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
1858
  * @param {number} [opts.pageSize] - Per-request page size
1221
- * @returns {AsyncGenerator<Array>}
1859
+ * @returns {AsyncGenerator<User[]>}
1222
1860
  */
1223
1861
  iterPages: ({ q, pageSize } = {}) =>
1224
1862
  iterPages(this, "/api/v1/users", {
@@ -1234,12 +1872,14 @@ class PlaidClient {
1234
1872
  * @param {boolean} isAdmin - Whether the user is an admin
1235
1873
  * @param {string} [displayName] - How the user is shown in the UI.
1236
1874
  * Defaults to the local part of the email.
1875
+ * @param {string} [auditMessage] - Audit message for this write
1876
+ * @returns {Promise<Created>}
1237
1877
  */
1238
1878
  create: (email, password, isAdmin, displayName, auditMessage) =>
1239
1879
  this._request("POST", "/api/v1/users", {
1240
1880
  auditMessage,
1241
1881
  body: bodyOf({
1242
- email,
1882
+ email: normalizeUserId(email),
1243
1883
  password,
1244
1884
  "is-admin": isAdmin,
1245
1885
  // Left out when not given (null too), so core gives the user the
@@ -1259,6 +1899,7 @@ class PlaidClient {
1259
1899
  * one of its operations matches, carrying only the ones that did.
1260
1900
  * @param {string[]|string} [kinds] - Only the entries of operations of
1261
1901
  * these kinds (e.g. `['review', 'guess-adoption']`), each whole
1902
+ * @returns {Promise<AuditEntry[]>}
1262
1903
  */
1263
1904
  audit: (userId, startTime, endTime, opTypes, kinds) =>
1264
1905
  listAll(this, `/api/v1/users/${userId}/audit`, {
@@ -1275,6 +1916,10 @@ class PlaidClient {
1275
1916
  * that may be long: audit() walks every page before it resolves.
1276
1917
  * @param {string} userId - The user ID
1277
1918
  * @param {object} [opts]
1919
+ * @param {string} [opts.startTime] - Only entries at or after this instant
1920
+ * @param {string} [opts.endTime] - Only entries at or before this instant
1921
+ * @param {string[]|string} [opts.opTypes] - Only these op types
1922
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
1278
1923
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
1279
1924
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1280
1925
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -1282,7 +1927,7 @@ class PlaidClient {
1282
1927
  * operations in `ops` (1..1000). `opCount` on every entry says how
1283
1928
  * many it has
1284
1929
  * @param {string} [opts.entryId] - Read only this entry, not a page
1285
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
1930
+ * @returns {Promise<Page<AuditEntry>>}
1286
1931
  */
1287
1932
  auditPage: (
1288
1933
  userId,
@@ -1304,6 +1949,7 @@ class PlaidClient {
1304
1949
  /**
1305
1950
  * Get a user by ID
1306
1951
  * @param {string} id - The resource ID
1952
+ * @returns {Promise<User>}
1307
1953
  */
1308
1954
  get: (id) =>
1309
1955
  this._request("GET", `/api/v1/users/${id}`),
@@ -1314,6 +1960,8 @@ class PlaidClient {
1314
1960
  * stays visible in listings with a deactivated-at timestamp.
1315
1961
  * Reversible via activate(), which restores login only.
1316
1962
  * @param {string} id - The resource ID
1963
+ * @param {string} [auditMessage] - Audit message for this write
1964
+ * @returns {Promise<void>}
1317
1965
  */
1318
1966
  delete: (id, auditMessage) =>
1319
1967
  this._request("DELETE", `/api/v1/users/${id}`, { auditMessage }),
@@ -1322,6 +1970,8 @@ class PlaidClient {
1322
1970
  * Project memberships, vocab maintainerships, and API tokens removed
1323
1971
  * at deactivation are NOT restored — re-grant them deliberately.
1324
1972
  * @param {string} id - The resource ID
1973
+ * @param {string} [auditMessage] - Audit message for this write
1974
+ * @returns {Promise<User>}
1325
1975
  */
1326
1976
  activate: (id, auditMessage) =>
1327
1977
  this._request("POST", `/api/v1/users/${id}/activate`, { auditMessage }),
@@ -1337,6 +1987,8 @@ class PlaidClient {
1337
1987
  * @param {string} [password] - New password
1338
1988
  * @param {string} [displayName] - New display name
1339
1989
  * @param {boolean} [isAdmin] - New admin status
1990
+ * @param {string} [auditMessage] - Audit message for this write
1991
+ * @returns {Promise<User>}
1340
1992
  */
1341
1993
  update: (id, password, displayName, isAdmin, auditMessage) =>
1342
1994
  this._request("PATCH", `/api/v1/users/${id}`, {
@@ -1348,23 +2000,89 @@ class PlaidClient {
1348
2000
  }),
1349
2001
  }),
1350
2002
  /**
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.
2003
+ * A token that shows any user's profile picture without an
2004
+ * Authorization header, for an `<img>`, which cannot send one. Resolves
2005
+ * to `{ token, expiresAt }` (`expiresAt` an ISO-8601 instant, a day on by
2006
+ * default). The server refuses it on every route but the picture, and
2007
+ * it stops working when the user signs out or changes password.
2008
+ *
2009
+ * One token serves the whole client: it is kept and handed out again
2010
+ * until under ten minutes of it remain (half its life, for one minted
2011
+ * near the end of a login), calls made while one is being minted share
2012
+ * that request, and it is dropped when the client's token changes.
2013
+ * avatarUrl() uses it.
2014
+ * @param {{renew?: boolean}} [options] - `renew`: mint a new one
2015
+ * whatever is kept, unless one was minted in the last few seconds
2016
+ * (or is being minted), which is shared
2017
+ * @returns {Promise<{token: string, expiresAt: string}>}
2018
+ */
2019
+ avatarToken: ({ renew = false } = {}) => {
2020
+ // A batch is a view of its client and shares the client's token.
2021
+ const owner = this.client ?? this;
2022
+ const cached = owner._avatarToken;
2023
+ if (cached && cached.forToken === owner.token) {
2024
+ if (cached.pending) return cached.pending;
2025
+ const now = owner.serverNow().getTime();
2026
+ // A renewal shares one minted a moment ago: every picture on a page
2027
+ // fails together when a token dies, and each asks.
2028
+ if (renew && now - cached.mintedAt < AVATAR_TOKEN_FRESH_MS) {
2029
+ return Promise.resolve(cached.link);
2030
+ }
2031
+ const expires = Date.parse(cached.link.expiresAt);
2032
+ // Renewed ten minutes before it runs out, or at half its life when
2033
+ // it was minted for less than twenty: the server caps it at the
2034
+ // login's own expiry, and near the end of a login every token it
2035
+ // mints runs out at that same moment, so a fixed ten minutes asked
2036
+ // for a new one on every call.
2037
+ const renewAt = Math.min(AVATAR_TOKEN_RENEW_MS, (expires - cached.mintedAt) / 2);
2038
+ if (!renew && expires - now > renewAt) return Promise.resolve(cached.link);
2039
+ }
2040
+ const entry = {
2041
+ forToken: owner.token,
2042
+ pending: null,
2043
+ link: null,
2044
+ mintedAt: owner.serverNow().getTime(),
2045
+ };
2046
+ entry.pending = owner
2047
+ ._request("POST", "/api/v1/avatar-link", { outOfBand: true })
2048
+ .then(
2049
+ (link) => {
2050
+ entry.pending = null;
2051
+ entry.link = link;
2052
+ return link;
2053
+ },
2054
+ (e) => {
2055
+ if (owner._avatarToken === entry) owner._avatarToken = null;
2056
+ throw e;
2057
+ },
2058
+ );
2059
+ owner._avatarToken = entry;
2060
+ return entry.pending;
2061
+ },
2062
+ /**
2063
+ * Build a URL for a user's profile picture, for an `<img>` src. The
2064
+ * URL carries the client's avatar token (see avatarToken()), never the
2065
+ * login token.
1355
2066
  *
1356
2067
  * Pass the user record's `avatarHash` as the second argument whenever you
1357
2068
  * have it: the URL then addresses that exact picture, so the browser can
1358
2069
  * 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.
2070
+ * user changes it. Resolves to null when `avatarHash` is null (the user
2071
+ * has no picture), so callers can fall back to initials without a
2072
+ * wasted request.
1361
2073
  * @param {string} id - The user ID
1362
- * @param {string} [avatarHash] - The user record's `avatarHash`
1363
- * @returns {string|null}
2074
+ * @param {string|null} [avatarHash] - The user record's `avatarHash`
2075
+ * @param {{renew?: boolean}} [options] - `renew`: mint a new avatar
2076
+ * token first, for a picture its URL failed to load (the token
2077
+ * stops working when the login it was minted under ends)
2078
+ * @returns {Promise<string|null>}
1364
2079
  */
1365
- avatarUrl: (id, avatarHash) => {
2080
+ avatarUrl: async (id, avatarHash, { renew = false } = {}) => {
1366
2081
  if (avatarHash === null) return null;
1367
- const params = new URLSearchParams({ token: this.token });
2082
+ // Through `self`, as in server.limits.
2083
+ const self = this;
2084
+ const { token } = await self.users.avatarToken({ renew });
2085
+ const params = new URLSearchParams({ "avatar-token": token });
1368
2086
  if (avatarHash) params.set("v", avatarHash);
1369
2087
  return `${this.baseUrl}/api/v1/users/${id}/avatar?${params}`;
1370
2088
  },
@@ -1372,6 +2090,7 @@ class PlaidClient {
1372
2090
  * Fetch a user's profile picture as raw bytes. Most callers want
1373
2091
  * avatarUrl() instead. This one is for non-browser consumers.
1374
2092
  * @param {string} id - The user ID
2093
+ * @returns {Promise<ArrayBuffer>}
1375
2094
  */
1376
2095
  getAvatar: (id) =>
1377
2096
  this._request("GET", `/api/v1/users/${id}/avatar`, {
@@ -1384,6 +2103,8 @@ class PlaidClient {
1384
2103
  * PNG, JPEG, WebP, and GIF. Resolves to the updated user record.
1385
2104
  * @param {string} id - The user ID
1386
2105
  * @param {File} file - The image to upload
2106
+ * @param {string} [auditMessage] - Audit message for this write
2107
+ * @returns {Promise<User>}
1387
2108
  */
1388
2109
  setAvatar: (id, file, auditMessage) => {
1389
2110
  const fd = new FormData();
@@ -1398,6 +2119,8 @@ class PlaidClient {
1398
2119
  /**
1399
2120
  * Remove a profile picture. Your own, or anyone's if you are an admin.
1400
2121
  * @param {string} id - The user ID
2122
+ * @param {string} [auditMessage] - Audit message for this write
2123
+ * @returns {Promise<void>}
1401
2124
  */
1402
2125
  // No flag: the upload beside this one is multipart and cannot be
1403
2126
  // batched, but a DELETE carries no blob, so the batch transport takes
@@ -1421,7 +2144,8 @@ class PlaidClient {
1421
2144
  * every project without dragging down the transcripts beside them.
1422
2145
  *
1423
2146
  * `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
2147
+ * one value runs to megabytes (`userDataValueBytes` in
2148
+ * `server.limits()`): a page of them with `includeValues` is the
1425
2149
  * largest response this API can be asked for. Raise it when the listing
1426
2150
  * is keys, or the values are known to be small.
1427
2151
  * @param {string} userId
@@ -1430,6 +2154,7 @@ class PlaidClient {
1430
2154
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1431
2155
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1432
2156
  * @param {number} [opts.pageSize=100] - Entries per request (1..1000)
2157
+ * @returns {Promise<UserDataEntry[]>}
1433
2158
  */
1434
2159
  list: (userId, { prefix, pattern, includeValues, pageSize = 100 } = {}) =>
1435
2160
  listAll(this, `/api/v1/users/${userId}/data`, {
@@ -1445,7 +2170,7 @@ class PlaidClient {
1445
2170
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1446
2171
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1447
2172
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1448
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2173
+ * @returns {Promise<Page<UserDataEntry>>}
1449
2174
  */
1450
2175
  listPage: (
1451
2176
  userId,
@@ -1464,6 +2189,7 @@ class PlaidClient {
1464
2189
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1465
2190
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1466
2191
  * @param {number} [opts.pageSize=100] - Entries per request (1..1000)
2192
+ * @returns {AsyncGenerator<UserDataEntry[]>}
1467
2193
  */
1468
2194
  iterPages: (
1469
2195
  userId,
@@ -1477,6 +2203,7 @@ class PlaidClient {
1477
2203
  * Read one private data entry ({key, updatedAt, version, value}); 404 if absent.
1478
2204
  * @param {string} userId
1479
2205
  * @param {string} key
2206
+ * @returns {Promise<UserDataEntry & {value: any}>}
1480
2207
  */
1481
2208
  get: (userId, key) =>
1482
2209
  this._request(
@@ -1484,8 +2211,11 @@ class PlaidClient {
1484
2211
  `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
1485
2212
  ),
1486
2213
  /**
1487
- * Create or replace one private data entry. `value` is any JSON (up to
1488
- * 1 MB). Not audited, not batchable.
2214
+ * Create or replace one private data entry. `value` is any JSON, up to
2215
+ * the server's `userDataValueBytes` (`server.limits()`, 5 MB by
2216
+ * default), and is refused with 413 over it, after which
2217
+ * `server.limits()` answers the figure the server has now. Not audited,
2218
+ * not batchable.
1489
2219
  *
1490
2220
  * The server stores it verbatim, but this client recases object keys on
1491
2221
  * the way out and back like any other body (`myKey` <-> `my-key`), so a
@@ -1506,20 +2236,31 @@ class PlaidClient {
1506
2236
  * @param {number} [opts.version] - The version this write was made from
1507
2237
  * @returns {Promise<{key: string, updatedAt: string, version: number}>}
1508
2238
  */
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
- ),
2239
+ put: async (userId, key, value, { version } = {}) => {
2240
+ try {
2241
+ return await this._request(
2242
+ "PUT",
2243
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
2244
+ {
2245
+ body: value,
2246
+ noBatch: true,
2247
+ queryParams: { version },
2248
+ },
2249
+ );
2250
+ } catch (err) {
2251
+ if (err?.status === 413) {
2252
+ // The cap may have changed since it was read: the caller deciding
2253
+ // what to do next reads the server's figure now.
2254
+ await this.server.refresh().catch(() => {});
2255
+ }
2256
+ throw err;
2257
+ }
2258
+ },
1519
2259
  /**
1520
2260
  * Delete one private data entry; 404 if absent.
1521
2261
  * @param {string} userId
1522
2262
  * @param {string} key
2263
+ * @returns {Promise<void>}
1523
2264
  */
1524
2265
  delete: (userId, key) =>
1525
2266
  this._request(
@@ -1537,6 +2278,7 @@ class PlaidClient {
1537
2278
  * string itself — that is only returned once, by create().
1538
2279
  * Transparently follows pagination cursors and returns the full flat array.
1539
2280
  * @param {string} userId - The user ID who owns the tokens
2281
+ * @returns {Promise<ApiToken[]>}
1540
2282
  */
1541
2283
  list: (userId) => listAll(this, `/api/v1/users/${userId}/tokens`),
1542
2284
  /**
@@ -1545,7 +2287,7 @@ class PlaidClient {
1545
2287
  * @param {object} [opts]
1546
2288
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1547
2289
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1548
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2290
+ * @returns {Promise<Page<ApiToken>>}
1549
2291
  */
1550
2292
  listPage: (userId, { limit, cursor } = {}) =>
1551
2293
  listPage(this, `/api/v1/users/${userId}/tokens`, { limit, cursor }),
@@ -1555,7 +2297,7 @@ class PlaidClient {
1555
2297
  * @param {string} userId - The user ID who owns the tokens
1556
2298
  * @param {object} [opts]
1557
2299
  * @param {number} [opts.pageSize] - Per-request page size
1558
- * @returns {AsyncGenerator<Array>}
2300
+ * @returns {AsyncGenerator<ApiToken[]>}
1559
2301
  */
1560
2302
  iterPages: (userId, { pageSize } = {}) =>
1561
2303
  iterPages(this, `/api/v1/users/${userId}/tokens`, { pageSize }),
@@ -1567,6 +2309,7 @@ class PlaidClient {
1567
2309
  * needs a sign-in token.
1568
2310
  * @param {string} userId - The user ID who will own the token
1569
2311
  * @param {string} name - A human label, e.g. "Stanza parser"
2312
+ * @param {string} [auditMessage] - Audit message for this write
1570
2313
  * @returns {Promise<{id: string, name: string, token: string}>}
1571
2314
  */
1572
2315
  create: (userId, name, auditMessage) =>
@@ -1583,6 +2326,8 @@ class PlaidClient {
1583
2326
  * needs a sign-in token.
1584
2327
  * @param {string} userId - The user ID who owns the token
1585
2328
  * @param {string} tokenId - The token ID to revoke
2329
+ * @param {string} [auditMessage] - Audit message for this write
2330
+ * @returns {Promise<void>}
1586
2331
  */
1587
2332
  revoke: (userId, tokenId, auditMessage) =>
1588
2333
  this._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
@@ -1600,6 +2345,7 @@ class PlaidClient {
1600
2345
  * @param {object} [opts]
1601
2346
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1602
2347
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
2348
+ * @returns {Promise<Invite[]>}
1603
2349
  */
1604
2350
  list: ({ projectId, all } = {}) =>
1605
2351
  listAll(this, "/api/v1/invites", {
@@ -1612,7 +2358,7 @@ class PlaidClient {
1612
2358
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1613
2359
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1614
2360
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1615
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2361
+ * @returns {Promise<Page<Invite>>}
1616
2362
  */
1617
2363
  listPage: ({ projectId, all, limit, cursor } = {}) =>
1618
2364
  listPage(this, "/api/v1/invites", {
@@ -1626,7 +2372,7 @@ class PlaidClient {
1626
2372
  * @param {string} [opts.projectId] - List this project's invites instead of your own
1627
2373
  * @param {boolean} [opts.all] - List every invite on the server (admin only)
1628
2374
  * @param {number} [opts.pageSize] - Per-request page size
1629
- * @returns {AsyncGenerator<Array>}
2375
+ * @returns {AsyncGenerator<Invite[]>}
1630
2376
  */
1631
2377
  iterPages: ({ projectId, all, pageSize } = {}) =>
1632
2378
  iterPages(this, "/api/v1/invites", {
@@ -1659,7 +2405,8 @@ class PlaidClient {
1659
2405
  * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
1660
2406
  * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
1661
2407
  * @param {string} [opts.note] - Human label shown in your invite list
1662
- * @returns {Promise<{id: string, code: string, kind: string, status: string}>}
2408
+ * @param {string} [auditMessage] - Audit message for this write
2409
+ * @returns {Promise<Invite & {code: string}>}
1663
2410
  */
1664
2411
  create: (
1665
2412
  {
@@ -1692,6 +2439,8 @@ class PlaidClient {
1692
2439
  * Revoke an invite, killing the link immediately. Idempotent. Allowed for
1693
2440
  * the creator, an admin, or a maintainer of the invite's project.
1694
2441
  * @param {string} id - The invite ID
2442
+ * @param {string} [auditMessage] - Audit message for this write
2443
+ * @returns {Promise<void>}
1695
2444
  */
1696
2445
  revoke: (id, auditMessage) =>
1697
2446
  this._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
@@ -1704,7 +2453,7 @@ class PlaidClient {
1704
2453
  * configuration and the backups on disk, and the settings an operator
1705
2454
  * gets asked to confirm. Carries no secrets. Runs a count per table and
1706
2455
  * 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}>}
2456
+ * @returns {Promise<ServerReport>}
1708
2457
  */
1709
2458
  server: () => this._request("GET", "/api/v1/admin/server"),
1710
2459
  /**
@@ -1718,7 +2467,7 @@ class PlaidClient {
1718
2467
  * rather than the client's usual timeout. A proxy in front of the server
1719
2468
  * may still give up first (a 504): the backup goes on, and its file
1720
2469
  * shows in `server().backup.backups` once it is written.
1721
- * @returns {Promise<{ok: boolean, directory: string, backups: Array}>}
2470
+ * @returns {Promise<BackupStatus>}
1722
2471
  *
1723
2472
  * outOfBand, as every admin action on the server itself is: none of them
1724
2473
  * writes project data (see the note at the top of http.js).
@@ -1738,6 +2487,7 @@ class PlaidClient {
1738
2487
  * Drop the lock on a document whoever holds it. Idempotent. For a client
1739
2488
  * that went away without releasing one. Admin only.
1740
2489
  * @param {string} documentId - The document ID
2490
+ * @returns {Promise<{result: string}>}
1741
2491
  */
1742
2492
  releaseLock: (documentId) =>
1743
2493
  this._request("DELETE", `/api/v1/admin/locks/${documentId}`, {
@@ -1747,7 +2497,7 @@ class PlaidClient {
1747
2497
  * Live login and invite rate-limit buckets: the address, the account
1748
2498
  * where there is one, failures inside the window, the limit, and whether
1749
2499
  * it is currently blocking. Admin only.
1750
- * @returns {Promise<{windowMs: number, logins: Array, ips: Array, invites: Array}>}
2500
+ * @returns {Promise<RateLimitSnapshot>}
1751
2501
  */
1752
2502
  rateLimits: () => this._request("GET", "/api/v1/admin/rate-limits"),
1753
2503
  /**
@@ -1757,6 +2507,7 @@ class PlaidClient {
1757
2507
  * @param {object} [opts]
1758
2508
  * @param {string} [opts.ip] - Clear only this address
1759
2509
  * @param {string} [opts.userId] - Narrow to one account on that address
2510
+ * @returns {Promise<{result: string}>}
1760
2511
  */
1761
2512
  clearRateLimits: ({ ip, userId } = {}) =>
1762
2513
  this._request("DELETE", "/api/v1/admin/rate-limits", {
@@ -1780,7 +2531,7 @@ class PlaidClient {
1780
2531
  * @param {string} [opts.status] - "2xx".."5xx", an exact code, or "failures"
1781
2532
  * @param {string} [opts.user] - Only requests made by this account
1782
2533
  * @param {string} [opts.method] - Only requests with this HTTP method
1783
- * @returns {Promise<{requests: object, events: object, file: (string|null)}>}
2534
+ * @returns {Promise<LiveLog>}
1784
2535
  */
1785
2536
  logs: ({ limit, q, level, status, user, method } = {}) =>
1786
2537
  this._request("GET", "/api/v1/admin/logs", {
@@ -1814,7 +2565,7 @@ class PlaidClient {
1814
2565
  * @param {string} [opts.prefix] - Only keys starting with this
1815
2566
  * @param {string} [opts.pattern] - Only keys matching this GLOB
1816
2567
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1817
- * @returns {Promise<Array<{userId: string, key: string, updatedAt: string, value?: *}>>}
2568
+ * @returns {Promise<AdminUserDataEntry[]>}
1818
2569
  */
1819
2570
  userData: ({ prefix, pattern, includeValues } = {}) =>
1820
2571
  listAll(this, "/api/v1/admin/user-data", {
@@ -1833,7 +2584,7 @@ class PlaidClient {
1833
2584
  * @param {boolean} [opts.includeValues] - Also return each entry's value
1834
2585
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
1835
2586
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
1836
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2587
+ * @returns {Promise<Page<AdminUserDataEntry>>}
1837
2588
  */
1838
2589
  userDataPage: ({ prefix, pattern, includeValues, limit, cursor } = {}) =>
1839
2590
  listPage(this, "/api/v1/admin/user-data", {
@@ -1861,6 +2612,7 @@ class PlaidClient {
1861
2612
  * matches, carrying only the ones that did.
1862
2613
  * @param {string[]|string} [opts.kinds] - Only the entries of operations
1863
2614
  * of these kinds (e.g. `['review']`), each whole
2615
+ * @returns {Promise<AuditEntry[]>}
1864
2616
  */
1865
2617
  list: ({ startTime, endTime, opTypes, kinds } = {}) =>
1866
2618
  listAll(this, "/api/v1/audit", {
@@ -1874,6 +2626,10 @@ class PlaidClient {
1874
2626
  /**
1875
2627
  * Fetch a single page of the instance-wide audit log. Admin only.
1876
2628
  * @param {object} [opts]
2629
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2630
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2631
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2632
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
1877
2633
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first, which
1878
2634
  * is what a feed wants. A cursor belongs to the direction that made it.
1879
2635
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
@@ -1882,7 +2638,7 @@ class PlaidClient {
1882
2638
  * operations in `ops` (1..1000). `opCount` on every entry says how
1883
2639
  * many it has
1884
2640
  * @param {string} [opts.entryId] - Read only this entry, not a page
1885
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
2641
+ * @returns {Promise<Page<AuditEntry>>}
1886
2642
  */
1887
2643
  listPage: ({ startTime, endTime, opTypes, kinds, order, limit, cursor, opsLimit, entryId } = {}) =>
1888
2644
  listPage(this, "/api/v1/audit", {
@@ -1900,7 +2656,13 @@ class PlaidClient {
1900
2656
  }),
1901
2657
  /**
1902
2658
  * Async-iterate the instance-wide audit log page by page. Admin only.
1903
- * @returns {AsyncGenerator<Array>}
2659
+ * @param {object} [opts]
2660
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2661
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2662
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2663
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
2664
+ * @param {number} [opts.pageSize] - Per-request page size
2665
+ * @returns {AsyncGenerator<AuditEntry[]>}
1904
2666
  */
1905
2667
  iterPages: ({ startTime, endTime, opTypes, kinds, pageSize } = {}) =>
1906
2668
  iterPages(this, "/api/v1/audit", {
@@ -1928,7 +2690,7 @@ class PlaidClient {
1928
2690
  * @param {boolean} [opts.daily] - Also return `byDay`, a list of
1929
2691
  * `{date, changes}` per user, oldest first, at the cost of a second
1930
2692
  * grouped scan
1931
- * @returns {Promise<Array<{user: object, operations: number, changes: number, documents: number, firstTs: string, lastTs: string, byDay?: object}>>}
2693
+ * @returns {Promise<ActivityTallyRow[]>}
1932
2694
  */
1933
2695
  tally: async ({ projectId, startTime, endTime, daily } = {}) => {
1934
2696
  const path = projectId
@@ -1946,15 +2708,24 @@ class PlaidClient {
1946
2708
  };
1947
2709
 
1948
2710
  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 }),
2711
+ ...layerMethods(this, "token-layers"),
2712
+ /**
2713
+ * Count, per document, the tokens of each layer whose surface matches a
2714
+ * regex, as a query's `value: {regex}` on a token decides. Each text is
2715
+ * read once, so it stays fast on a long document. Layers you cannot read
2716
+ * are left out.
2717
+ * @param {string[]} layerIds - The token layer ids
2718
+ * @param {{regex: string, flags?: string}} value - The regex spec
2719
+ * @returns {Promise<{results: Array<[string, string, number]>}>} [layerId,
2720
+ * documentId, count] rows, a document with tokens of the layer and no
2721
+ * match counting 0.
2722
+ */
2723
+ countMatching: (layerIds, value) =>
2724
+ this._request("POST", "/api/v1/query/token-counts", {
2725
+ body: { layers: layerIds, value },
2726
+ outOfBand: true,
2727
+ timeout: queryTimeout(this),
2728
+ noBusyRetry: true,
1958
2729
  }),
1959
2730
  /**
1960
2731
  * Create a new token layer.
@@ -1962,6 +2733,10 @@ class PlaidClient {
1962
2733
  * @param {string} name - The name
1963
2734
  * @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
2735
  * @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).
2736
+ * @param {string} [auditMessage] - Audit message for this write
2737
+ * @param {object} [opts]
2738
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
2739
+ * @returns {Promise<Created>}
1965
2740
  */
1966
2741
  create: (
1967
2742
  textLayerId,
@@ -1981,67 +2756,15 @@ class PlaidClient {
1981
2756
  "parent-token-layer-id": parentTokenLayerId,
1982
2757
  }),
1983
2758
  }),
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
- ),
2759
+ ...configMethods(this, "token-layers"),
2014
2760
  ...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}`, {
2036
- auditMessage,
2037
- body: bodyOf({ name }),
2038
- }),
2039
2761
  };
2040
2762
 
2041
2763
  this.documents = {
2042
2764
  /**
2043
2765
  * Check the lock status of a document.
2044
2766
  * @param {string} documentId - The document ID
2767
+ * @returns {Promise<{userId: string, expiresAt: number} | void>}
2045
2768
  */
2046
2769
  checkLock: (documentId) =>
2047
2770
  this._request("GET", `/api/v1/documents/${documentId}/lock`),
@@ -2066,6 +2789,7 @@ class PlaidClient {
2066
2789
  * @param {string} documentId - The document ID
2067
2790
  * @param {string} [auditMessage]
2068
2791
  * @param {string} [newLockId] - A holder id this client minted
2792
+ * @returns {Promise<LockGrant>}
2069
2793
  */
2070
2794
  acquireLock: (documentId, auditMessage, newLockId) =>
2071
2795
  this._request("POST", `/api/v1/documents/${documentId}/lock`, {
@@ -2082,6 +2806,8 @@ class PlaidClient {
2082
2806
  * outOfBand, for the same reason as acquireLock.
2083
2807
  * @param {string} documentId - The document ID
2084
2808
  * @param {string} lockId - The `lockId` acquireLock answered with
2809
+ * @param {string} [auditMessage] - Audit message for this write
2810
+ * @returns {Promise<LockGrant>}
2085
2811
  */
2086
2812
  renewLock: (documentId, lockId, auditMessage) =>
2087
2813
  this._request("POST", `/api/v1/documents/${documentId}/lock`, {
@@ -2096,6 +2822,8 @@ class PlaidClient {
2096
2822
  * and not released at all if it aborts.
2097
2823
  * @param {string} documentId - The document ID
2098
2824
  * @param {string} lockId - The `lockId` acquireLock answered with
2825
+ * @param {string} [auditMessage] - Audit message for this write
2826
+ * @returns {Promise<void>}
2099
2827
  */
2100
2828
  releaseLock: (documentId, lockId, auditMessage) =>
2101
2829
  this._request("DELETE", `/api/v1/documents/${documentId}/lock`, {
@@ -2139,9 +2867,17 @@ class PlaidClient {
2139
2867
  * lost lock is recorded on the CLIENT, like strict mode, so it stops
2140
2868
  * every write the client makes (on any batch of it too) and not only the
2141
2869
  * ones this block makes.
2870
+ *
2871
+ * A core restart drops every lock, and until the next renewal finds it
2872
+ * gone (up to half the lock window) this block's writes still pass, since
2873
+ * writes carry no lock id. When a write must not land past that point,
2874
+ * hold the document in strict mode too (`enterStrictMode`), so such a
2875
+ * write is refused with a 409.
2876
+ * @template T
2142
2877
  * @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)
2878
+ * @param {(lock: import("./documentLock.js").DocumentLock) => T | Promise<T>} fn - The work to run while holding it
2879
+ * @param {{keepAlive?: boolean}} [options] - `{ keepAlive }`: renew on a timer (default true)
2880
+ * @returns {Promise<T>}
2145
2881
  */
2146
2882
  locked: (documentId, fn, options) =>
2147
2883
  withDocumentLock(this, documentId, fn, options),
@@ -2150,11 +2886,34 @@ class PlaidClient {
2150
2886
  * no as-of form: the route refuses the parameter. Prefer the document's
2151
2887
  * own `mediaUrl`, which carries the file's version for caching.
2152
2888
  * @param {string} documentId - The document ID
2889
+ * @returns {Promise<ArrayBuffer>}
2153
2890
  */
2154
2891
  getMedia: (documentId) =>
2155
2892
  this._request("GET", `/api/v1/documents/${documentId}/media`, {
2156
2893
  binaryResponse: true,
2157
2894
  }),
2895
+ /**
2896
+ * Get a link that plays the document's recording without an
2897
+ * Authorization header, for an audio or video element's `src`, which
2898
+ * cannot send one. `url` is the document's media URL with a
2899
+ * `media-token` added, resolved against the client's base URL. The token
2900
+ * opens only this recording, for this user, and the server refuses it on
2901
+ * every other route. It expires at `expiresAt` (an ISO-8601 instant, six
2902
+ * hours on by default), and sooner when the user signs out, changes
2903
+ * password or loses access to the project: ask for a new link then. A
2904
+ * document with no recording is a 404. Writes nothing, so it takes no
2905
+ * Idempotency-Key and goes over the wire when made on a batch.
2906
+ * @param {string} documentId - The document ID
2907
+ * @returns {Promise<{url: string, expiresAt: string}>}
2908
+ */
2909
+ mediaLink: async (documentId) => {
2910
+ const link = await this._request(
2911
+ "POST",
2912
+ `/api/v1/documents/${documentId}/media/link`,
2913
+ { outOfBand: true },
2914
+ );
2915
+ return { ...link, url: `${this.baseUrl}${link.url}` };
2916
+ },
2158
2917
  /**
2159
2918
  * Upload a media file for a document. Uses Apache Tika for content validation.
2160
2919
  * A document that already has a recording refuses it with a 409
@@ -2165,7 +2924,8 @@ class PlaidClient {
2165
2924
  * @param {string} documentId - The document ID
2166
2925
  * @param {File} file - The file to upload
2167
2926
  * @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.
2927
+ * @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.
2928
+ * @returns {Promise<{message: string, extension: string, contentType: string}>}
2169
2929
  */
2170
2930
  uploadMedia: (documentId, file, auditMessage, { onProgress } = {}) => {
2171
2931
  const fd = new FormData();
@@ -2186,50 +2946,25 @@ class PlaidClient {
2186
2946
  * recording meant, the `?v=` of the document's `mediaUrl`. The delete
2187
2947
  * is refused with a 409 (`media-changed`, and the current `media-url`)
2188
2948
  * when the stored recording is another one.
2949
+ * @returns {Promise<void>}
2189
2950
  */
2190
2951
  // The upload above is multipart and cannot be batched, but a DELETE
2191
2952
  // carries no blob, so the batch transport takes it. It is a write
2192
- // of the document's own data and queues like any other. Note that the
2193
- // file removal happens outside the server's transaction, so a batch that
2194
- // aborts after this op does not bring the file back.
2953
+ // of the document's own data and queues like any other. The server
2954
+ // removes the file only after the write commits, so a batch that aborts
2955
+ // after this op leaves the recording in place.
2195
2956
  deleteMedia: (documentId, auditMessage, { mediaVersion } = {}) =>
2196
2957
  this._request("DELETE", `/api/v1/documents/${documentId}/media`, {
2197
2958
  auditMessage,
2198
2959
  queryParams: { "media-version": mediaVersion },
2199
2960
  }),
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
- }),
2961
+ ...metadataMethods(this, "documents"),
2228
2962
  /**
2229
2963
  * Get audit log for a document. Transparently follows pagination cursors
2230
2964
  * and returns the full flat array. Each entry, and each of its `ops`,
2231
2965
  * says what kind of credential made it as `credential`: `login`,
2232
- * `named-token`, `service` or `delegated` (absent on older operations),
2966
+ * `named-token` or `delegated` (absent on older operations, `service`
2967
+ * on some from 2026-10-06 to 2026-10-08),
2233
2968
  * beside `apiToken` (the named token's id and name) when there was one.
2234
2969
  * @param {string} documentId - The document ID
2235
2970
  * @param {string} [startTime] - Start of time range
@@ -2240,6 +2975,7 @@ class PlaidClient {
2240
2975
  * one of its operations matches, carrying only the ones that did.
2241
2976
  * @param {string[]|string} [kinds] - Only the entries of operations of
2242
2977
  * these kinds (e.g. `['review', 'guess-adoption']`), each whole
2978
+ * @returns {Promise<AuditEntry[]>}
2243
2979
  */
2244
2980
  audit: (documentId, startTime, endTime, opTypes, kinds) =>
2245
2981
  listAll(this, `/api/v1/documents/${documentId}/audit`, {
@@ -2256,6 +2992,10 @@ class PlaidClient {
2256
2992
  * that may be long: audit() walks every page before it resolves.
2257
2993
  * @param {string} documentId - The document ID
2258
2994
  * @param {object} [opts]
2995
+ * @param {string} [opts.startTime] - Only entries at or after this instant
2996
+ * @param {string} [opts.endTime] - Only entries at or before this instant
2997
+ * @param {string[]|string} [opts.opTypes] - Only these op types
2998
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
2259
2999
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2260
3000
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2261
3001
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -2263,7 +3003,7 @@ class PlaidClient {
2263
3003
  * operations in `ops` (1..1000). `opCount` on every entry says how
2264
3004
  * many it has
2265
3005
  * @param {string} [opts.entryId] - Read only this entry, not a page
2266
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3006
+ * @returns {Promise<Page<AuditEntry>>}
2267
3007
  */
2268
3008
  auditPage: (
2269
3009
  documentId,
@@ -2287,14 +3027,17 @@ class PlaidClient {
2287
3027
  * operation: what was deleted since then comes back under its original
2288
3028
  * id, what was added since is removed, and what changed is set back,
2289
3029
  * across every layer. A layer deleted since then, or a vocabulary entry
2290
- * that no longer exists, is skipped and reported under `skipped`.
3030
+ * that no longer exists, is skipped and reported under `skipped`. A name
3031
+ * another document holds now stays with it, and the document keeps its
3032
+ * current name, reported under `nameKept` (`{name, taken}`).
2291
3033
  * Resolves to a summary of the changes. Maintainers only.
2292
3034
  * @param {string} documentId - The document ID
2293
3035
  * @param {string} asOf - The moment to go back to (ISO-8601 instant),
2294
3036
  * typically a history entry's `endTime`
2295
- * @param {object} [options] - `{ dryRun }`: with `dryRun` true nothing
3037
+ * @param {{dryRun?: boolean}} [options] - `{ dryRun }`: with `dryRun` true nothing
2296
3038
  * is written and the summary says what would change
2297
3039
  * @param {string} [auditMessage] - Custom audit message for this operation
3040
+ * @returns {Promise<DocumentRestoreSummary>}
2298
3041
  */
2299
3042
  restore: (documentId, asOf, { dryRun } = {}, auditMessage) =>
2300
3043
  this._request("POST", `/api/v1/documents/${documentId}/restore`, {
@@ -2307,6 +3050,10 @@ class PlaidClient {
2307
3050
  /**
2308
3051
  * Get a document. Set `includeBody` to true to include all data.
2309
3052
  *
3053
+ * A document with a recording has `mediaUrl`, and `mediaDuration`, the
3054
+ * recording's length in seconds, when the server could read it from the
3055
+ * file at upload (WAV, MP4, M4A, MOV, FLAC, MP3).
3056
+ *
2310
3057
  * `layers` narrows a body read to the layers you name (ids of any kind:
2311
3058
  * text, token, span, or relation). A layer comes back when it is named
2312
3059
  * or is an ancestor of a named layer, and carries its own
@@ -2318,6 +3065,7 @@ class PlaidClient {
2318
3065
  * @param {boolean} [includeBody] - Include document body data
2319
3066
  * @param {string} [asOf] - Temporal query timestamp
2320
3067
  * @param {string[]|string} [layers] - Layer ids to restrict a body read to
3068
+ * @returns {Promise<PlaidDocument>}
2321
3069
  */
2322
3070
  get: (documentId, includeBody, asOf, layers) =>
2323
3071
  this._request("GET", `/api/v1/documents/${documentId}`, {
@@ -2330,15 +3078,21 @@ class PlaidClient {
2330
3078
  /**
2331
3079
  * Delete a document and all data contained.
2332
3080
  * @param {string} documentId - The document ID
3081
+ * @param {string} [auditMessage] - Audit message for this write
3082
+ * @returns {Promise<void>}
2333
3083
  */
2334
3084
  delete: (documentId, auditMessage) =>
2335
3085
  this._request("DELETE", `/api/v1/documents/${documentId}`, {
2336
3086
  auditMessage,
2337
3087
  }),
2338
3088
  /**
2339
- * Update a document's name.
3089
+ * Update a document's name. A name another document of the project holds,
3090
+ * ignoring case,
3091
+ * is refused with 422 and `name-taken`.
2340
3092
  * @param {string} documentId - The document ID
2341
3093
  * @param {string} name - The name
3094
+ * @param {string} [auditMessage] - Audit message for this write
3095
+ * @returns {Promise<PlaidDocument>}
2342
3096
  */
2343
3097
  update: (documentId, name, auditMessage) =>
2344
3098
  this._request("PATCH", `/api/v1/documents/${documentId}`, {
@@ -2350,11 +3104,24 @@ class PlaidClient {
2350
3104
  * @param {string} projectId - The project ID
2351
3105
  * @param {string} name - The name
2352
3106
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
3107
+ * @param {string} [auditMessage] - Audit message for this write
3108
+ * @param {object} [opts]
3109
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
3110
+ * @param {boolean} [opts.freeName] - A name another document of the project holds, ignoring case, becomes
3111
+ * the first free "name (2)", "name (3)" and on. Without it such a name is refused with 422
3112
+ * and `name-taken`.
3113
+ * @returns {Promise<{id: string, name: string}>} The new id and the name stored
2353
3114
  */
2354
- create: (projectId, name, metadata, auditMessage, { id } = {}) =>
3115
+ create: (projectId, name, metadata, auditMessage, { id, freeName } = {}) =>
2355
3116
  this._request("POST", "/api/v1/documents", {
2356
3117
  auditMessage,
2357
- body: bodyOf({ id, "project-id": projectId, name, metadata }),
3118
+ body: bodyOf({
3119
+ id,
3120
+ "project-id": projectId,
3121
+ name,
3122
+ metadata,
3123
+ "free-name": freeName || undefined,
3124
+ }),
2358
3125
  }),
2359
3126
  /**
2360
3127
  * Copy a document and everything in it into a new document of the same
@@ -2362,18 +3129,27 @@ class PlaidClient {
2362
3129
  * the vocabulary entries its links name, and holds the source's texts,
2363
3130
  * tokens, spans, relations and vocab links under fresh ids, with their
2364
3131
  * metadata. Comments do not travel. The media file does, unless
2365
- * `includeMedia` is false. Resolves to `{ id }`, plus `mediaError` when
2366
- * the source had media the copy could not take with it.
3132
+ * `includeMedia` is false. Resolves to `{ id, name }`, plus `mediaError` when
3133
+ * the source had media the copy could not take with it. A name another
3134
+ * document of the project holds, ignoring case, is refused with 422 and `name-taken`,
3135
+ * unless `freeName`, which takes the first free "name (2)", "name (3)"
3136
+ * and on.
2367
3137
  * @param {string} documentId - The document to copy
2368
3138
  * @param {string} name - The new document's name
2369
- * @param {object} [options] - `{ includeMedia }`: false leaves the media
3139
+ * @param {{includeMedia?: boolean, id?: string, freeName?: boolean}} [options] - `{ includeMedia }`: false leaves the media
2370
3140
  * file behind
2371
3141
  * @param {string} [auditMessage] - Custom audit message for this operation
3142
+ * @returns {Promise<{id: string, name: string, mediaError?: string}>}
2372
3143
  */
2373
- copy: (documentId, name, { includeMedia, id } = {}, auditMessage) =>
3144
+ copy: (documentId, name, { includeMedia, id, freeName } = {}, auditMessage) =>
2374
3145
  this._request("POST", `/api/v1/documents/${documentId}/copy`, {
2375
3146
  auditMessage,
2376
- body: bodyOf({ id, name, "include-media": includeMedia }),
3147
+ body: bodyOf({
3148
+ id,
3149
+ name,
3150
+ "include-media": includeMedia,
3151
+ "free-name": freeName || undefined,
3152
+ }),
2377
3153
  }),
2378
3154
  };
2379
3155
 
@@ -2382,6 +3158,8 @@ class PlaidClient {
2382
3158
  * Set a user's access level to read and write for this project.
2383
3159
  * @param {string} id - The resource ID
2384
3160
  * @param {string} userId - The user ID
3161
+ * @param {string} [auditMessage] - Audit message for this write
3162
+ * @returns {Promise<void>}
2385
3163
  */
2386
3164
  addWriter: (id, userId, auditMessage) =>
2387
3165
  this._request("POST", `/api/v1/projects/${id}/writers/${userId}`, {
@@ -2391,6 +3169,8 @@ class PlaidClient {
2391
3169
  * Remove a user's writer privileges for this project.
2392
3170
  * @param {string} id - The resource ID
2393
3171
  * @param {string} userId - The user ID
3172
+ * @param {string} [auditMessage] - Audit message for this write
3173
+ * @returns {Promise<void>}
2394
3174
  */
2395
3175
  removeWriter: (id, userId, auditMessage) =>
2396
3176
  this._request("DELETE", `/api/v1/projects/${id}/writers/${userId}`, {
@@ -2400,6 +3180,8 @@ class PlaidClient {
2400
3180
  * Set a user's access level to read-only for this project.
2401
3181
  * @param {string} id - The resource ID
2402
3182
  * @param {string} userId - The user ID
3183
+ * @param {string} [auditMessage] - Audit message for this write
3184
+ * @returns {Promise<void>}
2403
3185
  */
2404
3186
  addReader: (id, userId, auditMessage) =>
2405
3187
  this._request("POST", `/api/v1/projects/${id}/readers/${userId}`, {
@@ -2409,45 +3191,20 @@ class PlaidClient {
2409
3191
  * Remove a user's reader privileges for this project.
2410
3192
  * @param {string} id - The resource ID
2411
3193
  * @param {string} userId - The user ID
3194
+ * @param {string} [auditMessage] - Audit message for this write
3195
+ * @returns {Promise<void>}
2412
3196
  */
2413
3197
  removeReader: (id, userId, auditMessage) =>
2414
3198
  this._request("DELETE", `/api/v1/projects/${id}/readers/${userId}`, {
2415
3199
  auditMessage,
2416
3200
  }),
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
- ),
3201
+ ...configMethods(this, "projects"),
2447
3202
  /**
2448
3203
  * Assign a user as a maintainer for this project.
2449
3204
  * @param {string} id - The resource ID
2450
3205
  * @param {string} userId - The user ID
3206
+ * @param {string} [auditMessage] - Audit message for this write
3207
+ * @returns {Promise<void>}
2451
3208
  */
2452
3209
  addMaintainer: (id, userId, auditMessage) =>
2453
3210
  this._request("POST", `/api/v1/projects/${id}/maintainers/${userId}`, {
@@ -2457,6 +3214,8 @@ class PlaidClient {
2457
3214
  * Remove a user's maintainer privileges for this project.
2458
3215
  * @param {string} id - The resource ID
2459
3216
  * @param {string} userId - The user ID
3217
+ * @param {string} [auditMessage] - Audit message for this write
3218
+ * @returns {Promise<void>}
2460
3219
  */
2461
3220
  removeMaintainer: (id, userId, auditMessage) =>
2462
3221
  this._request(
@@ -2468,7 +3227,8 @@ class PlaidClient {
2468
3227
  * Get audit log for a project. Transparently follows pagination cursors
2469
3228
  * and returns the full flat array. Each entry, and each of its `ops`,
2470
3229
  * says what kind of credential made it as `credential`: `login`,
2471
- * `named-token`, `service` or `delegated` (absent on older operations),
3230
+ * `named-token` or `delegated` (absent on older operations, `service`
3231
+ * on some from 2026-10-06 to 2026-10-08),
2472
3232
  * beside `apiToken` (the named token's id and name) when there was one.
2473
3233
  * @param {string} projectId - The project ID
2474
3234
  * @param {string} [startTime] - Start of time range
@@ -2479,6 +3239,7 @@ class PlaidClient {
2479
3239
  * one of its operations matches, carrying only the ones that did.
2480
3240
  * @param {string[]|string} [kinds] - Only the entries of operations of
2481
3241
  * these kinds (e.g. `['review', 'guess-adoption']`), each whole
3242
+ * @returns {Promise<AuditEntry[]>}
2482
3243
  */
2483
3244
  audit: (projectId, startTime, endTime, opTypes, kinds) =>
2484
3245
  listAll(this, `/api/v1/projects/${projectId}/audit`, {
@@ -2495,6 +3256,10 @@ class PlaidClient {
2495
3256
  * that may be long: audit() walks every page before it resolves.
2496
3257
  * @param {string} projectId - The project ID
2497
3258
  * @param {object} [opts]
3259
+ * @param {string} [opts.startTime] - Only entries at or after this instant
3260
+ * @param {string} [opts.endTime] - Only entries at or before this instant
3261
+ * @param {string[]|string} [opts.opTypes] - Only these op types
3262
+ * @param {OperationKind[]|string} [opts.kinds] - Only the entries of operations of these kinds, each whole
2498
3263
  * @param {"asc"|"desc"} [opts.order] - "desc" pages newest-first
2499
3264
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2500
3265
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
@@ -2502,7 +3267,7 @@ class PlaidClient {
2502
3267
  * operations in `ops` (1..1000). `opCount` on every entry says how
2503
3268
  * many it has
2504
3269
  * @param {string} [opts.entryId] - Read only this entry, not a page
2505
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3270
+ * @returns {Promise<Page<AuditEntry>>}
2506
3271
  */
2507
3272
  auditPage: (
2508
3273
  projectId,
@@ -2539,6 +3304,8 @@ class PlaidClient {
2539
3304
  * Link a vocabulary to a project.
2540
3305
  * @param {string} id - The resource ID
2541
3306
  * @param {string} vocabId - The vocab layer ID
3307
+ * @param {string} [auditMessage] - Audit message for this write
3308
+ * @returns {Promise<void>}
2542
3309
  */
2543
3310
  linkVocab: (id, vocabId, auditMessage) =>
2544
3311
  this._request("POST", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
@@ -2548,6 +3315,8 @@ class PlaidClient {
2548
3315
  * Unlink a vocabulary from a project.
2549
3316
  * @param {string} id - The resource ID
2550
3317
  * @param {string} vocabId - The vocab layer ID
3318
+ * @param {string} [auditMessage] - Audit message for this write
3319
+ * @returns {Promise<void>}
2551
3320
  */
2552
3321
  unlinkVocab: (id, vocabId, auditMessage) =>
2553
3322
  this._request("DELETE", `/api/v1/projects/${id}/vocabs/${vocabId}`, {
@@ -2557,6 +3326,7 @@ class PlaidClient {
2557
3326
  * Get a project by ID. To fetch the project's documents, use
2558
3327
  * listDocuments(id) — the include-documents flag has been removed.
2559
3328
  * @param {string} id - The resource ID
3329
+ * @returns {Promise<Project>}
2560
3330
  */
2561
3331
  get: (id) =>
2562
3332
  this._request("GET", `/api/v1/projects/${id}`),
@@ -2567,6 +3337,7 @@ class PlaidClient {
2567
3337
  * Note: this endpoint does not support temporal (`as-of`) queries; the
2568
3338
  * server rejects `?as-of=` on the documents-list route with a 400.
2569
3339
  * @param {string} id - The project ID
3340
+ * @returns {Promise<DocumentListEntry[]>}
2570
3341
  */
2571
3342
  listDocuments: (id) => listAll(this, `/api/v1/projects/${id}/documents`),
2572
3343
  /**
@@ -2578,7 +3349,7 @@ class PlaidClient {
2578
3349
  * @param {object} [opts]
2579
3350
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2580
3351
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
2581
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3352
+ * @returns {Promise<Page<DocumentListEntry>>}
2582
3353
  */
2583
3354
  listDocumentsPage: (id, { limit, cursor } = {}) =>
2584
3355
  listPage(this, `/api/v1/projects/${id}/documents`, { limit, cursor }),
@@ -2591,7 +3362,7 @@ class PlaidClient {
2591
3362
  * @param {string} id - The project ID
2592
3363
  * @param {object} [opts]
2593
3364
  * @param {number} [opts.pageSize] - Per-request page size
2594
- * @returns {AsyncGenerator<Array>}
3365
+ * @returns {AsyncGenerator<DocumentListEntry[]>}
2595
3366
  */
2596
3367
  iterDocuments: (id, { pageSize } = {}) =>
2597
3368
  iterPages(this, `/api/v1/projects/${id}/documents`, { pageSize }),
@@ -2603,6 +3374,7 @@ class PlaidClient {
2603
3374
  * @param {string} [auditMessage] - Custom audit-log message
2604
3375
  * @param {object} [options]
2605
3376
  * @param {number} [options.timeout] - Per-request timeout in ms, the client's own by default (0/null disables)
3377
+ * @returns {Promise<void>}
2606
3378
  */
2607
3379
  delete: (id, auditMessage, { timeout } = {}) =>
2608
3380
  this._request("DELETE", `/api/v1/projects/${id}`, {
@@ -2613,6 +3385,8 @@ class PlaidClient {
2613
3385
  * Update a project's name.
2614
3386
  * @param {string} id - The resource ID
2615
3387
  * @param {string} name - The name
3388
+ * @param {string} [auditMessage] - Audit message for this write
3389
+ * @returns {Promise<Project>}
2616
3390
  */
2617
3391
  update: (id, name, auditMessage) =>
2618
3392
  this._request("PATCH", `/api/v1/projects/${id}`, {
@@ -2622,6 +3396,7 @@ class PlaidClient {
2622
3396
  /**
2623
3397
  * List all projects accessible to user. Transparently follows pagination
2624
3398
  * cursors and returns the full flat array.
3399
+ * @returns {Promise<ProjectListEntry[]>}
2625
3400
  */
2626
3401
  list: () => listAll(this, "/api/v1/projects"),
2627
3402
  /**
@@ -2629,7 +3404,7 @@ class PlaidClient {
2629
3404
  * @param {object} [opts]
2630
3405
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
2631
3406
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
2632
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
3407
+ * @returns {Promise<Page<ProjectListEntry>>}
2633
3408
  */
2634
3409
  listPage: ({ limit, cursor } = {}) =>
2635
3410
  listPage(this, "/api/v1/projects", {
@@ -2640,7 +3415,7 @@ class PlaidClient {
2640
3415
  * Async-iterate projects page by page; yields each page's entries array.
2641
3416
  * @param {object} [opts]
2642
3417
  * @param {number} [opts.pageSize] - Per-request page size
2643
- * @returns {AsyncGenerator<Array>}
3418
+ * @returns {AsyncGenerator<ProjectListEntry[]>}
2644
3419
  */
2645
3420
  iterPages: ({ pageSize } = {}) =>
2646
3421
  iterPages(this, "/api/v1/projects", {
@@ -2649,6 +3424,10 @@ class PlaidClient {
2649
3424
  /**
2650
3425
  * Create a new project. Note: this also registers the user as a maintainer.
2651
3426
  * @param {string} name - The name
3427
+ * @param {string} [auditMessage] - Audit message for this write
3428
+ * @param {object} [opts]
3429
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
3430
+ * @returns {Promise<Created>}
2652
3431
  */
2653
3432
  create: (name, auditMessage, { id } = {}) =>
2654
3433
  this._request("POST", "/api/v1/projects", {
@@ -2658,74 +3437,16 @@ class PlaidClient {
2658
3437
  };
2659
3438
 
2660
3439
  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
- }),
3440
+ ...configMethods(this, "text-layers"),
3441
+ ...layerMethods(this, "text-layers"),
2725
3442
  /**
2726
3443
  * Create a new text layer for a project.
2727
3444
  * @param {string} projectId - The project ID
2728
3445
  * @param {string} name - The name
3446
+ * @param {string} [auditMessage] - Audit message for this write
3447
+ * @param {object} [opts]
3448
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
3449
+ * @returns {Promise<Created>}
2729
3450
  */
2730
3451
  create: (projectId, name, auditMessage, { id } = {}) =>
2731
3452
  this._request("POST", "/api/v1/text-layers", {
@@ -2735,39 +3456,16 @@ class PlaidClient {
2735
3456
  };
2736
3457
 
2737
3458
  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
- }),
3459
+ ...metadataMethods(this, "vocab-items"),
2766
3460
  /**
2767
3461
  * Create a new vocab item
2768
3462
  * @param {string} vocabLayerId - The vocab layer ID
2769
3463
  * @param {string} form - The vocab item form
2770
3464
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
3465
+ * @param {string} [auditMessage] - Audit message for this write
3466
+ * @param {object} [opts]
3467
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
3468
+ * @returns {Promise<Created>}
2771
3469
  */
2772
3470
  create: (vocabLayerId, form, metadata, auditMessage, { id } = {}) =>
2773
3471
  this._request("POST", "/api/v1/vocab-items", {
@@ -2778,6 +3476,8 @@ class PlaidClient {
2778
3476
  * Create multiple vocab items in a single operation. Entries may target
2779
3477
  * different vocab layers; the user must have write access to each.
2780
3478
  * @param {Array<{vocabLayerId: string, form: string, metadata?: any}>} body - The vocab items to create
3479
+ * @param {string} [auditMessage] - Audit message for this write
3480
+ * @returns {Promise<{ids: string[]}>}
2781
3481
  */
2782
3482
  bulkCreate: (body, auditMessage) =>
2783
3483
  this._request("POST", "/api/v1/vocab-items/bulk", {
@@ -2788,6 +3488,7 @@ class PlaidClient {
2788
3488
  * Update many vocab items in a single operation: set forms and/or patch
2789
3489
  * metadata.
2790
3490
  * @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.
3491
+ * @param {string} [auditMessage] - Audit message for this write
2791
3492
  * @returns {Promise<{count: number}>} How many vocab items were updated.
2792
3493
  */
2793
3494
  bulkUpdate: (body, auditMessage) =>
@@ -2802,6 +3503,8 @@ class PlaidClient {
2802
3503
  * Every document holding one of those links has its version bumped, and
2803
3504
  * a strict-mode client picks up their new versions from the response.
2804
3505
  * @param {string[]} body - The vocab item IDs to delete
3506
+ * @param {string} [auditMessage] - Audit message for this write
3507
+ * @returns {Promise<void>}
2805
3508
  */
2806
3509
  bulkDelete: (body, auditMessage) =>
2807
3510
  this._request("DELETE", "/api/v1/vocab-items/bulk", {
@@ -2811,6 +3514,7 @@ class PlaidClient {
2811
3514
  /**
2812
3515
  * Get a vocab item by ID
2813
3516
  * @param {string} id - The resource ID
3517
+ * @returns {Promise<VocabItem>}
2814
3518
  */
2815
3519
  get: (id) =>
2816
3520
  this._request("GET", `/api/v1/vocab-items/${id}`),
@@ -2823,6 +3527,7 @@ class PlaidClient {
2823
3527
  * @param {{expectedLinkCount?: number}} [options] - `expectedLinkCount`: the
2824
3528
  * number of links the caller showed. The delete is refused with a 409 when
2825
3529
  * the entry has any other number of links by then.
3530
+ * @returns {Promise<void>}
2826
3531
  */
2827
3532
  delete: (id, auditMessage, { expectedLinkCount } = {}) =>
2828
3533
  this._request("DELETE", `/api/v1/vocab-items/${id}`, {
@@ -2844,6 +3549,7 @@ class PlaidClient {
2844
3549
  * Needs maintainer rights on the vocabulary.
2845
3550
  * @param {string} survivorId - The entry that stays
2846
3551
  * @param {string[]} loserIds - The entries merged into it
3552
+ * @param {string} [auditMessage] - Audit message for this write
2847
3553
  * @returns {Promise<{moved: number, duplicates: number, removed: string[]}>}
2848
3554
  */
2849
3555
  merge: (survivorId, loserIds, auditMessage) =>
@@ -2858,6 +3564,8 @@ class PlaidClient {
2858
3564
  * from the response.
2859
3565
  * @param {string} id - The resource ID
2860
3566
  * @param {string} form - The vocab item form
3567
+ * @param {string} [auditMessage] - Audit message for this write
3568
+ * @returns {Promise<VocabItem>}
2861
3569
  */
2862
3570
  update: (id, form, auditMessage) =>
2863
3571
  this._request("PATCH", `/api/v1/vocab-items/${id}`, {
@@ -2867,82 +3575,24 @@ class PlaidClient {
2867
3575
  };
2868
3576
 
2869
3577
  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
- ),
3578
+ ...layerMethods(this, "relation-layers"),
2881
3579
  /**
2882
3580
  * Create a new relation layer.
2883
3581
  * @param {string} spanLayerId - The span layer ID
2884
3582
  * @param {string} name - The name
3583
+ * @param {string} [auditMessage] - Audit message for this write
3584
+ * @param {object} [opts]
3585
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
3586
+ * @returns {Promise<Created>}
2885
3587
  */
2886
3588
  create: (spanLayerId, name, auditMessage, { id } = {}) =>
2887
3589
  this._request("POST", "/api/v1/relation-layers", {
2888
3590
  auditMessage,
2889
3591
  body: bodyOf({ id, "span-layer-id": spanLayerId, name }),
2890
3592
  }),
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
- ),
3593
+ ...configMethods(this, "relation-layers"),
2921
3594
  ...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
- }),
3595
+ ...valueSetMethods(this, "relation-layers"),
2946
3596
  };
2947
3597
 
2948
3598
  this.tokens = {
@@ -2963,6 +3613,10 @@ class PlaidClient {
2963
3613
  * @param {number} end - End offset, exclusive (Unicode code points)
2964
3614
  * @param {number} [precedence] - Ordering precedence
2965
3615
  * @param {any} [metadata] - Metadata map. Omit to leave unset; pass null to send JSON null.
3616
+ * @param {string} [auditMessage] - Audit message for this write
3617
+ * @param {object} [opts]
3618
+ * @param {string} [opts.id] - The new entity's id, minted by the caller (see uuidv7)
3619
+ * @returns {Promise<Created>}
2966
3620
  */
2967
3621
  create: (
2968
3622
  tokenLayerId,
@@ -2989,6 +3643,7 @@ class PlaidClient {
2989
3643
  /**
2990
3644
  * Get a token.
2991
3645
  * @param {string} tokenId - The token ID
3646
+ * @returns {Promise<Token>}
2992
3647
  */
2993
3648
  get: (tokenId) =>
2994
3649
  this._request("GET", `/api/v1/tokens/${tokenId}`),
@@ -2996,6 +3651,8 @@ class PlaidClient {
2996
3651
  * Delete a token and remove it from any spans. If this causes a span to
2997
3652
  * have no remaining tokens, the span will also be deleted.
2998
3653
  * @param {string} tokenId - The token ID
3654
+ * @param {string} [auditMessage] - Audit message for this write
3655
+ * @returns {Promise<void>}
2999
3656
  */
3000
3657
  delete: (tokenId, auditMessage) =>
3001
3658
  this._request("DELETE", `/api/v1/tokens/${tokenId}`, { auditMessage }),
@@ -3008,6 +3665,8 @@ class PlaidClient {
3008
3665
  * to leave unchanged; pass a number to set; pass null explicitly to
3009
3666
  * CLEAR it (revert to no explicit ordering). bodyOf keeps null but
3010
3667
  * drops undefined, so the three cases map correctly to the server.
3668
+ * @param {string} [auditMessage] - Audit message for this write
3669
+ * @returns {Promise<Token>}
3011
3670
  */
3012
3671
  update: (tokenId, begin, end, precedence, auditMessage) =>
3013
3672
  this._request("PATCH", `/api/v1/tokens/${tokenId}`, {
@@ -3017,6 +3676,7 @@ class PlaidClient {
3017
3676
  /**
3018
3677
  * Create multiple tokens in a single operation.
3019
3678
  * @param {Array} body - The request body
3679
+ * @param {string} [auditMessage] - Audit message for this write
3020
3680
  * @returns {Promise<{ids: string[]}>} The created entity IDs, in input order. (bulkDelete resolves to no value.)
3021
3681
  */
3022
3682
  bulkCreate: (body, auditMessage) =>
@@ -3024,12 +3684,15 @@ class PlaidClient {
3024
3684
  /**
3025
3685
  * Delete multiple tokens in a single operation. Provide an array of IDs.
3026
3686
  * @param {Array} body - The request body
3687
+ * @param {string} [auditMessage] - Audit message for this write
3688
+ * @returns {Promise<void>}
3027
3689
  */
3028
3690
  bulkDelete: (body, auditMessage) =>
3029
3691
  this._request("DELETE", "/api/v1/tokens/bulk", { auditMessage, body }),
3030
3692
  /**
3031
3693
  * Update many tokens in a single operation: patch metadata.
3032
3694
  * @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.
3695
+ * @param {string} [auditMessage] - Audit message for this write
3033
3696
  * @returns {Promise<{count: number}>} How many tokens were updated.
3034
3697
  */
3035
3698
  bulkUpdate: (body, auditMessage) =>
@@ -3038,8 +3701,9 @@ class PlaidClient {
3038
3701
  body,
3039
3702
  }),
3040
3703
  /**
3041
- * Split a token at a Unicode code-point offset. The original token becomes the
3042
- * left half (keeps its ID, spans, vocab-links); the new right token's ID is returned.
3704
+ * Split a token at a Unicode code-point offset. A new token is made for one
3705
+ * half and its ID is returned. The original token keeps its ID, spans and
3706
+ * vocab-links on the left half, or on the right half with `keep: "right"`.
3043
3707
  * @param {string} tokenId - The token ID
3044
3708
  * @param {number} position - Code-point offset to split at (strictly between begin and end)
3045
3709
  * @param {string} [auditMessage] - Audit message for this write
@@ -3050,6 +3714,7 @@ class PlaidClient {
3050
3714
  * half. A relation layer whose relations must stay inside one token
3051
3715
  * of this layer declares a same-ancestor constraint, and the server deletes
3052
3716
  * the relations the split leaves crossing in the same transaction.
3717
+ * @returns {Promise<Created>}
3053
3718
  */
3054
3719
  split: (
3055
3720
  tokenId,
@@ -3068,6 +3733,8 @@ class PlaidClient {
3068
3733
  * merged extent must not engulf a third token.
3069
3734
  * @param {string} tokenId - The anchor token ID
3070
3735
  * @param {string} otherTokenId - The other token to merge in
3736
+ * @param {string} [auditMessage] - Audit message for this write
3737
+ * @returns {Promise<Token>}
3071
3738
  */
3072
3739
  merge: (tokenId, otherTokenId, auditMessage) =>
3073
3740
  this._request("POST", `/api/v1/tokens/${tokenId}/merge`, {
@@ -3081,49 +3748,24 @@ class PlaidClient {
3081
3748
  * @param {string} tokenId - The token ID
3082
3749
  * @param {number} [begin] - New start offset, inclusive (Unicode code points)
3083
3750
  * @param {number} [end] - New end offset, exclusive (Unicode code points)
3751
+ * @param {string} [auditMessage] - Audit message for this write
3752
+ * @returns {Promise<Token>}
3084
3753
  */
3085
3754
  shift: (tokenId, begin, end, auditMessage) =>
3086
3755
  this._request("POST", `/api/v1/tokens/${tokenId}/shift`, {
3087
3756
  auditMessage,
3088
3757
  body: bodyOf({ begin, end }),
3089
3758
  }),
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
- }),
3759
+ ...metadataMethods(this, "tokens"),
3118
3760
  };
3119
3761
 
3120
3762
  this.messages = {
3121
3763
  /**
3122
3764
  * Open a Server-Sent Events stream for a project.
3123
3765
  * @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.
3766
+ * @param {(eventType: string, data: any) => void | boolean} onEvent - Callback function that receives (eventType, data). If it returns true, listening will stop.
3125
3767
  * @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
3768
+ * @returns {SSEConnection} SSE connection object with .close() and .getStats() methods
3127
3769
  */
3128
3770
  listen: (projectId, onEvent, path) =>
3129
3771
  createSSEConnection(this, projectId, onEvent, path),
@@ -3143,7 +3785,7 @@ class PlaidClient {
3143
3785
  * queues, so a message queued after the writes goes out after them.
3144
3786
  * @param {string} projectId - The UUID of the project to send to
3145
3787
  * @param {any} data - The message data to send
3146
- * @returns {Promise<any>} Response from the send operation
3788
+ * @returns {Promise<{success: boolean, message: string}>}
3147
3789
  */
3148
3790
  sendMessage: (projectId, data) =>
3149
3791
  this._request("POST", `/api/v1/projects/${projectId}/message`, {
@@ -3156,7 +3798,7 @@ class PlaidClient {
3156
3798
  * connected services carry `online: true`; previously-seen offline ones
3157
3799
  * carry `online: false` plus a `lastSeenAt` stamp.
3158
3800
  * @param {string} projectId - The UUID of the project to query
3159
- * @returns {Promise<Array>} Array of discovered service information
3801
+ * @returns {Promise<DiscoveredService[]>} Array of discovered service information
3160
3802
  */
3161
3803
  discoverServices: (projectId) => discoverServices(this, projectId),
3162
3804
 
@@ -3176,14 +3818,14 @@ class PlaidClient {
3176
3818
  * The registration reopens its channel whenever it drops, so a server
3177
3819
  * restart needs no service restart.
3178
3820
  * @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)
3821
+ * @param {ServiceInfo} serviceInfo - Service information {serviceId, serviceName, description}
3822
+ * @param {(data: any, responseHelper: ResponseHelper) => void} onServiceRequest - Callback (data, responseHelper)
3181
3823
  * @param {Object} [extras] - Optional additional service metadata
3182
- * @param {function} [onStatus] - Optional callback (event, projectId, detail) for
3824
+ * @param {(event: ServiceStatusEvent, projectId: string, detail?: string) => void} [onStatus] - Optional callback (event, projectId, detail) for
3183
3825
  * connection transitions: 'registered', 'reconnected', 'disconnected',
3184
3826
  * and 'stopped' when the server refuses the channel for good, which
3185
3827
  * ends the registration
3186
- * @returns {Object} Service registration with .stop(), .isRunning(), .isConnected()
3828
+ * @returns {ServiceRegistration} Service registration with .stop(), .isRunning(), .isConnected()
3187
3829
  */
3188
3830
  serve: (projectId, serviceInfo, onServiceRequest, extras, onStatus) =>
3189
3831
  serve(this, projectId, serviceInfo, onServiceRequest, extras, onStatus),
@@ -3204,7 +3846,7 @@ class PlaidClient {
3204
3846
  * @param {number} [timeout=10000] - Timeout in ms
3205
3847
  * @param {function} [onProgress] - Called with each progress payload {percent, message}
3206
3848
  * @param {AbortSignal} [signal] - Abort to stop waiting; rejects with an AbortError
3207
- * @param {Object} [opts] - {requestId, onAccepted, projectIds, noOperation}
3849
+ * @param {{requestId?: string, onAccepted?: (requestId: string) => void, projectIds?: string[], noOperation?: boolean}} [opts] - {requestId, onAccepted, projectIds, noOperation}
3208
3850
  * @returns {Promise<any>} The service's result
3209
3851
  */
3210
3852
  requestService: (
@@ -3270,6 +3912,11 @@ class PlaidClient {
3270
3912
  cancelServiceRequest(this, projectId, requestId),
3271
3913
  };
3272
3914
 
3915
+ // outOfBand: a query is a read that travels as a POST (see the note at
3916
+ // the top of http.js). Made on a batch it goes over the wire like any
3917
+ // read, and it never joins a logical operation. It waits past core's own
3918
+ // query limit, so a query too broad answers with core's 408, and a 503
3919
+ // from core's full queue of large queries is not retried.
3273
3920
  /**
3274
3921
  * Run a query over every project you can read.
3275
3922
  *
@@ -3301,16 +3948,11 @@ class PlaidClient {
3301
3948
  *
3302
3949
  * @param {Object} body - The query AST ({find, where, scope?, limit?, orderBy?,
3303
3950
  * return?, bindings?}).
3304
- * @returns {Promise<Object>} For 'ids'/'entities': {columns, results, count, truncated}.
3951
+ * @returns {Promise<QueryResult>} For 'ids'/'entities': {columns, results, count, truncated}.
3305
3952
  * For 'count': {return: 'count', count, truncated}, where `truncated` says
3306
3953
  * the count stopped at the server's cap. Entity cells are full entity
3307
3954
  * objects (same shape as the GET endpoints).
3308
3955
  */
3309
- // outOfBand: a query is a read that travels as a POST (see the note at
3310
- // the top of http.js). Made on a batch it goes over the wire like any
3311
- // read, and it never joins a logical operation. It waits past core's own
3312
- // query limit, so a query too broad answers with core's 408, and a 503
3313
- // from core's full queue of large queries is not retried.
3314
3956
  this.query = (body) =>
3315
3957
  this._request("POST", "/api/v1/query", {
3316
3958
  body,
@@ -3326,12 +3968,14 @@ class PlaidClient {
3326
3968
  /**
3327
3969
  * Get a logical-operation group (its label + creator).
3328
3970
  * @param {string} id - The group id
3971
+ * @returns {Promise<OperationGroup>}
3329
3972
  */
3330
3973
  get: (id) => this._request("GET", `/api/v1/operation-groups/${id}`),
3331
3974
  /**
3332
3975
  * Relabel a logical-operation group after the fact. Owner or admin only.
3333
3976
  * @param {string} id - The group id
3334
3977
  * @param {string|null} message - The new label
3978
+ * @returns {Promise<OperationGroup>}
3335
3979
  */
3336
3980
  update: (id, message) =>
3337
3981
  this._request("PATCH", `/api/v1/operation-groups/${id}`, {
@@ -3349,11 +3993,13 @@ class PlaidClient {
3349
3993
  * A comment outlives its anchor: deleting the entity does not delete
3350
3994
  * the comment. `anchorLabel` is the caption shown once that happens
3351
3995
  * ("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
3996
+ * @param {CommentableType} entityType - What kind of thing is being commented on
3353
3997
  * @param {string} entityId - The commented entity's id
3354
3998
  * @param {string} body - The comment text (1..10000 characters)
3355
3999
  * @param {object} [opts]
3356
4000
  * @param {string} [opts.anchorLabel] - What the comment is about, in words (at most 200 characters)
4001
+ * @param {string} [opts.id] - The new comment's id, minted by the caller (see uuidv7)
4002
+ * @returns {Promise<Comment>}
3357
4003
  */
3358
4004
  create: (entityType, entityId, body, { anchorLabel, id } = {}) =>
3359
4005
  this._request("POST", "/api/v1/comments", {
@@ -3371,6 +4017,7 @@ class PlaidClient {
3371
4017
  /**
3372
4018
  * Read one comment.
3373
4019
  * @param {string} id - The comment id
4020
+ * @returns {Promise<Comment>}
3374
4021
  */
3375
4022
  get: (id) => this._request("GET", `/api/v1/comments/${id}`),
3376
4023
  /**
@@ -3378,6 +4025,7 @@ class PlaidClient {
3378
4025
  * maintainers, not admins. Sets `edited` on the comment.
3379
4026
  * @param {string} id - The comment id
3380
4027
  * @param {string} body - The replacement text
4028
+ * @returns {Promise<Comment>}
3381
4029
  */
3382
4030
  update: (id, body) =>
3383
4031
  this._request("PATCH", `/api/v1/comments/${id}`, {
@@ -3388,6 +4036,7 @@ class PlaidClient {
3388
4036
  * Delete a comment. The author may delete their own; a project
3389
4037
  * maintainer (or admin) may delete any.
3390
4038
  * @param {string} id - The comment id
4039
+ * @returns {Promise<void>}
3391
4040
  */
3392
4041
  delete: (id) =>
3393
4042
  this._request("DELETE", `/api/v1/comments/${id}`, { noOperation: true }),
@@ -3399,6 +4048,7 @@ class PlaidClient {
3399
4048
  * @param {string} [filters.documentId] - Every comment anywhere in one document
3400
4049
  * @param {string} [filters.entityType] - With entityId, one entity's thread
3401
4050
  * @param {string} [filters.entityId] - With entityType, one entity's thread
4051
+ * @returns {Promise<Comment[]>}
3402
4052
  */
3403
4053
  list: (projectId, { documentId, entityType, entityId } = {}) =>
3404
4054
  listAll(this, `/api/v1/projects/${projectId}/comments`, {
@@ -3417,7 +4067,7 @@ class PlaidClient {
3417
4067
  * @param {string} [opts.documentId] - Every comment anywhere in one document
3418
4068
  * @param {string} [opts.entityType] - With entityId, one entity's thread
3419
4069
  * @param {string} [opts.entityId] - With entityType, one entity's thread
3420
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
4070
+ * @returns {Promise<Page<Comment>>}
3421
4071
  */
3422
4072
  listPage: (
3423
4073
  projectId,
@@ -3440,7 +4090,7 @@ class PlaidClient {
3440
4090
  * @param {string} [opts.documentId] - Every comment anywhere in one document
3441
4091
  * @param {string} [opts.entityType] - With entityId, one entity's thread
3442
4092
  * @param {string} [opts.entityId] - With entityType, one entity's thread
3443
- * @returns {AsyncGenerator<Array>}
4093
+ * @returns {AsyncGenerator<Comment[]>}
3444
4094
  */
3445
4095
  iterPages: (
3446
4096
  projectId,
@@ -3463,7 +4113,7 @@ class PlaidClient {
3463
4113
  * The response is NOT key-transformed: its keys are entity ids, and
3464
4114
  * camelCasing would mangle the hyphens in a UUID.
3465
4115
  * @param {string} projectId - The project to read
3466
- * @param {object} [filters] - Same filters as list()
4116
+ * @param {{documentId?: string, entityType?: CommentableType, entityId?: string}} [filters] - Same filters as list()
3467
4117
  * @returns {Promise<Object<string, number>>}
3468
4118
  */
3469
4119
  counts: (projectId, { documentId, entityType, entityId } = {}) =>
@@ -3482,6 +4132,7 @@ class PlaidClient {
3482
4132
  * @param {string} vocabId - The vocab layer to read
3483
4133
  * @param {object} [filters]
3484
4134
  * @param {string} [filters.entityId] - One entry's thread
4135
+ * @returns {Promise<Comment[]>}
3485
4136
  */
3486
4137
  listInVocab: (vocabId, { entityId } = {}) =>
3487
4138
  listAll(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3494,7 +4145,7 @@ class PlaidClient {
3494
4145
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
3495
4146
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
3496
4147
  * @param {string} [opts.entityId] - One entry's thread
3497
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
4148
+ * @returns {Promise<Page<Comment>>}
3498
4149
  */
3499
4150
  listInVocabPage: (vocabId, { limit, cursor, entityId } = {}) =>
3500
4151
  listPage(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3509,7 +4160,7 @@ class PlaidClient {
3509
4160
  * @param {object} [opts]
3510
4161
  * @param {string} [opts.entityId] - One entry's thread
3511
4162
  * @param {number} [opts.pageSize] - Per-request page size
3512
- * @returns {AsyncGenerator<Array>}
4163
+ * @returns {AsyncGenerator<Comment[]>}
3513
4164
  */
3514
4165
  iterInVocabPages: (vocabId, { entityId, pageSize } = {}) =>
3515
4166
  iterPages(this, `/api/v1/vocab-layers/${vocabId}/comments`, {
@@ -3537,15 +4188,15 @@ class PlaidClient {
3537
4188
 
3538
4189
  this.events = {
3539
4190
  /**
3540
- * Record one research-telemetry event (core manual, "Research
3541
- * telemetry"). Fire and forget: the event is buffered and sent with
4191
+ * Record one research-telemetry event (plaid-core sql/client_event.clj).
4192
+ * Fire and forget: the event is buffered and sent with
3542
4193
  * others every ten seconds, at fifty events, and when the page is
3543
4194
  * hidden. A batch that fails is dropped, nothing is retried, and
3544
4195
  * nothing is ever thrown. Sends nothing for a project whose switch
3545
4196
  * (`config.plaid.research.telemetry`) is off, which it reads from the
3546
4197
  * project itself. `suggestion.shown` is recorded once per target,
3547
4198
  * field and value in a page session.
3548
- * @param {'suggestion.shown'|'suggestion.adopted'|'suggestion.dismissed'|'plan.opened'} type - The event type
4199
+ * @param {ClientEventType} type - The event type
3549
4200
  * @param {object} fields
3550
4201
  * @param {string} fields.projectId - The project it happened in
3551
4202
  * @param {string} [fields.documentId] - The document it happened in
@@ -3556,6 +4207,7 @@ class PlaidClient {
3556
4207
  record: (type, fields) => recorderFor(this).record(this, type, fields),
3557
4208
  /**
3558
4209
  * Send what the recorder holds now, rather than at its next interval.
4210
+ * @returns {void}
3559
4211
  */
3560
4212
  flush: () => recorderFor(this).flush(),
3561
4213
  /**
@@ -3563,6 +4215,7 @@ class PlaidClient {
3563
4215
  * maintainer changed it. Turning it off drops what is buffered.
3564
4216
  * @param {string} projectId - The project
3565
4217
  * @param {boolean} on - Whether telemetry is on
4218
+ * @returns {void}
3566
4219
  */
3567
4220
  setEnabled: (projectId, on) => recorderFor(this).setEnabled(projectId, on),
3568
4221
  /**
@@ -3587,6 +4240,7 @@ class PlaidClient {
3587
4240
  * @param {string|string[]} [filters.types] - Only these types
3588
4241
  * @param {string} [filters.startTime] - Only events the server stamped at or after this instant
3589
4242
  * @param {string} [filters.endTime] - Only events the server stamped at or before this instant
4243
+ * @returns {Promise<ClientEvent[]>}
3590
4244
  */
3591
4245
  list: (projectId, { types, startTime, endTime } = {}) =>
3592
4246
  listAll(this, `/api/v1/projects/${projectId}/events`, {
@@ -3601,7 +4255,7 @@ class PlaidClient {
3601
4255
  * @param {string|string[]} [opts.types] - Only these types
3602
4256
  * @param {string} [opts.startTime] - Only events at or after this instant
3603
4257
  * @param {string} [opts.endTime] - Only events at or before this instant
3604
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
4258
+ * @returns {Promise<Page<ClientEvent>>}
3605
4259
  */
3606
4260
  listPage: (
3607
4261
  projectId,
@@ -3620,7 +4274,7 @@ class PlaidClient {
3620
4274
  * @param {string|string[]} [opts.types] - Only these types
3621
4275
  * @param {string} [opts.startTime] - Only events at or after this instant
3622
4276
  * @param {string} [opts.endTime] - Only events at or before this instant
3623
- * @returns {AsyncGenerator<Array>}
4277
+ * @returns {AsyncGenerator<ClientEvent[]>}
3624
4278
  */
3625
4279
  iterPages: (projectId, { pageSize, types, startTime, endTime } = {}) =>
3626
4280
  iterPages(this, `/api/v1/projects/${projectId}/events`, {
@@ -3641,6 +4295,9 @@ class PlaidClient {
3641
4295
  * @param {object} [opts]
3642
4296
  * @param {string} [opts.body] - The Markdown text (up to 20000 characters; may be empty)
3643
4297
  * @param {boolean} [opts.pinned] - Send this one to the assistant in full on every turn
4298
+ * @param {string} [opts.id] - The new guideline's id, minted by the caller (see uuidv7)
4299
+ * @param {string} [auditMessage] - Audit message for this write
4300
+ * @returns {Promise<{id: string}>}
3644
4301
  */
3645
4302
  create: (projectId, title, { body, pinned, id } = {}, auditMessage) =>
3646
4303
  this._request("POST", `/api/v1/projects/${projectId}/guidelines`, {
@@ -3650,6 +4307,7 @@ class PlaidClient {
3650
4307
  /**
3651
4308
  * Read one guideline, Markdown body included.
3652
4309
  * @param {string} id - The guideline id
4310
+ * @returns {Promise<Guideline>}
3653
4311
  */
3654
4312
  get: (id) => this._request("GET", `/api/v1/guidelines/${id}`),
3655
4313
  /**
@@ -3666,6 +4324,8 @@ class PlaidClient {
3666
4324
  * @param {string} [changes.body] - The new Markdown text
3667
4325
  * @param {boolean} [changes.pinned] - Whether the assistant always gets it in full
3668
4326
  * @param {string} [changes.expectedUpdatedAt] - Write only if this is still the stored updatedAt
4327
+ * @param {string} [auditMessage] - Audit message for this write
4328
+ * @returns {Promise<Guideline>}
3669
4329
  */
3670
4330
  update: (
3671
4331
  id,
@@ -3680,6 +4340,8 @@ class PlaidClient {
3680
4340
  /**
3681
4341
  * Delete a guideline.
3682
4342
  * @param {string} id - The guideline id
4343
+ * @param {string} [auditMessage] - Audit message for this write
4344
+ * @returns {Promise<void>}
3683
4345
  */
3684
4346
  delete: (id, auditMessage) =>
3685
4347
  this._request("DELETE", `/api/v1/guidelines/${id}`, { auditMessage }),
@@ -3694,6 +4356,7 @@ class PlaidClient {
3694
4356
  * @param {string} projectId - The project to read
3695
4357
  * @param {object} [opts]
3696
4358
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
4359
+ * @returns {Promise<Guideline[]>}
3697
4360
  */
3698
4361
  list: (projectId, { includeBodies } = {}) =>
3699
4362
  listAll(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3706,7 +4369,7 @@ class PlaidClient {
3706
4369
  * @param {number} [opts.limit] - Page size (1..1000; server default 100)
3707
4370
  * @param {string} [opts.cursor] - Opaque cursor from a previous page
3708
4371
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3709
- * @returns {Promise<{entries: Array, nextCursor: (string|null)}>}
4372
+ * @returns {Promise<Page<Guideline>>}
3710
4373
  */
3711
4374
  listPage: (projectId, { limit, cursor, includeBodies } = {}) =>
3712
4375
  listPage(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3720,7 +4383,7 @@ class PlaidClient {
3720
4383
  * @param {object} [opts]
3721
4384
  * @param {number} [opts.pageSize] - Per-request page size
3722
4385
  * @param {boolean} [opts.includeBodies] - Return each body instead of its length
3723
- * @returns {AsyncGenerator<Array>}
4386
+ * @returns {AsyncGenerator<Guideline[]>}
3724
4387
  */
3725
4388
  iterPages: (projectId, { pageSize, includeBodies } = {}) =>
3726
4389
  iterPages(this, `/api/v1/projects/${projectId}/guidelines`, {
@@ -3732,6 +4395,7 @@ class PlaidClient {
3732
4395
 
3733
4396
  // --- Core methods ---
3734
4397
 
4398
+ /** @private */
3735
4399
  async _request(method, path, options = {}) {
3736
4400
  return makeRequest(this, method, path, options);
3737
4401
  }
@@ -3794,7 +4458,7 @@ class PlaidClient {
3794
4458
  * matching end, and the outer numbering resumes after it.
3795
4459
  *
3796
4460
  * @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.
4461
+ * @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
4462
  * @returns {string} The operation's group id.
3799
4463
  */
3800
4464
  beginOperation(message, { groupId, kind, ref, keys, minted } = {}) {
@@ -3843,7 +4507,7 @@ class PlaidClient {
3843
4507
  * A seed for the Idempotency-Keys of a logical operation that may be run
3844
4508
  * again from the top (see beginOperation's `keys`). Keep it with the work
3845
4509
  * it belongs to and pass it to every run.
3846
- * @returns {{seed: string, stamps: Map<number, number|null>}}
4510
+ * @returns {KeySeed}
3847
4511
  */
3848
4512
  keySeed() {
3849
4513
  return { seed: uuidv7(), stamps: new Map() };
@@ -3898,10 +4562,11 @@ class PlaidClient {
3898
4562
  *
3899
4563
  * await client.withOperation('Import ELAN corpus', run, { kind: 'import', ref: 'format:elan' });
3900
4564
  *
4565
+ * @template T
3901
4566
  * @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.
4567
+ * @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.
4568
+ * @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.
4569
+ * @returns {Promise<T>} Whatever `fn` resolves to.
3905
4570
  */
3906
4571
  async withOperation(message, fn, { kind, ref, groupId, keys, minted } = {}) {
3907
4572
  this.beginOperation(message, { kind, ref, groupId, keys, minted });
@@ -3956,10 +4621,15 @@ class PlaidClient {
3956
4621
  * parents created earlier in the same batch, and any op's failure rolls the
3957
4622
  * whole batch back. A batch is not nestable. Prefer `batched()`, which
3958
4623
  * submits or aborts for you.
3959
- * @returns {PlaidClient} the batch view
4624
+ *
4625
+ * `timeout` (ms, or null for none) is how long each of this batch's
4626
+ * requests may wait for its answer, in place of the client's
4627
+ * `batchTimeout`, for this batch alone.
4628
+ * @param {{timeout?: number | null}} [options]
4629
+ * @returns {PlaidBatch} the batch view
3960
4630
  */
3961
- batch() {
3962
- return openBatch(this);
4631
+ batch({ timeout } = {}) {
4632
+ return openBatch(this, { timeout });
3963
4633
  }
3964
4634
 
3965
4635
  /**
@@ -3968,8 +4638,10 @@ class PlaidClient {
3968
4638
  * recased: the headers keep the server's spelling. `key` is the request's
3969
4639
  * Idempotency-Key: a batch whose answer is lost (no response, 502, 504) is
3970
4640
  * sent again under it, and answered from the first send if that landed.
4641
+ * `timeout` is this request's, in place of `batchTimeout` when given.
4642
+ * @private
3971
4643
  */
3972
- async _postBatch(url, body, { key = uuidv7() } = {}) {
4644
+ async _postBatch(url, body, { key = uuidv7(), timeout } = {}) {
3973
4645
  const fetchOptions = {
3974
4646
  method: "POST",
3975
4647
  headers: {
@@ -3985,7 +4657,9 @@ class PlaidClient {
3985
4657
  // repeating it is safe. A fresh timeout signal per attempt — an
3986
4658
  // AbortSignal.timeout stays aborted once it fires.
3987
4659
  const attempt = async () => {
3988
- const signal = timeoutSignal(this.batchTimeout);
4660
+ const signal = timeoutSignal(
4661
+ timeout !== undefined ? timeout : this.batchTimeout,
4662
+ );
3989
4663
  let res;
3990
4664
  try {
3991
4665
  res = await fetch(
@@ -4099,12 +4773,14 @@ class PlaidClient {
4099
4773
  * });
4100
4774
  *
4101
4775
  * A write made on `client` inside `fn` is not part of the batch: it goes
4102
- * over the wire at once, as it would anywhere else. See `batch()`.
4103
- * @param {(batch: PlaidClient) => (void | Promise<void>)} fn
4104
- * @returns {Promise<Array>}
4776
+ * over the wire at once, as it would anywhere else. See `batch()`, which
4777
+ * also says what `timeout` does.
4778
+ * @param {(batch: PlaidBatch) => (void | Promise<void>)} fn
4779
+ * @param {{timeout?: number | null}} [options]
4780
+ * @returns {Promise<any[]>}
4105
4781
  */
4106
- async batched(fn) {
4107
- const batch = this.batch();
4782
+ async batched(fn, { timeout } = {}) {
4783
+ const batch = this.batch({ timeout });
4108
4784
  try {
4109
4785
  await fn(batch);
4110
4786
  } catch (e) {
@@ -4119,20 +4795,22 @@ class PlaidClient {
4119
4795
  * instance — for a launcher or status page checking whether a server is up
4120
4796
  * before anyone logs in.
4121
4797
  * @param {string} baseUrl - The API base URL
4122
- * @param {object} [options]
4798
+ * @param {{timeout?: number | null}} [options]
4799
+ * @returns {Promise<ServerHealth>}
4123
4800
  */
4124
4801
  static async health(baseUrl, options = {}) {
4125
- return anonymousGet(baseUrl, "/health", options);
4802
+ return anonymousRequest("GET", baseUrl, "/health", undefined, options);
4126
4803
  }
4127
4804
 
4128
4805
  /**
4129
4806
  * The limits this server enforces, with NO authentication and no client
4130
4807
  * instance. See the instance method for the shape.
4131
4808
  * @param {string} baseUrl - The API base URL
4132
- * @param {object} [options]
4809
+ * @param {{timeout?: number | null}} [options]
4810
+ * @returns {Promise<{limits: ServerLimits}>}
4133
4811
  */
4134
4812
  static async info(baseUrl, options = {}) {
4135
- return anonymousGet(baseUrl, "/api/v1/info", options);
4813
+ return anonymousRequest("GET", baseUrl, "/api/v1/info", undefined, options);
4136
4814
  }
4137
4815
 
4138
4816
  /**
@@ -4163,7 +4841,7 @@ class PlaidClient {
4163
4841
  * @returns {Promise<{kind: string, status: string, expiresAt: string, projectName?: string, projectRole?: string, email?: string, grantAdmin: boolean}>}
4164
4842
  */
4165
4843
  static async lookupInvite(baseUrl, code, options = {}) {
4166
- return anonymousPost(baseUrl, "/api/v1/invites/lookup", { code }, options);
4844
+ return anonymousRequest("POST", baseUrl, "/api/v1/invites/lookup", { code }, options);
4167
4845
  }
4168
4846
 
4169
4847
  /**
@@ -4192,10 +4870,16 @@ class PlaidClient {
4192
4870
  { email, password, displayName } = {},
4193
4871
  options = {},
4194
4872
  ) {
4195
- const data = await anonymousPost(
4873
+ const data = await anonymousRequest(
4874
+ "POST",
4196
4875
  baseUrl,
4197
4876
  "/api/v1/invites/redeem",
4198
- bodyOf({ code, email, password, "display-name": displayName }),
4877
+ bodyOf({
4878
+ code,
4879
+ email: normalizeUserId(email),
4880
+ password,
4881
+ "display-name": displayName,
4882
+ }),
4199
4883
  options,
4200
4884
  );
4201
4885
  return {
@@ -4220,35 +4904,14 @@ class PlaidClient {
4220
4904
  */
4221
4905
  static async login(baseUrl, userId, password, options = {}) {
4222
4906
  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
- }
4907
+ const data = await anonymousRequest(
4908
+ "POST",
4909
+ baseUrl,
4910
+ "/api/v1/login",
4911
+ { "user-id": normalizeUserId(userId), password },
4912
+ options,
4913
+ );
4914
+ return new PlaidClient(baseUrl, data.token || "", options);
4252
4915
  }
4253
4916
  }
4254
4917
 
@@ -4262,23 +4925,156 @@ class PlaidClient {
4262
4925
  * neither body is re-cased.
4263
4926
  */
4264
4927
  function configRequest(auditMessage, value, options, isDelete = false) {
4928
+ // The answer is a 204 with no body, so there is nothing to keep un-recased.
4265
4929
  if (!options || !Object.hasOwn(options, "expected")) {
4266
- return isDelete
4267
- ? { auditMessage, skipResponseTransform: true }
4268
- : { auditMessage, rawBody: value, skipResponseTransform: true };
4930
+ return isDelete ? { auditMessage } : { auditMessage, rawBody: value };
4269
4931
  }
4270
4932
  const expected = options.expected === undefined ? null : options.expected;
4271
4933
  return {
4272
4934
  auditMessage,
4273
4935
  queryParams: { "if-unchanged": true },
4274
4936
  rawBody: isDelete ? { expected } : { expected, value },
4275
- skipResponseTransform: true,
4937
+ };
4938
+ }
4939
+
4940
+ /**
4941
+ * The three metadata methods of a bundle whose entities carry metadata, on
4942
+ * `/api/v1/<kind>/<id>/metadata`. Metadata is opaque, so no body is re-cased.
4943
+ * Each answers the entity as its GET does.
4944
+ * @template {keyof MetadataRecords} K
4945
+ * @param {PlaidClient} client
4946
+ * @param {K} kind
4947
+ */
4948
+ function metadataMethods(client, kind) {
4949
+ const path = (id) => `/api/v1/${kind}/${id}/metadata`;
4950
+ return {
4951
+ /**
4952
+ * Replace all metadata of an entity. Keys left out of the new map are removed.
4953
+ * @param {string} id - The entity's ID
4954
+ * @param {object} body - The new metadata map
4955
+ * @param {string} [auditMessage] - Audit message for this write
4956
+ * @returns {Promise<MetadataRecords[K]>}
4957
+ */
4958
+ setMetadata: (id, body, auditMessage) =>
4959
+ client._request("PUT", path(id), { auditMessage, rawBody: body }),
4960
+ /**
4961
+ * Remove all metadata from an entity.
4962
+ * @param {string} id - The entity's ID
4963
+ * @param {string} [auditMessage] - Audit message for this write
4964
+ * @returns {Promise<MetadataRecords[K]>}
4965
+ */
4966
+ deleteMetadata: (id, auditMessage) => client._request("DELETE", path(id), { auditMessage }),
4967
+ /**
4968
+ * 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.
4969
+ * @param {string} id - The entity's ID
4970
+ * @param {MetadataOp[]} body - The metadata ops
4971
+ * @param {string} [auditMessage] - Audit message for this write
4972
+ * @returns {Promise<MetadataRecords[K]>}
4973
+ */
4974
+ patchMetadata: (id, body, auditMessage) =>
4975
+ client._request("PATCH", path(id), { auditMessage, rawBody: body }),
4976
+ };
4977
+ }
4978
+
4979
+ /**
4980
+ * The two config methods of a bundle whose records carry editor config, on
4981
+ * `/api/v1/<kind>/<id>/config/<namespace>/<key>`.
4982
+ * @param {PlaidClient} client
4983
+ * @param {string} kind
4984
+ */
4985
+ function configMethods(client, kind) {
4986
+ const path = (id, namespace, configKey) => `/api/v1/${kind}/${id}/config/${namespace}/${configKey}`;
4987
+ return {
4988
+ /**
4989
+ * Set a configuration value in an editor namespace.
4990
+ * @param {string} id - The record's ID
4991
+ * @param {string} namespace - The config namespace
4992
+ * @param {string} configKey - The config key
4993
+ * @param {any} configValue - Configuration value to set
4994
+ * @param {string} [auditMessage] - Audit message for this write
4995
+ * @param {{expected?: any}} [options] - `expected`: the value read for this key (null or undefined when it
4996
+ * was absent). The write is then refused with a 409 when the stored value differs. A value the key
4997
+ * already holds is not written and leaves no audit row, so the value read, expecting it, states in a
4998
+ * batch that the key is unchanged.
4999
+ * @returns {Promise<void>}
5000
+ */
5001
+ setConfig: (id, namespace, configKey, configValue, auditMessage, options) =>
5002
+ client._request(
5003
+ "PUT",
5004
+ path(id, namespace, configKey),
5005
+ configRequest(auditMessage, configValue, options),
5006
+ ),
5007
+ /**
5008
+ * Remove a configuration value.
5009
+ * @param {string} id - The record's ID
5010
+ * @param {string} namespace - The config namespace
5011
+ * @param {string} configKey - The config key
5012
+ * @param {string} [auditMessage] - Audit message for this write
5013
+ * @param {{expected?: any}} [options] - `expected`, as on setConfig. A key that is absent is not
5014
+ * written, so expecting null states in a batch that it is still absent.
5015
+ * @returns {Promise<void>}
5016
+ */
5017
+ deleteConfig: (id, namespace, configKey, auditMessage, options) =>
5018
+ client._request(
5019
+ "DELETE",
5020
+ path(id, namespace, configKey),
5021
+ configRequest(auditMessage, undefined, options, true),
5022
+ ),
5023
+ };
5024
+ }
5025
+
5026
+ /**
5027
+ * The get, delete, update and shift methods of the four layer bundles, on
5028
+ * `/api/v1/<kind>/<layerId>`.
5029
+ * @template {keyof LayerRecords} K
5030
+ * @param {PlaidClient} client
5031
+ * @param {K} kind
5032
+ */
5033
+ function layerMethods(client, kind) {
5034
+ const path = (layerId) => `/api/v1/${kind}/${layerId}`;
5035
+ return {
5036
+ /**
5037
+ * Get a layer by ID.
5038
+ * @param {string} layerId - The layer ID
5039
+ * @returns {Promise<LayerRecords[K]>}
5040
+ */
5041
+ get: (layerId) => client._request("GET", path(layerId)),
5042
+ /**
5043
+ * Delete a layer.
5044
+ * @param {string} layerId - The layer ID
5045
+ * @param {string} [auditMessage] - Audit message for this write
5046
+ * @returns {Promise<void>}
5047
+ */
5048
+ delete: (layerId, auditMessage) => client._request("DELETE", path(layerId), { auditMessage }),
5049
+ /**
5050
+ * Rename a layer.
5051
+ * @param {string} layerId - The layer ID
5052
+ * @param {string} name - The new name
5053
+ * @param {string} [auditMessage] - Audit message for this write
5054
+ * @returns {Promise<LayerRecords[K]>}
5055
+ */
5056
+ update: (layerId, name, auditMessage) =>
5057
+ client._request("PATCH", path(layerId), { auditMessage, body: bodyOf({ name }) }),
5058
+ /**
5059
+ * Move a layer one place in its display order.
5060
+ * @param {string} layerId - The layer ID
5061
+ * @param {string} direction - The direction ("up" or "down")
5062
+ * @param {string} [auditMessage] - Audit message for this write
5063
+ * @returns {Promise<void>}
5064
+ */
5065
+ shift: (layerId, direction, auditMessage) =>
5066
+ client._request("POST", `${path(layerId)}/shift`, {
5067
+ auditMessage,
5068
+ body: bodyOf({ direction }),
5069
+ }),
4276
5070
  };
4277
5071
  }
4278
5072
 
4279
5073
  /**
4280
5074
  * The four layer-constraint methods of a layer bundle, on `/api/v1/<kind>`.
4281
5075
  * See constraints.js and the core manual, "Layer constraints".
5076
+ * @param {PlaidClient} client
5077
+ * @param {string} kind
4282
5078
  */
4283
5079
  function constraintMethods(client, kind) {
4284
5080
  const base = (layerId) => `/api/v1/${kind}/${layerId}/constraints`;
@@ -4289,12 +5085,12 @@ function constraintMethods(client, kind) {
4289
5085
  * stored data breaks them (see violationsOf).
4290
5086
  * @param {string} layerId - The layer ID
4291
5087
  * @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"]}]
5088
+ * @param {LayerConstraint[]} constraints - e.g. [{type: "single-span"}, {type: "value-set", values: ["N"]}]
4293
5089
  * @param {string} [auditMessage] - Audit message for this write
4294
5090
  * @param {{expected?: Array<object>|null}} [options] - `expected`: the list read for this
4295
5091
  * namespace (null when it was absent). The write is then refused with a 409 when the stored
4296
5092
  * list differs.
4297
- * @returns {Promise<{constraints: object}>} the layer's whole constraint map
5093
+ * @returns {Promise<{constraints: Record<string, LayerConstraint[]>}>} the layer's whole constraint map
4298
5094
  */
4299
5095
  setConstraints: (layerId, namespace, constraints, auditMessage, options) =>
4300
5096
  client._request("PUT", `${base(layerId)}/${namespace}`, {
@@ -4307,6 +5103,7 @@ function constraintMethods(client, kind) {
4307
5103
  * @param {string} namespace - The declaring app's namespace
4308
5104
  * @param {string} [auditMessage] - Audit message for this write
4309
5105
  * @param {{expected?: Array<object>|null}} [options] - `expected`, as on setConstraints.
5106
+ * @returns {Promise<void>}
4310
5107
  */
4311
5108
  deleteConstraints: (layerId, namespace, auditMessage, options) => {
4312
5109
  const body = constraintsBody(undefined, options);
@@ -4319,11 +5116,17 @@ function constraintMethods(client, kind) {
4319
5116
  * The violations the given constraints would meet in this layer's stored
4320
5117
  * data. Writes nothing.
4321
5118
  * @param {string} layerId - The layer ID
4322
- * @param {Array<object>} constraints - The list to check
4323
- * @returns {Promise<{violations: Array<object>, violationCount: number}>}
5119
+ * @param {LayerConstraint[]} constraints - The list to check
5120
+ * @param {{limit?: number}} [options] - `limit`: how many violations to list (default 100)
5121
+ * @returns {Promise<{violations: ConstraintViolation[], violationCount: number}>}
4324
5122
  */
4325
- checkConstraints: (layerId, constraints) =>
4326
- client._request("POST", `${base(layerId)}/check`, { body: { constraints } }),
5123
+ checkConstraints: (layerId, constraints, options) =>
5124
+ client._request("POST", `${base(layerId)}/check`, {
5125
+ body: {
5126
+ constraints,
5127
+ ...(options?.limit != null ? { limit: options.limit } : {}),
5128
+ },
5129
+ }),
4327
5130
  /**
4328
5131
  * Apply the remedies of the given constraints' remediable types
4329
5132
  * (coextensive, single-span, single-link, same-ancestor) to every
@@ -4332,10 +5135,10 @@ function constraintMethods(client, kind) {
4332
5135
  * ask. A document another user holds the lock on is left as it is and
4333
5136
  * listed under `locked`.
4334
5137
  * @param {string} layerId - The layer ID
4335
- * @param {Array<object>} constraints - The list to repair for
5138
+ * @param {LayerConstraint[]} constraints - The list to repair for
4336
5139
  * @param {string} [auditMessage] - Audit message for this write
4337
5140
  * @param {{document?: string}} [options]
4338
- * @returns {Promise<{repaired: Array<object>, locked: Array<{document: string, lockedBy: string}>, violations: Array<object>, violationCount: number}>}
5141
+ * @returns {Promise<{repaired: ConstraintRepair[], locked: Array<{document: string, lockedBy: string}>, violations: ConstraintViolation[], violationCount: number}>}
4339
5142
  */
4340
5143
  repairConstraints: (layerId, constraints, auditMessage, options) =>
4341
5144
  client._request("POST", `${base(layerId)}/repair`, {
@@ -4345,6 +5148,38 @@ function constraintMethods(client, kind) {
4345
5148
  };
4346
5149
  }
4347
5150
 
5151
+ /**
5152
+ * The value-set read of a span or relation layer bundle, on
5153
+ * `/api/v1/<kind>/<layerId>/constraints/refused-values`.
5154
+ * @param {PlaidClient} client
5155
+ * @param {string} kind
5156
+ */
5157
+ function valueSetMethods(client, kind) {
5158
+ return {
5159
+ /**
5160
+ * The stored values of this layer that a value-set rule would refuse,
5161
+ * before it is declared: exactly what a declaration of it would meet,
5162
+ * with the rule's exemptions (an unconfirmed machine value, a value an
5163
+ * import, a document copy or a restore set). Writes nothing, and a
5164
+ * reader may ask.
5165
+ * @param {string} layerId - The layer ID
5166
+ * @param {ValueSetRule} rule - The list as a value-set declaration takes it
5167
+ * @param {{limit?: number}} [options] - `limit`: how many values to list, the most frequent first
5168
+ * (default 20, at most 100)
5169
+ * @returns {Promise<RefusedValues>}
5170
+ */
5171
+ refusedValues: (layerId, rule, options) =>
5172
+ client._request("POST", `/api/v1/${kind}/${layerId}/constraints/refused-values`, {
5173
+ body: bodyOf({
5174
+ values: rule?.values,
5175
+ delimiters: rule?.delimiters,
5176
+ parts: rule?.parts,
5177
+ limit: options?.limit,
5178
+ }),
5179
+ }),
5180
+ };
5181
+ }
5182
+
4348
5183
  export default PlaidClient;
4349
5184
  export { PlaidClient };
4350
5185
  export { valueSetAllows, violationsOf } from "./constraints.js";
@@ -4357,6 +5192,7 @@ export {
4357
5192
  cpSlicer,
4358
5193
  utf16ToCp,
4359
5194
  cpIndexOf,
5195
+ composeText,
4360
5196
  } from "./codepoint.js";
4361
5197
  export { composeTextEdits, gapsToOps, applyTextOps } from "./textEdits.js";
4362
5198
  export {
@@ -4385,6 +5221,9 @@ export {
4385
5221
  // request, and `critical()` holds that off around writes. See ./services.js
4386
5222
  // and the manual, "Stopping a request".
4387
5223
  export { ServiceCancelled, createCancelScope } from "./services.js";
5224
+ // The two messages a write that got no answer is reported with: one the
5225
+ // server may have saved, one never sent. See ./services.js.
5226
+ export { UNKNOWN_OUTCOME, UNREACHABLE } from "./services.js";
4388
5227
  // Who asked for a service run, named in its History label and in what it
4389
5228
  // stores. See ./requester.js.
4390
5229
  export { REQUESTED_BY, makeRequester, requesterOf } from "./requester.js";
@@ -4431,6 +5270,6 @@ export { createdId, createdIds } from "./created.js";
4431
5270
  // UUIDv7 ids for what a client creates. See ./ids.js and the manual,
4432
5271
  // "Retrying a write". uuidv4 works where crypto.randomUUID does not (a page
4433
5272
  // served over plain HTTP from any host but localhost).
4434
- export { uuidv4, uuidv7 } from "./ids.js";
5273
+ export { uuidv4, uuidv7, normalizeUserId } from "./ids.js";
4435
5274
  export { wasReplayed } from "./replayed.js";
4436
5275
  export { MAX_BATCH_OPS };