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

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.
Files changed (49) hide show
  1. package/package.json +3 -2
  2. package/src/apiAnnotations.d.ts +508 -0
  3. package/src/apiAnnotations.js +543 -0
  4. package/src/apiDocuments.d.ts +569 -0
  5. package/src/apiDocuments.js +551 -0
  6. package/src/apiLayers.d.ts +369 -0
  7. package/src/apiLayers.js +222 -0
  8. package/src/apiMessages.d.ts +293 -0
  9. package/src/apiMessages.js +265 -0
  10. package/src/apiPeople.d.ts +626 -0
  11. package/src/apiPeople.js +698 -0
  12. package/src/apiProject.d.ts +761 -0
  13. package/src/apiProject.js +774 -0
  14. package/src/apiServer.d.ts +455 -0
  15. package/src/apiServer.js +422 -0
  16. package/src/apiVocab.d.ts +456 -0
  17. package/src/apiVocab.js +425 -0
  18. package/src/batch.d.ts +54 -0
  19. package/src/batch.js +323 -0
  20. package/src/client.d.ts +1321 -0
  21. package/src/client.js +743 -0
  22. package/src/constraints.d.ts +8 -0
  23. package/src/constraints.js +14 -0
  24. package/src/documentLock.d.ts +18 -4
  25. package/src/documentLock.js +43 -9
  26. package/src/entryForms.d.ts +63 -0
  27. package/src/entryForms.js +152 -0
  28. package/src/events.js +2 -1
  29. package/src/families.d.ts +349 -0
  30. package/src/families.js +396 -0
  31. package/src/http.d.ts +39 -18
  32. package/src/http.js +127 -87
  33. package/src/index.d.ts +90 -4557
  34. package/src/index.js +113 -5172
  35. package/src/operations.d.ts +36 -0
  36. package/src/operations.js +164 -0
  37. package/src/provenance.d.ts +47 -4
  38. package/src/provenance.js +168 -12
  39. package/src/roles.d.ts +39 -0
  40. package/src/roles.js +44 -0
  41. package/src/serviceSchema.d.ts +19 -13
  42. package/src/serviceSchema.js +30 -15
  43. package/src/services.d.ts +20 -3
  44. package/src/services.js +45 -8
  45. package/src/sse.js +10 -1
  46. package/src/testing.d.ts +128 -0
  47. package/src/testing.js +379 -0
  48. package/src/types.d.ts +867 -0
  49. package/src/types.js +663 -0
@@ -0,0 +1,698 @@
1
+ /**
2
+ * The people bundles: users, their API tokens and private data, invites and
3
+ * the signed-in user's sign-ins, each a factory of the client (client.js
4
+ * installs them).
5
+ */
6
+
7
+ import { auditMethods, bodyOf } from "./families.js";
8
+ import { normalizeUserId } from "./ids.js";
9
+ import { iterPages, listAll, listPage } from "./pagination.js";
10
+
11
+ /** An avatar token is minted again once less than this is left of it. */
12
+ const AVATAR_TOKEN_RENEW_MS = 10 * 60 * 1000;
13
+ // An avatar token minted this recently is the renewal a caller asking for one
14
+ // wanted.
15
+ const AVATAR_TOKEN_FRESH_MS = 5 * 1000;
16
+
17
+ /**
18
+ * @typedef {import("./types.js").TextEditOp} TextEditOp
19
+ * @typedef {import("./types.js").TextGap} TextGap
20
+ * @typedef {import("./types.js").MetadataOp} MetadataOp
21
+ * @typedef {import("./types.js").LayerConstraint} LayerConstraint
22
+ * @typedef {import("./types.js").ConstraintWriteOptions} ConstraintWriteOptions
23
+ * @typedef {import("./types.js").ConstraintViolation} ConstraintViolation
24
+ * @typedef {import("./types.js").Requester} Requester
25
+ * @typedef {import("./types.js").BatchRef} BatchRef
26
+ * @typedef {import("./types.js").ServiceParam} ServiceParam
27
+ * @typedef {import("./types.js").ServiceInfo} ServiceInfo
28
+ * @typedef {import("./types.js").DiscoveredService} DiscoveredService
29
+ * @typedef {import("./types.js").ResponseHelper} ResponseHelper
30
+ * @typedef {import("./types.js").ServiceRegistration} ServiceRegistration
31
+ * @typedef {import("./types.js").ServiceStatusEvent} ServiceStatusEvent
32
+ * @typedef {import("./types.js").OperationGroupsBundle} OperationGroupsBundle
33
+ * @typedef {import("./types.js").PlaidClientOptions} PlaidClientOptions
34
+ * @typedef {import("./types.js").OperationKind} OperationKind
35
+ * @typedef {import("./types.js").KeySeed} KeySeed
36
+ * @typedef {import("./types.js").PlaidBatch} PlaidBatch
37
+ * @typedef {import("./types.js").AuditPageOptions} AuditPageOptions
38
+ * @typedef {import("./types.js").VocabItemRestoreSummary} VocabItemRestoreSummary
39
+ * @typedef {import("./types.js").UserDataEntry} UserDataEntry
40
+ * @typedef {import("./types.js").AdminUserDataEntry} AdminUserDataEntry
41
+ * @typedef {import("./types.js").ActivityTallyRow} ActivityTallyRow
42
+ * @typedef {import("./types.js").ConstraintRepair} ConstraintRepair
43
+ * @typedef {import("./types.js").ValueSetRule} ValueSetRule
44
+ * @typedef {import("./types.js").RefusedValues} RefusedValues
45
+ * @typedef {import("./types.js").CommentableType} CommentableType
46
+ * @typedef {import("./types.js").Comment} Comment
47
+ * @typedef {import("./types.js").Guideline} Guideline
48
+ * @typedef {import("./types.js").ClientEventType} ClientEventType
49
+ * @typedef {import("./types.js").ClientEvent} ClientEvent
50
+ * @typedef {import("./types.js").Invite} Invite
51
+ * @typedef {import("./types.js").ServerLimits} ServerLimits
52
+ * @typedef {import("./types.js").ServerHealth} ServerHealth
53
+ * @typedef {import("./types.js").RateLimitBucket} RateLimitBucket
54
+ * @typedef {import("./types.js").RateLimitSnapshot} RateLimitSnapshot
55
+ * @typedef {import("./types.js").BackupStatus} BackupStatus
56
+ * @typedef {import("./types.js").ServerReport} ServerReport
57
+ * @typedef {import("./types.js").LiveLog} LiveLog
58
+ * @typedef {import("./types.js").SSEConnection} SSEConnection
59
+ * @typedef {import("./types.js").SSEStats} SSEStats
60
+ * @typedef {import("./types.js").AtomicValue} AtomicValue
61
+ * @typedef {import("./types.js").Created} Created
62
+ * @typedef {import("./types.js").Project} Project
63
+ * @typedef {import("./types.js").ProjectTextLayer} ProjectTextLayer
64
+ * @typedef {import("./types.js").ProjectTokenLayer} ProjectTokenLayer
65
+ * @typedef {import("./types.js").ProjectSpanLayer} ProjectSpanLayer
66
+ * @typedef {import("./types.js").ProjectRelationLayer} ProjectRelationLayer
67
+ * @typedef {import("./types.js").ProjectVocab} ProjectVocab
68
+ * @typedef {import("./types.js").PlaidDocument} PlaidDocument
69
+ * @typedef {import("./types.js").DocumentTextLayer} DocumentTextLayer
70
+ * @typedef {import("./types.js").DocumentTokenLayer} DocumentTokenLayer
71
+ * @typedef {import("./types.js").DocumentSpanLayer} DocumentSpanLayer
72
+ * @typedef {import("./types.js").PlaidText} PlaidText
73
+ * @typedef {import("./types.js").TextReshape} TextReshape
74
+ * @typedef {import("./types.js").Token} Token
75
+ * @typedef {import("./types.js").Span} Span
76
+ * @typedef {import("./types.js").Relation} Relation
77
+ * @typedef {import("./types.js").VocabItem} VocabItem
78
+ * @typedef {import("./types.js").VocabLink} VocabLink
79
+ * @typedef {import("./types.js").VocabLayer} VocabLayer
80
+ * @typedef {import("./types.js").TextLayer} TextLayer
81
+ * @typedef {import("./types.js").TokenLayer} TokenLayer
82
+ * @typedef {import("./types.js").SpanLayer} SpanLayer
83
+ * @typedef {import("./types.js").RelationLayer} RelationLayer
84
+ * @typedef {import("./types.js").MetadataRecords} MetadataRecords
85
+ * @typedef {import("./types.js").LayerRecords} LayerRecords
86
+ * @typedef {import("./types.js").User} User
87
+ * @typedef {import("./types.js").ProjectListEntry} ProjectListEntry
88
+ * @typedef {import("./types.js").DocumentListEntry} DocumentListEntry
89
+ * @typedef {import("./types.js").ApiToken} ApiToken
90
+ * @typedef {import("./types.js").AuditUser} AuditUser
91
+ * @typedef {import("./types.js").AuditTarget} AuditTarget
92
+ * @typedef {import("./types.js").AuditOp} AuditOp
93
+ * @typedef {import("./types.js").AuditEntry} AuditEntry
94
+ * @typedef {import("./types.js").LockGrant} LockGrant
95
+ * @typedef {import("./types.js").DocumentRestoreSummary} DocumentRestoreSummary
96
+ * @typedef {import("./types.js").RestoreCounts} RestoreCounts
97
+ * @typedef {import("./types.js").OperationGroup} OperationGroup
98
+ * @typedef {import("./types.js").QueryResult} QueryResult
99
+ */
100
+
101
+ /**
102
+ * @template [T=any]
103
+ * @typedef {import("./types.js").Page<T>} Page
104
+ */
105
+
106
+ /** @param {import("./client.js").PlaidClient} client */
107
+ export function usersApi(client) {
108
+ return {
109
+ /**
110
+ * Get a user by ID
111
+ * @param {string} id - The resource ID
112
+ * @returns {Promise<User>}
113
+ */
114
+ get: (id) =>
115
+ client._request("GET", `/api/v1/users/${id}`),
116
+ /**
117
+ * Fetch a user's profile picture as raw bytes. Most callers want
118
+ * avatarUrl() instead. This one is for non-browser consumers.
119
+ * @param {string} id - The user ID
120
+ * @returns {Promise<ArrayBuffer>}
121
+ */
122
+ getAvatar: (id) =>
123
+ client._request("GET", `/api/v1/users/${id}/avatar`, {
124
+ binaryResponse: true,
125
+ }),
126
+ /**
127
+ * List (or search) users. Transparently follows pagination cursors and
128
+ * returns the full flat array. Admin-or-maintainer only.
129
+ * @param {object} [opts]
130
+ * @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
131
+ * @returns {Promise<User[]>}
132
+ */
133
+ list: ({ q } = {}) =>
134
+ listAll(client, "/api/v1/users", { query: { q } }),
135
+ /**
136
+ * Fetch a single page of users (optionally filtered by `q`).
137
+ * @param {object} [opts]
138
+ * @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
139
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
140
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
141
+ * @returns {Promise<Page<User>>}
142
+ */
143
+ listPage: ({ q, limit, cursor } = {}) =>
144
+ listPage(client, "/api/v1/users", {
145
+ limit,
146
+ cursor,
147
+ query: { q },
148
+ }),
149
+ /**
150
+ * Async-iterate users page by page; yields each page's entries array.
151
+ * @param {object} [opts]
152
+ * @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
153
+ * @param {number} [opts.pageSize] - Per-request page size
154
+ * @returns {AsyncGenerator<User[]>}
155
+ */
156
+ iterPages: ({ q, pageSize } = {}) =>
157
+ iterPages(client, "/api/v1/users", {
158
+ pageSize,
159
+ query: { q },
160
+ }),
161
+ /**
162
+ * Create a new user. Admin only, and not with a named API token (403):
163
+ * it needs a sign-in token.
164
+ * @param {string} email - The account's email address. It becomes the
165
+ * user's id and is what they log in with; it can never be changed.
166
+ * @param {string} password - The password
167
+ * @param {boolean} isAdmin - Whether the user is an admin
168
+ * @param {string} [displayName] - How the user is shown in the UI.
169
+ * Defaults to the local part of the email.
170
+ * @param {string} [auditMessage] - Audit message for this write
171
+ * @returns {Promise<Created>}
172
+ */
173
+ create: (email, password, isAdmin, displayName, auditMessage) =>
174
+ client._request("POST", "/api/v1/users", {
175
+ auditMessage,
176
+ body: bodyOf({
177
+ email: normalizeUserId(email),
178
+ password,
179
+ "is-admin": isAdmin,
180
+ // Left out when not given (null too), so core gives the user the
181
+ // local part of the email. Core refuses a null display name.
182
+ "display-name": displayName ?? undefined,
183
+ }),
184
+ }),
185
+ /**
186
+ * Modify a user. Admins may change the display name, password, and admin
187
+ * status of any user. All other users may only modify their own display
188
+ * name or password. A password change cannot be made with a named API
189
+ * token (403): it needs a sign-in token.
190
+ *
191
+ * A user's id is their email address and is fixed for the life of the
192
+ * account — it is what they log in with, so nothing can change it.
193
+ * @param {string} id - The resource ID (the user's email address)
194
+ * @param {string} [password] - New password
195
+ * @param {string} [displayName] - New display name
196
+ * @param {boolean} [isAdmin] - New admin status
197
+ * @param {string} [auditMessage] - Audit message for this write
198
+ * @returns {Promise<User>}
199
+ */
200
+ update: (id, password, displayName, isAdmin, auditMessage) =>
201
+ client._request("PATCH", `/api/v1/users/${id}`, {
202
+ auditMessage,
203
+ body: bodyOf({
204
+ password,
205
+ "display-name": displayName,
206
+ "is-admin": isAdmin,
207
+ }),
208
+ }),
209
+ /**
210
+ * Deactivate a user. Users are never hard-deleted: deactivation
211
+ * rejects their logins and tokens, strips their project memberships
212
+ * and vocab maintainerships, and revokes their API tokens. The user
213
+ * stays visible in listings with a deactivated-at timestamp.
214
+ * Reversible via activate(), which restores login only.
215
+ * @param {string} id - The resource ID
216
+ * @param {string} [auditMessage] - Audit message for this write
217
+ * @returns {Promise<void>}
218
+ */
219
+ delete: (id, auditMessage) =>
220
+ client._request("DELETE", `/api/v1/users/${id}`, { auditMessage }),
221
+ /**
222
+ * Reactivate a deactivated user, restoring their ability to log in.
223
+ * Project memberships, vocab maintainerships, and API tokens removed
224
+ * at deactivation are NOT restored — re-grant them deliberately.
225
+ * @param {string} id - The resource ID
226
+ * @param {string} [auditMessage] - Audit message for this write
227
+ * @returns {Promise<User>}
228
+ */
229
+ activate: (id, auditMessage) =>
230
+ client._request("POST", `/api/v1/users/${id}/activate`, { auditMessage }),
231
+ /**
232
+ * A token that shows any user's profile picture without an
233
+ * Authorization header, for an `<img>`, which cannot send one. Resolves
234
+ * to `{ token, expiresAt }` (`expiresAt` an ISO-8601 instant, a day on by
235
+ * default). The server refuses it on every route but the picture, and
236
+ * it stops working when the user signs out or changes password.
237
+ *
238
+ * One token serves the whole client: it is kept and handed out again
239
+ * until under ten minutes of it remain (half its life, for one minted
240
+ * near the end of a login), calls made while one is being minted share
241
+ * that request, and it is dropped when the client's token changes.
242
+ * avatarUrl() uses it.
243
+ * @param {{renew?: boolean}} [options] - `renew`: mint a new one
244
+ * whatever is kept, unless one was minted in the last few seconds
245
+ * (or is being minted), which is shared
246
+ * @returns {Promise<{token: string, expiresAt: string}>}
247
+ */
248
+ avatarToken: ({ renew = false } = {}) => {
249
+ // A batch is a view of its client and shares the client's token.
250
+ const owner = client.client ?? client;
251
+ const cached = owner._avatarToken;
252
+ if (cached && cached.forToken === owner.token) {
253
+ if (cached.pending) return cached.pending;
254
+ const now = owner.serverNow().getTime();
255
+ // A renewal shares one minted a moment ago: every picture on a page
256
+ // fails together when a token dies, and each asks.
257
+ if (renew && now - cached.mintedAt < AVATAR_TOKEN_FRESH_MS) {
258
+ return Promise.resolve(cached.link);
259
+ }
260
+ const expires = Date.parse(cached.link.expiresAt);
261
+ // Renewed ten minutes before it runs out, or at half its life when
262
+ // it was minted for less than twenty: the server caps it at the
263
+ // login's own expiry, and near the end of a login every token it
264
+ // mints runs out at that same moment, so a fixed ten minutes asked
265
+ // for a new one on every call.
266
+ const renewAt = Math.min(AVATAR_TOKEN_RENEW_MS, (expires - cached.mintedAt) / 2);
267
+ if (!renew && expires - now > renewAt) return Promise.resolve(cached.link);
268
+ }
269
+ const entry = {
270
+ forToken: owner.token,
271
+ pending: null,
272
+ link: null,
273
+ mintedAt: owner.serverNow().getTime(),
274
+ };
275
+ entry.pending = owner
276
+ ._request("POST", "/api/v1/avatar-link", { outOfBand: true })
277
+ .then(
278
+ (link) => {
279
+ entry.pending = null;
280
+ entry.link = link;
281
+ return link;
282
+ },
283
+ (e) => {
284
+ if (owner._avatarToken === entry) owner._avatarToken = null;
285
+ throw e;
286
+ },
287
+ );
288
+ owner._avatarToken = entry;
289
+ return entry.pending;
290
+ },
291
+ /**
292
+ * Build a URL for a user's profile picture, for an `<img>` src. The
293
+ * URL carries the client's avatar token (see avatarToken()), never the
294
+ * login token.
295
+ *
296
+ * Pass the user record's `avatarHash` as the second argument whenever you
297
+ * have it: the URL then addresses that exact picture, so the browser can
298
+ * cache it indefinitely and still pick up a replacement the moment the
299
+ * user changes it. Resolves to null when `avatarHash` is null (the user
300
+ * has no picture), so callers can fall back to initials without a
301
+ * wasted request.
302
+ * @param {string} id - The user ID
303
+ * @param {string|null} [avatarHash] - The user record's `avatarHash`
304
+ * @param {{renew?: boolean}} [options] - `renew`: mint a new avatar
305
+ * token first, for a picture its URL failed to load (the token
306
+ * stops working when the login it was minted under ends)
307
+ * @returns {Promise<string|null>}
308
+ */
309
+ avatarUrl: async (id, avatarHash, { renew = false } = {}) => {
310
+ if (avatarHash === null) return null;
311
+ // Through `self`, as in server.limits.
312
+ const self = client;
313
+ const { token } = await self.users.avatarToken({ renew });
314
+ const params = new URLSearchParams({ "avatar-token": token });
315
+ if (avatarHash) params.set("v", avatarHash);
316
+ return `${client.baseUrl}/api/v1/users/${id}/avatar?${params}`;
317
+ },
318
+ /**
319
+ * Upload a profile picture. Your own, or anyone's if you are an admin.
320
+ * The server center-crops to a square, scales to the configured edge
321
+ * length, and re-encodes, so no client-side resizing is needed. Accepts
322
+ * PNG, JPEG, WebP, and GIF. Resolves to the updated user record.
323
+ * @param {string} id - The user ID
324
+ * @param {File} file - The image to upload
325
+ * @param {string} [auditMessage] - Audit message for this write
326
+ * @returns {Promise<User>}
327
+ */
328
+ setAvatar: (id, file, auditMessage) => {
329
+ const fd = new FormData();
330
+ fd.append("file", file);
331
+ return client._request("PUT", `/api/v1/users/${id}/avatar`, {
332
+ auditMessage,
333
+ body: fd,
334
+ formData: true,
335
+ noBatch: true,
336
+ });
337
+ },
338
+ /**
339
+ * Remove a profile picture. Your own, or anyone's if you are an admin.
340
+ * @param {string} id - The user ID
341
+ * @param {string} [auditMessage] - Audit message for this write
342
+ * @returns {Promise<void>}
343
+ */
344
+ // No flag: the upload beside this one is multipart and cannot be
345
+ // batched, but a DELETE carries no blob, so the batch transport takes
346
+ // it. It is an ordinary audited write and queues like any other.
347
+ deleteAvatar: (id, auditMessage) =>
348
+ client._request("DELETE", `/api/v1/users/${id}/avatar`, {
349
+ auditMessage,
350
+ }),
351
+ ...auditMethods(client, "users"),
352
+ };
353
+ }
354
+
355
+ /** @param {import("./client.js").PlaidClient} client */
356
+ export function apiTokensApi(client) {
357
+ return {
358
+ /**
359
+ * List a user's named API tokens. Never includes the signed token
360
+ * string itself — that is only returned once, by create().
361
+ * Transparently follows pagination cursors and returns the full flat array.
362
+ * @param {string} userId - The user ID who owns the tokens
363
+ * @returns {Promise<ApiToken[]>}
364
+ */
365
+ list: (userId) => listAll(client, `/api/v1/users/${userId}/tokens`),
366
+ /**
367
+ * Fetch a single page of a user's named API tokens.
368
+ * @param {string} userId - The user ID who owns the tokens
369
+ * @param {object} [opts]
370
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
371
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
372
+ * @returns {Promise<Page<ApiToken>>}
373
+ */
374
+ listPage: (userId, { limit, cursor } = {}) =>
375
+ listPage(client, `/api/v1/users/${userId}/tokens`, { limit, cursor }),
376
+ /**
377
+ * Async-iterate a user's named API tokens page by page; yields each
378
+ * page's entries array.
379
+ * @param {string} userId - The user ID who owns the tokens
380
+ * @param {object} [opts]
381
+ * @param {number} [opts.pageSize] - Per-request page size
382
+ * @returns {AsyncGenerator<ApiToken[]>}
383
+ */
384
+ iterPages: (userId, { pageSize } = {}) =>
385
+ iterPages(client, `/api/v1/users/${userId}/tokens`, { pageSize }),
386
+ /**
387
+ * Mint a named API token for a user. The returned `token` is the signed
388
+ * credential and is shown ONLY here — store it immediately. API tokens
389
+ * do not expire and survive password changes / logout; revoke to kill.
390
+ * A client signed in with a named API token cannot do this (403): it
391
+ * needs a sign-in token.
392
+ * @param {string} userId - The user ID who will own the token
393
+ * @param {string} name - A human label, e.g. "Stanza parser"
394
+ * @param {string} [auditMessage] - Audit message for this write
395
+ * @returns {Promise<{id: string, name: string, token: string}>}
396
+ */
397
+ create: (userId, name, auditMessage) =>
398
+ client._request("POST", `/api/v1/users/${userId}/tokens`, {
399
+ auditMessage,
400
+ body: bodyOf({ name }),
401
+ // The answer is the secret, which the server never keeps, so it
402
+ // takes no Idempotency-Key and a batch cannot carry it.
403
+ noBatch: true,
404
+ }),
405
+ /**
406
+ * Revoke a named API token (soft-revoke; idempotent).
407
+ * A client signed in with a named API token cannot do this (403): it
408
+ * needs a sign-in token.
409
+ * @param {string} userId - The user ID who owns the token
410
+ * @param {string} tokenId - The token ID to revoke
411
+ * @param {string} [auditMessage] - Audit message for this write
412
+ * @returns {Promise<void>}
413
+ */
414
+ revoke: (userId, tokenId, auditMessage) =>
415
+ client._request("DELETE", `/api/v1/users/${userId}/tokens/${tokenId}`, {
416
+ auditMessage,
417
+ }),
418
+ };
419
+ }
420
+
421
+ /** @param {import("./client.js").PlaidClient} client */
422
+ export function invitesApi(client) {
423
+ return {
424
+ /**
425
+ * List invites you minted, oldest first. With `projectId`, lists that
426
+ * project's invites instead (including co-maintainers'), which requires
427
+ * maintainer or admin on it. Never includes invite codes — the code is
428
+ * returned once, by create(), and is not recoverable afterward.
429
+ * Transparently follows pagination cursors and returns the full flat array.
430
+ * @param {object} [opts]
431
+ * @param {string} [opts.projectId] - List this project's invites instead of your own
432
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
433
+ * @returns {Promise<Invite[]>}
434
+ */
435
+ list: ({ projectId, all } = {}) =>
436
+ listAll(client, "/api/v1/invites", {
437
+ query: { "project-id": projectId, all },
438
+ }),
439
+ /**
440
+ * Fetch a single page of invites.
441
+ * @param {object} [opts]
442
+ * @param {string} [opts.projectId] - List this project's invites instead of your own
443
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
444
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
445
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
446
+ * @returns {Promise<Page<Invite>>}
447
+ */
448
+ listPage: ({ projectId, all, limit, cursor } = {}) =>
449
+ listPage(client, "/api/v1/invites", {
450
+ limit,
451
+ cursor,
452
+ query: { "project-id": projectId, all },
453
+ }),
454
+ /**
455
+ * Async-iterate invites page by page; yields each page's entries array.
456
+ * @param {object} [opts]
457
+ * @param {string} [opts.projectId] - List this project's invites instead of your own
458
+ * @param {boolean} [opts.all] - List every invite on the server (admin only)
459
+ * @param {number} [opts.pageSize] - Per-request page size
460
+ * @returns {AsyncGenerator<Invite[]>}
461
+ */
462
+ iterPages: ({ projectId, all, pageSize } = {}) =>
463
+ iterPages(client, "/api/v1/invites", {
464
+ pageSize,
465
+ query: { "project-id": projectId, all },
466
+ }),
467
+ /**
468
+ * Mint an invite. The returned `code` is shown ONLY here — it is never
469
+ * stored and cannot be recovered, so build and hand off the link now.
470
+ * Use PlaidClient.inviteUrl() to turn it into one.
471
+ *
472
+ * Admins may mint anything. A project maintainer may mint role grants on
473
+ * projects they maintain, and nothing else: no admin grant, no grantless
474
+ * invite, no password resets — so for a non-admin, projectId/projectRole
475
+ * are required in practice (403 without them).
476
+ *
477
+ * Needs a signed-in session: a client signed in with a named API token
478
+ * cannot mint any invite (403).
479
+ *
480
+ * EVERY option is optional; `create()` with no arguments mints a
481
+ * single-use signup link granting nothing but an account. Two pairing
482
+ * rules the server enforces with a 400: projectId and projectRole must be
483
+ * given TOGETHER, and targetUserId may not be combined with projectId,
484
+ * grantAdmin, or a maxUses above 1.
485
+ * @param {object} [opts]
486
+ * @param {string} [opts.projectId] - Project the redeemer joins (requires projectRole)
487
+ * @param {string} [opts.projectRole] - "reader" | "writer" | "maintainer" (requires projectId)
488
+ * @param {boolean} [opts.grantAdmin] - Make the new account a global admin (admin only)
489
+ * @param {string} [opts.targetUserId] - Password reset for that user instead of a signup; admin only, single-use, grants nothing
490
+ * @param {number} [opts.maxUses] - How many accounts this link may create (default 1)
491
+ * @param {number} [opts.ttlDays] - Days until it expires (default 14, max 365)
492
+ * @param {string} [opts.note] - Human label shown in your invite list
493
+ * @param {string} [auditMessage] - Audit message for this write
494
+ * @returns {Promise<Invite & {code: string}>}
495
+ */
496
+ create: (
497
+ {
498
+ projectId,
499
+ projectRole,
500
+ grantAdmin,
501
+ targetUserId,
502
+ maxUses,
503
+ ttlDays,
504
+ note,
505
+ } = {},
506
+ auditMessage,
507
+ ) =>
508
+ client._request("POST", "/api/v1/invites", {
509
+ auditMessage,
510
+ // The answer is the code, which the server never keeps, so it
511
+ // takes no Idempotency-Key and a batch cannot carry it.
512
+ noBatch: true,
513
+ body: bodyOf({
514
+ "project-id": projectId,
515
+ "project-role": projectRole,
516
+ "grant-admin": grantAdmin,
517
+ "target-user-id": targetUserId,
518
+ "max-uses": maxUses,
519
+ "ttl-days": ttlDays,
520
+ note,
521
+ }),
522
+ }),
523
+ /**
524
+ * Revoke an invite, killing the link immediately. Idempotent. Allowed for
525
+ * the creator, an admin, or a maintainer of the invite's project.
526
+ * @param {string} id - The invite ID
527
+ * @param {string} [auditMessage] - Audit message for this write
528
+ * @returns {Promise<void>}
529
+ */
530
+ revoke: (id, auditMessage) =>
531
+ client._request("DELETE", `/api/v1/invites/${id}`, { auditMessage }),
532
+ };
533
+ }
534
+
535
+ // The signed-in user's sign-ins. Signing in is `PlaidClient.login`, which
536
+ // makes the client.
537
+ /** @param {import("./client.js").PlaidClient} client */
538
+ export function authApi(client) {
539
+ return {
540
+ /**
541
+ * End every sign-in of this user on every device: each browser tab,
542
+ * script and service holding one of the user's sign-in tokens is
543
+ * refused from now on, this client included. Named API tokens are not
544
+ * affected (revoke one with `apiTokens.revoke`). To sign out of one tab
545
+ * only, discard its token instead. A signal rather than project data:
546
+ * made on a batch it still goes out at once, and it never joins an
547
+ * operation.
548
+ * @returns {Promise<void>}
549
+ */
550
+ logoutEverywhere: async () => {
551
+ await client._request("POST", "/api/v1/logout", { outOfBand: true });
552
+ },
553
+ };
554
+ }
555
+
556
+ /** @param {import("./client.js").PlaidClient} client */
557
+ export function userDataApi(client) {
558
+ return {
559
+ /**
560
+ * Read one private data entry ({key, updatedAt, version, value}); 404 if absent.
561
+ * @param {string} userId
562
+ * @param {string} key
563
+ * @returns {Promise<UserDataEntry & {value: any}>}
564
+ */
565
+ get: (userId, key) =>
566
+ client._request(
567
+ "GET",
568
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
569
+ ),
570
+ /**
571
+ * List a user's private data entries ({key, updatedAt}, plus value when
572
+ * includeValues), ordered by key. Owner or admin only. Transparently
573
+ * follows pagination cursors and returns the full flat array.
574
+ *
575
+ * Narrow with `prefix` (the literal head of a key) and/or `pattern`, a
576
+ * GLOB over the whole key (`*` any run, `?` one character) — the way to
577
+ * ask for a key convention identified by a segment in the middle, e.g.
578
+ * `igt:assistant:*:meta:*` for every conversation's sidebar entry across
579
+ * every project without dragging down the transcripts beside them.
580
+ *
581
+ * `pageSize` is exposed here, and defaults lower than elsewhere, because
582
+ * one value runs to megabytes (`userDataValueBytes` in
583
+ * `server.limits()`): a page of them with `includeValues` is the
584
+ * largest response this API can be asked for. Raise it when the listing
585
+ * is keys, or the values are known to be small.
586
+ * @param {string} userId
587
+ * @param {object} [opts]
588
+ * @param {string} [opts.prefix] - Only keys starting with this prefix
589
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
590
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
591
+ * @param {number} [opts.pageSize=100] - Entries per request (1..1000)
592
+ * @returns {Promise<UserDataEntry[]>}
593
+ */
594
+ list: (userId, { prefix, pattern, includeValues, pageSize = 100 } = {}) =>
595
+ listAll(client, `/api/v1/users/${userId}/data`, {
596
+ pageSize,
597
+ query: { prefix, pattern, "include-values": includeValues },
598
+ }),
599
+ /**
600
+ * One page of a user's private data entries, ordered by key.
601
+ * @param {string} userId
602
+ * @param {object} [opts]
603
+ * @param {string} [opts.prefix] - Only keys starting with this prefix
604
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
605
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
606
+ * @param {number} [opts.limit] - Page size (1..1000; server default 100)
607
+ * @param {string} [opts.cursor] - Opaque cursor from a previous page
608
+ * @returns {Promise<Page<UserDataEntry>>}
609
+ */
610
+ listPage: (
611
+ userId,
612
+ { prefix, pattern, includeValues, limit, cursor } = {},
613
+ ) =>
614
+ listPage(client, `/api/v1/users/${userId}/data`, {
615
+ limit,
616
+ cursor,
617
+ query: { prefix, pattern, "include-values": includeValues },
618
+ }),
619
+ /**
620
+ * Async generator over a user's private data entries, one page at a time.
621
+ * @param {string} userId
622
+ * @param {object} [opts]
623
+ * @param {string} [opts.prefix] - Only keys starting with this prefix
624
+ * @param {string} [opts.pattern] - Only keys matching this GLOB
625
+ * @param {boolean} [opts.includeValues] - Also return each entry's value
626
+ * @param {number} [opts.pageSize=100] - Entries per request (1..1000)
627
+ * @returns {AsyncGenerator<UserDataEntry[]>}
628
+ */
629
+ iterPages: (
630
+ userId,
631
+ { prefix, pattern, includeValues, pageSize = 100 } = {},
632
+ ) =>
633
+ iterPages(client, `/api/v1/users/${userId}/data`, {
634
+ pageSize,
635
+ query: { prefix, pattern, "include-values": includeValues },
636
+ }),
637
+ /**
638
+ * Delete one private data entry; 404 if absent.
639
+ * @param {string} userId
640
+ * @param {string} key
641
+ * @returns {Promise<void>}
642
+ */
643
+ delete: (userId, key) =>
644
+ client._request(
645
+ "DELETE",
646
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
647
+ {
648
+ noBatch: true,
649
+ },
650
+ ),
651
+ /**
652
+ * Create or replace one private data entry. `value` is any JSON, up to
653
+ * the server's `userDataValueBytes` (`server.limits()`, 5 MB by
654
+ * default), and is refused with 413 over it, after which
655
+ * `server.limits()` answers the figure the server has now. Not audited,
656
+ * not batchable.
657
+ *
658
+ * The server stores it verbatim, but this client recases object keys on
659
+ * the way out and back like any other body (`myKey` <-> `my-key`), so a
660
+ * value whose keys are camelCase round-trips unchanged while one keyed
661
+ * by arbitrary strings does not. Put such a map under a `metadata` key,
662
+ * which both clients pass through untouched.
663
+ *
664
+ * Every write raises the entry's `version` by one, and reads answer it.
665
+ * With `version`, the write lands only when the entry is still at that
666
+ * version (0: only when there is no entry), and is otherwise refused with
667
+ * 409, `error: "version-mismatch"`, and the stored `version` and
668
+ * `updatedAt` on the error's `responseData`: read it again and make the
669
+ * change on what is there.
670
+ * @param {string} userId
671
+ * @param {string} key
672
+ * @param {*} value
673
+ * @param {object} [opts]
674
+ * @param {number} [opts.version] - The version this write was made from
675
+ * @returns {Promise<{key: string, updatedAt: string, version: number}>}
676
+ */
677
+ put: async (userId, key, value, { version } = {}) => {
678
+ try {
679
+ return await client._request(
680
+ "PUT",
681
+ `/api/v1/users/${userId}/data/${encodeURIComponent(key)}`,
682
+ {
683
+ body: value,
684
+ noBatch: true,
685
+ queryParams: { version },
686
+ },
687
+ );
688
+ } catch (err) {
689
+ if (err?.status === 413) {
690
+ // The cap may have changed since it was read: the caller deciding
691
+ // what to do next reads the server's figure now.
692
+ await client.server.refresh().catch(() => {});
693
+ }
694
+ throw err;
695
+ }
696
+ },
697
+ };
698
+ }