pryv 3.0.3 → 3.1.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pryv",
3
- "version": "3.0.3",
3
+ "version": "3.1.0",
4
4
  "description": "Pryv JavaScript library",
5
5
  "keywords": [
6
6
  "Pryv",
package/src/Connection.js CHANGED
@@ -6,6 +6,7 @@ const utils = require('./utils.js');
6
6
  const jsonParser = require('./lib/json-parser');
7
7
  const libGetEventStreamed = require('./lib/getEventStreamed');
8
8
  const PryvError = require('./lib/PryvError');
9
+ const StaleAccessIdError = require('./lib/StaleAccessIdError');
9
10
  const buildSearchParams = require('./lib/buildSearchParams');
10
11
 
11
12
  /**
@@ -71,11 +72,22 @@ class Connection {
71
72
 
72
73
  /**
73
74
  * Get access info for this connection.
74
- * It's async as it is fetched from the API.
75
+ *
76
+ * Memoized per-Connection: the first call fetches from the server and
77
+ * caches the result; subsequent calls return the cached copy in O(1).
78
+ * Pass `forceRefresh: true` to invalidate the cache and fetch a fresh
79
+ * copy from the server — used internally by `connection.socket` to
80
+ * react to Plan 66 `accessUpdated` server-push events. A failed
81
+ * server fetch leaves any prior cached value intact.
82
+ *
83
+ * @param {boolean} [forceRefresh=false] - bypass + refresh the cache
75
84
  * @returns {Promise<AccessInfo>} Promise resolving to the access info
76
85
  */
77
- async accessInfo () {
78
- return this.get('access-info', null);
86
+ async accessInfo (forceRefresh = false) {
87
+ if (!forceRefresh && this._accessInfoCache != null) return this._accessInfoCache;
88
+ const fresh = await this.get('access-info', null);
89
+ this._accessInfoCache = fresh;
90
+ return fresh;
79
91
  }
80
92
 
81
93
  /**
@@ -416,6 +428,48 @@ class Connection {
416
428
  return utils.buildAPIEndpoint(this);
417
429
  }
418
430
 
431
+ /**
432
+ * Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): update an access by composite id.
433
+ * Wraps `accesses.update` and translates the 409 `stale-resource` response
434
+ * into a typed `StaleAccessIdError` so callers can `instanceof`-test and
435
+ * refetch + retry without re-parsing the inner error.
436
+ *
437
+ * Pass `id` as the wire-format reference returned by the server — bare
438
+ * cuid on a never-updated access, composite `<base>:<serial>` otherwise.
439
+ * `changes` is the body of mutable fields (name, deviceName, permissions,
440
+ * expireAfter, expires:null, clientData).
441
+ *
442
+ * @param {string} id
443
+ * @param {Object} changes
444
+ * @returns {Promise<Object>} the updated access (with new composite id)
445
+ * @throws {StaleAccessIdError} if the server reports the id is stale
446
+ */
447
+ async updateAccess (id, changes) {
448
+ try {
449
+ return await this.apiOne('accesses.update', { id, update: changes }, 'access');
450
+ } catch (e) {
451
+ if (e && e.innerObject && e.innerObject.id === 'stale-resource') {
452
+ throw new StaleAccessIdError(e.message, e.innerObject.data || {});
453
+ }
454
+ throw e;
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Plan 66: fetch an access by composite id including its full version
460
+ * history (oldest first). Server: `accesses.getOne ?includeHistory=true`.
461
+ *
462
+ * Useful for audit views. Pass the composite `<base>:<serial>` to
463
+ * inspect a specific past version (the result's `current` field then
464
+ * points at the live head's composite id).
465
+ *
466
+ * @param {string} id
467
+ * @returns {Promise<{ access: Object, current?: string, history?: Object[] }>}
468
+ */
469
+ async getAccessWithHistory (id) {
470
+ return await this.apiOne('accesses.getOne', { id, includeHistory: true });
471
+ }
472
+
419
473
  // private method that handle meta data parsing
420
474
  _handleMeta (res, requestLocalTimestamp) {
421
475
  if (!res.meta) throw new Error('Cannot find .meta in response.');
package/src/Service.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
4
  */
5
5
  const utils = require('./utils.js');
6
+ const PryvError = require('./lib/PryvError.js');
7
+ const MfaRequiredError = require('./lib/MfaRequiredError.js');
6
8
  // Connection is required at the end of this file to allow circular requires.
7
9
  const Assets = require('./ServiceAssets.js');
8
10
 
@@ -182,20 +184,388 @@ class Service {
182
184
  );
183
185
 
184
186
  if (!response.ok) {
185
- if (body?.error?.message) {
186
- throw new Error(body.error.message);
187
- }
188
- throw new Error('Login failed: ' + JSON.stringify(body));
187
+ throw PryvError.fromApiResponse(response, body);
188
+ }
189
+
190
+ if (body && body.mfaToken) {
191
+ throw new MfaRequiredError(body.mfaToken, response, body);
189
192
  }
190
193
 
191
- if (!body.token) {
192
- throw new Error('Invalid login response: ' + JSON.stringify(body));
194
+ if (!body || !body.token) {
195
+ throw new PryvError(
196
+ 'Invalid login response: ' + JSON.stringify(body)
197
+ );
193
198
  }
194
199
  return new Connection(
195
200
  Service.buildAPIEndpoint(await this.info(), username, body.token),
196
201
  this // Pre load Connection with service
197
202
  );
198
203
  }
204
+
205
+ /**
206
+ * Re-trigger an MFA challenge (e.g. resend SMS) during a pending login.
207
+ * Use after `login()` threw `MfaRequiredError` if the user needs another
208
+ * SMS code.
209
+ *
210
+ * @param {string} userId
211
+ * @param {string} mfaToken - From `MfaRequiredError.mfaToken`
212
+ * @returns {Promise<void>}
213
+ * @throws {PryvError} on 4xx/5xx (e.g. invalid/expired mfaToken)
214
+ */
215
+ async mfaChallenge (userId, mfaToken) {
216
+ if (!userId || !mfaToken) {
217
+ throw new PryvError('mfaChallenge requires userId and mfaToken');
218
+ }
219
+ const url = await this.apiEndpointFor(userId) + 'mfa/challenge';
220
+ const { response, body } = await utils.fetchPost(url, {}, {
221
+ Authorization: mfaToken
222
+ });
223
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
224
+ }
225
+
226
+ /**
227
+ * Finish an MFA-protected login by submitting the SMS code. Returns a
228
+ * fully-formed `Connection` (parallel to `Service.login`).
229
+ *
230
+ * @param {string} userId
231
+ * @param {string} mfaToken - From `MfaRequiredError.mfaToken`
232
+ * @param {string} code - The SMS verification code
233
+ * @returns {Promise<Connection>}
234
+ * @throws {PryvError} on bad code, expired mfaToken, etc.
235
+ */
236
+ async mfaVerify (userId, mfaToken, code) {
237
+ if (!userId || !mfaToken || code == null) {
238
+ throw new PryvError('mfaVerify requires userId, mfaToken, code');
239
+ }
240
+ const url = await this.apiEndpointFor(userId) + 'mfa/verify';
241
+ const { response, body } = await utils.fetchPost(url, { code }, {
242
+ Authorization: mfaToken
243
+ });
244
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
245
+ if (!body || !body.token) {
246
+ throw new PryvError(
247
+ 'mfa.verify did not return a token: ' + JSON.stringify(body)
248
+ );
249
+ }
250
+ return new Connection(
251
+ Service.buildAPIEndpoint(await this.info(), userId, body.token),
252
+ this
253
+ );
254
+ }
255
+
256
+ /**
257
+ * Check whether a username is registered on this service.
258
+ * One round-trip via `POST <register>/<userId>/server`.
259
+ *
260
+ * @param {string} userId - The username to check
261
+ * @returns {Promise<boolean>} `true` if registered, `false` on 404
262
+ * @throws {PryvError} on network errors or non-404 API errors
263
+ */
264
+ async userExists (userId) {
265
+ const serviceInfo = await this.info();
266
+ const url = serviceInfo.register + encodeURIComponent(userId) + '/server';
267
+ const { response, body } = await utils.fetchPost(url, {});
268
+ if (response.ok) return true;
269
+ if (response.status === 404) return false;
270
+ throw PryvError.fromApiResponse(response, body);
271
+ }
272
+
273
+ /**
274
+ * Resolve an email address to a username on this service.
275
+ * One round-trip via `GET <register>/<email>/uid`.
276
+ *
277
+ * @param {string} email - The email to look up
278
+ * @returns {Promise<string|null>} The username, or `null` if unknown
279
+ * @throws {PryvError} on network errors or non-404 API errors
280
+ */
281
+ async userIdForEmail (email) {
282
+ const serviceInfo = await this.info();
283
+ const url = serviceInfo.register + encodeURIComponent(email) + '/uid';
284
+ const { response, body } = await utils.fetchGet(url);
285
+ if (response.ok) return (body && (body.uid || body.username)) || null;
286
+ if (response.status === 404) return null;
287
+ throw PryvError.fromApiResponse(response, body);
288
+ }
289
+
290
+ /**
291
+ * Fetch the raw hostings tree advertised by `<register>/hostings`.
292
+ *
293
+ * Returns the nested API shape `{ regions: { <region>: { zones:
294
+ * { <zone>: { hostings: { <key>: { name, description, availableCore,
295
+ * available } } } } } } }`. For a flat list ready to render in a UI,
296
+ * use `flatHostings()`.
297
+ *
298
+ * @returns {Promise<Object>} the raw `/reg/hostings` body
299
+ * @throws {PryvError} on non-2xx
300
+ */
301
+ async availableHostings () {
302
+ const serviceInfo = await this.info();
303
+ const { response, body } = await utils.fetchGet(
304
+ serviceInfo.register + 'hostings'
305
+ );
306
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
307
+ return body;
308
+ }
309
+
310
+ /**
311
+ * Flatten `availableHostings()` into a list of `{ key, name, description,
312
+ * region, zone, availableCore, available }` items.
313
+ *
314
+ * @returns {Promise<Array<Object>>}
315
+ * @throws {PryvError} on non-2xx
316
+ */
317
+ async flatHostings () {
318
+ const tree = await this.availableHostings();
319
+ const out = [];
320
+ const regions = (tree && tree.regions) || {};
321
+ for (const [regionKey, region] of Object.entries(regions)) {
322
+ const zones = (region && region.zones) || {};
323
+ for (const [zoneKey, zone] of Object.entries(zones)) {
324
+ const hostings = (zone && zone.hostings) || {};
325
+ for (const [key, h] of Object.entries(hostings)) {
326
+ if (!h) continue;
327
+ out.push({
328
+ key,
329
+ name: h.name,
330
+ description: h.description,
331
+ region: regionKey,
332
+ zone: zoneKey,
333
+ availableCore: h.availableCore,
334
+ available: h.available === true
335
+ });
336
+ }
337
+ }
338
+ }
339
+ return out;
340
+ }
341
+
342
+ /**
343
+ * Register a new user on this service.
344
+ *
345
+ * Hides the v1/v2 register endpoint difference. v2 platforms (service
346
+ * version >= 2.0 or >= 1.6) accept camelCase fields at `<register>users`;
347
+ * older v1 service-register expects mixed-case fields at `<register>user`.
348
+ *
349
+ * Pass `hosting: 'auto'` to use the first hosting flagged `available: true`
350
+ * in `flatHostings()` — useful for tests and single-hosting platforms.
351
+ *
352
+ * @param {Object} opts
353
+ * @param {string} opts.username
354
+ * @param {string} opts.password
355
+ * @param {string} opts.email
356
+ * @param {string} opts.hosting - Hosting key (use `service.flatHostings()` to discover) or `'auto'`
357
+ * @param {string} opts.appId
358
+ * @param {string} [opts.language='en']
359
+ * @param {string} [opts.invitationToken='enjoy']
360
+ * @param {string} [opts.referer]
361
+ * @returns {Promise<{ username: string, apiEndpoint: string }>}
362
+ * @throws {PryvError} on duplicate username, weak password, etc.
363
+ */
364
+ async createUser (opts) {
365
+ if (!opts || !opts.username || !opts.password || !opts.email ||
366
+ !opts.hosting || !opts.appId) {
367
+ throw new PryvError(
368
+ 'createUser requires username, password, email, hosting, appId'
369
+ );
370
+ }
371
+ const serviceInfo = await this.info();
372
+ const isModern = supportsCamelCaseRegister(serviceInfo.version);
373
+ const language = opts.language || 'en';
374
+ const invitationToken = opts.invitationToken || 'enjoy';
375
+
376
+ let hosting = opts.hosting;
377
+ if (hosting === 'auto') {
378
+ const flat = await this.flatHostings();
379
+ const first = flat.find(h => h.available);
380
+ if (!first) {
381
+ throw new PryvError(
382
+ 'createUser({ hosting: "auto" }): no hosting flagged available'
383
+ );
384
+ }
385
+ hosting = first.key;
386
+ }
387
+
388
+ let url, payload;
389
+ if (isModern) {
390
+ url = serviceInfo.register + 'users';
391
+ payload = {
392
+ appId: opts.appId,
393
+ username: opts.username,
394
+ password: opts.password,
395
+ email: opts.email,
396
+ hosting,
397
+ language,
398
+ invitationToken
399
+ };
400
+ if (opts.referer != null) payload.referer = opts.referer;
401
+ } else {
402
+ url = serviceInfo.register + 'user';
403
+ payload = {
404
+ appid: opts.appId,
405
+ username: opts.username,
406
+ password: opts.password,
407
+ email: opts.email,
408
+ hosting,
409
+ languageCode: language,
410
+ invitationtoken: invitationToken
411
+ };
412
+ if (opts.referer != null) payload.referer = opts.referer;
413
+ }
414
+ const { response, body } = await utils.fetchPost(url, payload);
415
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
416
+ const apiEndpoint = await this.apiEndpointFor(opts.username);
417
+ return { username: opts.username, apiEndpoint };
418
+ }
419
+
420
+ /**
421
+ * Trigger a password-reset email for the given user.
422
+ * Pre-auth — no token required.
423
+ *
424
+ * @param {string} userId
425
+ * @param {string} appId
426
+ * @returns {Promise<void>}
427
+ * @throws {PryvError} on 4xx/5xx
428
+ */
429
+ async requestPasswordReset (userId, appId) {
430
+ if (!userId || !appId) {
431
+ throw new PryvError('requestPasswordReset requires userId and appId');
432
+ }
433
+ const url = await this.apiEndpointFor(userId) +
434
+ 'account/request-password-reset';
435
+ const { response, body } = await utils.fetchPost(url, {
436
+ appId,
437
+ username: userId
438
+ });
439
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
440
+ }
441
+
442
+ /**
443
+ * Start an access-request flow. Posts to the platform's auth endpoint
444
+ * (`serviceInfo.access`) and returns the envelope the consumer needs to
445
+ * present an approve-link to the user and poll for completion.
446
+ *
447
+ * `Browser.setupAuth` already wraps this for the high-level browser flow.
448
+ * Use this method when you're a non-browser caller (CLI, native app, bot)
449
+ * or building your own UI on top.
450
+ *
451
+ * @param {Object} authRequest - The auth-request body
452
+ * @param {string} authRequest.requestingAppId
453
+ * @param {Array<{ streamId: string, level: string, defaultName: string }>} authRequest.requestedPermissions
454
+ * @param {string} [authRequest.languageCode='en']
455
+ * @param {string|boolean} [authRequest.returnUrl]
456
+ * @param {string} [authRequest.referer]
457
+ * @param {Object} [authRequest.clientData]
458
+ * @param {string} [authRequest.deviceName]
459
+ * @param {number} [authRequest.expireAfter]
460
+ * @returns {Promise<{ key: string, authUrl: string, poll: string, pollRateMs: number }>}
461
+ * @throws {PryvError} on non-2xx
462
+ */
463
+ async startAccessRequest (authRequest) {
464
+ if (!authRequest || !authRequest.requestingAppId) {
465
+ throw new PryvError(
466
+ 'startAccessRequest requires authRequest.requestingAppId'
467
+ );
468
+ }
469
+ const serviceInfo = await this.info();
470
+ const { response, body } = await utils.fetchPost(
471
+ serviceInfo.access,
472
+ authRequest
473
+ );
474
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
475
+ if (!body || !body.key || !body.poll) {
476
+ throw new PryvError(
477
+ 'Invalid access-request response: ' + JSON.stringify(body)
478
+ );
479
+ }
480
+ return {
481
+ key: body.key,
482
+ authUrl: body.authUrl || body.url,
483
+ poll: body.poll,
484
+ pollRateMs: body.poll_rate_ms != null ? body.poll_rate_ms : body.pollRateMs
485
+ };
486
+ }
487
+
488
+ /**
489
+ * Poll an in-progress access request once. Accepts either:
490
+ * - a `key` returned by `startAccessRequest` (poll URL is built from
491
+ * `serviceInfo.access + key`)
492
+ * - a full poll URL (use as-is — recommended, since the server-issued
493
+ * URL is canonical and may include a different subdomain).
494
+ *
495
+ * Returns the raw body. Inspect `body.status` to drive the flow:
496
+ * - `'NEED_SIGNIN'` → user has not interacted yet; keep polling.
497
+ * - `'ACCEPTED'` → `body.apiEndpoint` + `body.username` + `body.token` are set.
498
+ * - `'REFUSED'` → user declined.
499
+ *
500
+ * @param {string} keyOrPollUrl
501
+ * @returns {Promise<Object>}
502
+ * @throws {PryvError} on transport errors or non-2xx-and-not-403-REFUSED
503
+ */
504
+ async pollAccessRequest (keyOrPollUrl) {
505
+ if (!keyOrPollUrl) {
506
+ throw new PryvError('pollAccessRequest requires a key or poll URL');
507
+ }
508
+ let pollUrl = keyOrPollUrl;
509
+ if (!/^https?:\/\//.test(keyOrPollUrl)) {
510
+ const serviceInfo = await this.info();
511
+ pollUrl = serviceInfo.access + keyOrPollUrl;
512
+ }
513
+ const { response, body } = await utils.fetchGet(pollUrl);
514
+ // 403 with status=REFUSED is the canonical "user declined" terminal
515
+ // state — treat as a successful poll, not an error (matches the
516
+ // behaviour of `Auth/AuthController.js`).
517
+ if (response.status === 403 && body && body.status === 'REFUSED') {
518
+ return body;
519
+ }
520
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
521
+ return body;
522
+ }
523
+
524
+ /**
525
+ * Set a new password using a reset token (from the reset email).
526
+ * Pre-auth — no login token required.
527
+ *
528
+ * @param {string} userId
529
+ * @param {string} newPassword
530
+ * @param {string} resetToken
531
+ * @param {string} appId
532
+ * @returns {Promise<void>}
533
+ * @throws {PryvError} on `unknown-or-expired-reset-token`, weak password, etc.
534
+ */
535
+ async resetPassword (userId, newPassword, resetToken, appId) {
536
+ if (!userId || !newPassword || !resetToken || !appId) {
537
+ throw new PryvError(
538
+ 'resetPassword requires userId, newPassword, resetToken, appId'
539
+ );
540
+ }
541
+ const url = await this.apiEndpointFor(userId) + 'account/reset-password';
542
+ const { response, body } = await utils.fetchPost(url, {
543
+ username: userId,
544
+ newPassword,
545
+ resetToken,
546
+ appId
547
+ });
548
+ if (!response.ok) throw PryvError.fromApiResponse(response, body);
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Detect whether the platform's service-info `version` supports the modern
554
+ * camelCase register endpoint (`POST /users`). v2 service-register routes
555
+ * both, but v1 platforms only accept the mixed-case `POST /user`.
556
+ *
557
+ * @param {string|undefined} version - service-info `version` field
558
+ * @returns {boolean}
559
+ */
560
+ function supportsCamelCaseRegister (version) {
561
+ if (!version || typeof version !== 'string') return true; // optimistic: assume v2+
562
+ const m = /^(\d+)\.(\d+)/.exec(version);
563
+ if (!m) return true;
564
+ const major = parseInt(m[1], 10);
565
+ const minor = parseInt(m[2], 10);
566
+ if (major >= 2) return true;
567
+ if (major === 1 && minor >= 6) return true;
568
+ return false;
199
569
  }
200
570
 
201
571
  module.exports = Service;
package/src/index.d.ts CHANGED
@@ -166,14 +166,101 @@ declare module 'pryv' {
166
166
 
167
167
  /**
168
168
  * Custom error class for Pryv library errors.
169
- * Includes an innerObject property for wrapping underlying errors.
169
+ *
170
+ * - `innerObject`: legacy field, set by the 2-arg constructor.
171
+ * - `id` / `status` / `response`: structured fields populated by
172
+ * `PryvError.fromApiResponse(response, body)` for errors that came
173
+ * from a Pryv API call. All three are `undefined` for legacy errors.
170
174
  */
171
175
  export class PryvError extends globalThis.Error {
172
176
  constructor(message: string, innerObject?: globalThis.Error | object);
173
177
  name: 'PryvError';
174
178
  innerObject?: globalThis.Error | object;
179
+ id?: string;
180
+ status?: number;
181
+ response?: { body: any; status: number };
182
+ static fromApiResponse(response: Response, body?: any): PryvError;
175
183
  }
176
184
 
185
+ /**
186
+ * Thrown by `Service.login` when the platform returned `{ mfaToken }`
187
+ * instead of `{ token }`. Carries the `mfaToken` so the consumer can
188
+ * call `Service.mfaVerify(userId, err.mfaToken, code)` after prompting
189
+ * the user for their SMS code.
190
+ */
191
+ export class MfaRequiredError extends PryvError {
192
+ constructor(mfaToken: string, response: Response, body?: any);
193
+ name: 'MfaRequiredError';
194
+ mfaToken: string;
195
+ }
196
+
197
+ /** Catalogue of Pryv API error ids (mirrors open-pryv.io ErrorIds.js). */
198
+ export const ERRORS: {
199
+ readonly API_UNAVAILABLE: 'api-unavailable';
200
+ readonly CORRUPTED_DATA: 'corrupted-data';
201
+ readonly FORBIDDEN: 'forbidden';
202
+ readonly INVALID_ACCESS_TOKEN: 'invalid-access-token';
203
+ readonly INVALID_CREDENTIALS: 'invalid-credentials';
204
+ readonly UNSUPPORTED_OPERATION: 'unsupported-operation';
205
+ readonly INVALID_EVENT_TYPE: 'invalid-event-type';
206
+ readonly INVALID_ITEM_ID: 'invalid-item-id';
207
+ readonly INVALID_METHOD: 'invalid-method';
208
+ readonly INVALID_OPERATION: 'invalid-operation';
209
+ readonly INVALID_PARAMETERS_FORMAT: 'invalid-parameters-format';
210
+ readonly INVALID_REQUEST_STRUCTURE: 'invalid-request-structure';
211
+ readonly ITEM_ALREADY_EXISTS: 'item-already-exists';
212
+ readonly MISSING_HEADER: 'missing-header';
213
+ readonly UNEXPECTED_ERROR: 'unexpected-error';
214
+ readonly UNKNOWN_REFERENCED_RESOURCE: 'unknown-referenced-resource';
215
+ readonly UNKNOWN_RESOURCE: 'unknown-resource';
216
+ readonly UNSUPPORTED_CONTENT_TYPE: 'unsupported-content-type';
217
+ readonly TOO_MANY_RESULTS: 'too-many-results';
218
+ readonly GONE: 'removed-method';
219
+ readonly UNAVAILABLE_METHOD: 'unavailable-method';
220
+ readonly INVALID_INVITATION_TOKEN: 'invitationToken-invalid';
221
+ readonly INVALID_USERNAME: 'username-invalid';
222
+ readonly USERNAME_REQUIRED: 'username-required';
223
+ readonly INVALID_EMAIL: 'email-invalid';
224
+ readonly INVALID_LANGUAGE: 'language-invalid';
225
+ readonly INVALID_APP_ID: 'appid-invalid';
226
+ readonly INVALID_PASSWORD: 'password-invalid';
227
+ readonly INVALID_REFERER: 'referer-invalid';
228
+ readonly EMAIL_REQUIRED: 'email-required';
229
+ readonly PASSWORD_REQUIRED: 'password-required';
230
+ readonly MISSING_REQUIRED_FIELD: 'missing-required-field';
231
+ readonly NEW_PASSWORD_FIELD_IS_REQUIRED: 'newPassword-required';
232
+ readonly DENIED_STREAM_ACCESS: 'denied-stream-access';
233
+ readonly TOO_HIGH_ACCESS_FOR_SYSTEM_STREAMS: 'too-high-access-for-account-stream';
234
+ readonly FORBIDDEN_MULTIPLE_ACCOUNT_STREAMS: 'forbidden-multiple-account-streams-events';
235
+ readonly FORBIDDEN_ACCOUNT_EVENT_MODIFICATION: 'forbidden-none-editable-account-streams';
236
+ readonly FORBIDDEN_TO_CHANGE_ACCOUNT_STREAM_ID: 'forbidden-change-account-streams-id';
237
+ readonly FORBIDDEN_TO_EDIT_NONEDITABLE_ACCOUNT_FIELDS: 'forbidden-to-edit-noneditable-account-fields';
238
+ readonly UNKNOWN_USER: 'unknown-user';
239
+ readonly UNKNOWN_EMAIL: 'unknown-email';
240
+ };
241
+
242
+ export type CreateUserOptions = {
243
+ username: string;
244
+ password: string;
245
+ email: string;
246
+ /** Hosting key (from `flatHostings()`) or the literal `'auto'` */
247
+ hosting: string;
248
+ appId: string;
249
+ language?: string;
250
+ invitationToken?: string;
251
+ referer?: string;
252
+ };
253
+
254
+ export type HostingItem = {
255
+ key: string;
256
+ name: string;
257
+ description: string;
258
+ region: string;
259
+ zone: string;
260
+ availableCore: string;
261
+ available: boolean;
262
+ };
263
+
177
264
  export type StreamsQuery = {
178
265
  any?: Identifier[];
179
266
  all?: Identifier[];
@@ -636,7 +723,12 @@ declare module 'pryv' {
636
723
  fields: string[],
637
724
  points: Array<Array<number | string>>,
638
725
  ): Promise<HFSeriesAddResult>;
639
- accessInfo(): Promise<AccessInfo>;
726
+ /** Memoized; pass `forceRefresh: true` to bypass + refresh the cache. */
727
+ accessInfo(forceRefresh?: boolean): Promise<AccessInfo>;
728
+ /** Plan 66: update an access by composite id. */
729
+ updateAccess(id: string, changes: object): Promise<object>;
730
+ /** Plan 66: fetch an access including its full version history. */
731
+ getAccessWithHistory(id: string): Promise<{ access: object, current?: string, history?: object[] }>;
640
732
  revoke(throwOnFail?: boolean, usingConnection?: Connection): Promise<any>;
641
733
  readonly deltaTime: number;
642
734
  readonly apiEndpoint: string;
@@ -736,6 +828,24 @@ declare module 'pryv' {
736
828
  supportsHF(): Promise<boolean>;
737
829
  isDnsLess(): Promise<boolean>;
738
830
 
831
+ userExists(userId: string): Promise<boolean>;
832
+ userIdForEmail(email: string): Promise<string | null>;
833
+ availableHostings(): Promise<any>;
834
+ flatHostings(): Promise<HostingItem[]>;
835
+ createUser(opts: CreateUserOptions): Promise<{ username: string; apiEndpoint: string }>;
836
+ requestPasswordReset(userId: string, appId: string): Promise<void>;
837
+ resetPassword(userId: string, newPassword: string, resetToken: string, appId: string): Promise<void>;
838
+ mfaChallenge(userId: string, mfaToken: string): Promise<void>;
839
+ mfaVerify(userId: string, mfaToken: string, code: string): Promise<Connection>;
840
+
841
+ startAccessRequest(authRequest: AuthSettings['authRequest']): Promise<{
842
+ key: string;
843
+ authUrl: string;
844
+ poll: string;
845
+ pollRateMs: number;
846
+ }>;
847
+ pollAccessRequest(keyOrPollUrl: string): Promise<any>;
848
+
739
849
  static buildAPIEndpoint(
740
850
  serviceInfo: ServiceInfo,
741
851
  username: string,
@@ -1002,6 +1112,8 @@ declare module 'pryv' {
1002
1112
  getQueryParamsFromURL(url: string): KeyValue;
1003
1113
  };
1004
1114
  PryvError: typeof PryvError;
1115
+ MfaRequiredError: typeof MfaRequiredError;
1116
+ ERRORS: typeof ERRORS;
1005
1117
  version: version;
1006
1118
  };
1007
1119
 
package/src/index.js CHANGED
@@ -9,7 +9,10 @@
9
9
  * @property {pryv.Connection} Connection - To interact with an individual's (user) data set
10
10
  * @property {pryv.Browser} Browser - Browser Tools - Access request helpers and visuals (button)
11
11
  * @property {pryv.utils} utils - Exposes some utils for HTTP calls and tools to manipulate Pryv's API endpoints
12
- * @property {pryv.PryvError} PryvError - Custom error class with innerObject support
12
+ * @property {pryv.PryvError} PryvError - Custom error class with innerObject + structured API-error fields
13
+ * @property {pryv.MfaRequiredError} MfaRequiredError - Thrown by Service.login when the platform returns an mfaToken instead of a token. Carries `.mfaToken`.
14
+ * @property {pryv.StaleAccessIdError} StaleAccessIdError - Plan 66: thrown when a Pryv.io server rejects an `accesses.update` / `accesses.delete` with a 409 stale-resource. Refetch + retry.
15
+ * @property {Object} ERRORS - Catalogue of Pryv API error ids (mirrors open-pryv.io/components/errors)
13
16
  */
14
17
  module.exports = {
15
18
  Service: require('./Service'),
@@ -18,5 +21,8 @@ module.exports = {
18
21
  Browser: require('./Browser'),
19
22
  utils: require('./utils'),
20
23
  PryvError: require('./lib/PryvError'),
24
+ MfaRequiredError: require('./lib/MfaRequiredError'),
25
+ StaleAccessIdError: require('./lib/StaleAccessIdError'),
26
+ ERRORS: require('./lib/errorIds'),
21
27
  version: require('../package.json').version
22
28
  };