@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.
- package/package.json +3 -2
- package/src/apiAnnotations.d.ts +508 -0
- package/src/apiAnnotations.js +543 -0
- package/src/apiDocuments.d.ts +569 -0
- package/src/apiDocuments.js +551 -0
- package/src/apiLayers.d.ts +369 -0
- package/src/apiLayers.js +222 -0
- package/src/apiMessages.d.ts +293 -0
- package/src/apiMessages.js +265 -0
- package/src/apiPeople.d.ts +626 -0
- package/src/apiPeople.js +698 -0
- package/src/apiProject.d.ts +761 -0
- package/src/apiProject.js +774 -0
- package/src/apiServer.d.ts +455 -0
- package/src/apiServer.js +422 -0
- package/src/apiVocab.d.ts +456 -0
- package/src/apiVocab.js +425 -0
- package/src/batch.d.ts +54 -0
- package/src/batch.js +323 -0
- package/src/client.d.ts +1321 -0
- package/src/client.js +743 -0
- package/src/constraints.d.ts +8 -0
- package/src/constraints.js +14 -0
- package/src/documentLock.d.ts +18 -4
- package/src/documentLock.js +43 -9
- package/src/entryForms.d.ts +63 -0
- package/src/entryForms.js +152 -0
- package/src/events.js +2 -1
- package/src/families.d.ts +349 -0
- package/src/families.js +396 -0
- package/src/http.d.ts +39 -18
- package/src/http.js +127 -87
- package/src/index.d.ts +90 -4557
- package/src/index.js +113 -5172
- package/src/operations.d.ts +36 -0
- package/src/operations.js +164 -0
- package/src/provenance.d.ts +47 -4
- package/src/provenance.js +168 -12
- package/src/roles.d.ts +39 -0
- package/src/roles.js +44 -0
- package/src/serviceSchema.d.ts +19 -13
- package/src/serviceSchema.js +30 -15
- package/src/services.d.ts +20 -3
- package/src/services.js +45 -8
- package/src/sse.js +10 -1
- package/src/testing.d.ts +128 -0
- package/src/testing.js +379 -0
- package/src/types.d.ts +867 -0
- package/src/types.js +663 -0
package/src/apiPeople.js
ADDED
|
@@ -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
|
+
}
|