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 +1 -1
- package/src/Connection.js +57 -3
- package/src/Service.js +376 -6
- package/src/index.d.ts +114 -2
- package/src/index.js +7 -1
- package/src/lib/MfaRequiredError.js +46 -0
- package/src/lib/PryvError.js +38 -4
- package/src/lib/StaleAccessIdError.js +40 -0
- package/src/lib/errorIds.js +67 -0
- package/src/utils.js +50 -0
- package/test/Connection.test.js +26 -0
- package/test/PryvError.test.js +34 -0
- package/test/Service.accessRequest.test.js +78 -0
- package/test/Service.createUser.test.js +104 -0
- package/test/Service.hostings.test.js +53 -0
- package/test/Service.mfa.test.js +94 -0
- package/test/Service.passwordReset.test.js +87 -0
- package/test/Service.userExists.test.js +29 -0
- package/test/Service.userIdForEmail.test.js +30 -0
- package/test/utils.test.js +44 -0
package/package.json
CHANGED
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
};
|