pryv 3.4.0 → 3.5.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/Auth/AuthController.js +49 -1
- package/src/Connection.js +8 -8
- package/src/Service.js +34 -0
- package/src/index.d.ts +2 -2
- package/src/index.js +25 -3
- package/src/lib/StaleAccessIdError.js +3 -3
- package/src/utils.js +8 -8
- package/test/AuthController.test.js +74 -0
- package/test/Connection.test.js +1 -1
- package/test/Service.accessRequest.test.js +24 -0
- package/test/utils.test.js +1 -1
package/package.json
CHANGED
|
@@ -22,8 +22,15 @@ class AuthController {
|
|
|
22
22
|
validateSettings.call(this, settings);
|
|
23
23
|
|
|
24
24
|
this.stateChangeListeners = [];
|
|
25
|
+
// External `onStateChange` callers only see `{ status, id, key, serviceInfo? }`
|
|
26
|
+
// on AUTHORIZED — credentials (`username`, `token`, `apiEndpoint`) stay
|
|
27
|
+
// inside the lib. Internal listeners (e.g. LoginButton, for cookie
|
|
28
|
+
// autologin) get the full unfiltered state.
|
|
25
29
|
if (this.settings.onStateChange) {
|
|
26
|
-
this.
|
|
30
|
+
const externalListener = this.settings.onStateChange;
|
|
31
|
+
this.stateChangeListeners.push(function (state) {
|
|
32
|
+
externalListener(filterForExternalListener(state));
|
|
33
|
+
});
|
|
27
34
|
}
|
|
28
35
|
this.service = service;
|
|
29
36
|
|
|
@@ -166,6 +173,10 @@ class AuthController {
|
|
|
166
173
|
async startAuthRequest () {
|
|
167
174
|
// @ts-ignore - postAccess uses .call(this) for context
|
|
168
175
|
this.state = await postAccess.call(this);
|
|
176
|
+
// Remember the polling key so listeners on the terminal AUTHORIZED
|
|
177
|
+
// state can be handed `{ key, serviceInfo? }` (the polling response
|
|
178
|
+
// itself doesn't echo `key` back).
|
|
179
|
+
this._authFlowKey = this.state?.key;
|
|
169
180
|
|
|
170
181
|
await doPolling.call(this);
|
|
171
182
|
|
|
@@ -205,6 +216,11 @@ class AuthController {
|
|
|
205
216
|
// @ts-ignore - this is bound via .call()
|
|
206
217
|
setTimeout(await doPolling.bind(this), this.state?.poll_rate_ms);
|
|
207
218
|
} else {
|
|
219
|
+
// Carry the key forward — listeners on the narrow public surface
|
|
220
|
+
// need it, and the server doesn't echo it back on ACCEPTED.
|
|
221
|
+
if (this._authFlowKey != null && pollResponse.key == null) {
|
|
222
|
+
pollResponse.key = this._authFlowKey;
|
|
223
|
+
}
|
|
208
224
|
this.state = pollResponse;
|
|
209
225
|
}
|
|
210
226
|
|
|
@@ -245,6 +261,38 @@ class AuthController {
|
|
|
245
261
|
|
|
246
262
|
// ----------- private methods -------------
|
|
247
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Narrow the state passed to *external* `onStateChange` callers so the
|
|
266
|
+
* calling app sees only `{ status, id, key, serviceInfo? }` on the
|
|
267
|
+
* terminal AUTHORIZED state reached through the auth-flow polling path.
|
|
268
|
+
* `username` / `token` / `apiEndpoint` are kept inside the lib; the
|
|
269
|
+
* calling app uses `pryv.connectFromKey(key, serviceInfoUrl)` to obtain
|
|
270
|
+
* a `Connection`.
|
|
271
|
+
*
|
|
272
|
+
* The cookie-autologin path (no fresh `key` available, restored from
|
|
273
|
+
* `LoginButton.getAuthorizationData()`) passes through unchanged so
|
|
274
|
+
* existing pages that build a `Connection` directly from the restored
|
|
275
|
+
* state on page load keep working.
|
|
276
|
+
*
|
|
277
|
+
* Non-AUTHORIZED states pass through unchanged so error messages /
|
|
278
|
+
* loading flags / etc. still reach the listener.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} state - full internal state
|
|
281
|
+
* @returns {Object} narrowed state
|
|
282
|
+
*/
|
|
283
|
+
function filterForExternalListener (state) {
|
|
284
|
+
if (state == null || state.status !== AuthStates.AUTHORIZED) {
|
|
285
|
+
return state;
|
|
286
|
+
}
|
|
287
|
+
// No key → cookie-autologin path; preserve existing shape.
|
|
288
|
+
if (state.key == null) {
|
|
289
|
+
return state;
|
|
290
|
+
}
|
|
291
|
+
const out = { status: state.status, id: state.id, key: state.key };
|
|
292
|
+
if (state.serviceInfo != null) out.serviceInfo = state.serviceInfo;
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
|
|
248
296
|
async function checkAutoLogin (authController) {
|
|
249
297
|
const loginButton = authController.loginButton;
|
|
250
298
|
if (loginButton == null) {
|
package/src/Connection.js
CHANGED
|
@@ -77,8 +77,8 @@ class Connection {
|
|
|
77
77
|
* caches the result; subsequent calls return the cached copy in O(1).
|
|
78
78
|
* Pass `forceRefresh: true` to invalidate the cache and fetch a fresh
|
|
79
79
|
* copy from the server — used internally by `connection.socket` to
|
|
80
|
-
* react to
|
|
81
|
-
*
|
|
80
|
+
* react to `accessUpdated` server-push events. A failed server
|
|
81
|
+
* fetch leaves any prior cached value intact.
|
|
82
82
|
*
|
|
83
83
|
* @param {boolean} [forceRefresh=false] - bypass + refresh the cache
|
|
84
84
|
* @returns {Promise<AccessInfo>} Promise resolving to the access info
|
|
@@ -429,10 +429,10 @@ class Connection {
|
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* into a typed `StaleAccessIdError` so callers can `instanceof`-test
|
|
435
|
-
* refetch + retry without re-parsing the inner error.
|
|
432
|
+
* Update an access by composite id (Pryv.io ≥ 2.0.0-pre.X). Wraps
|
|
433
|
+
* `accesses.update` and translates the 409 `stale-resource` response
|
|
434
|
+
* into a typed `StaleAccessIdError` so callers can `instanceof`-test
|
|
435
|
+
* and refetch + retry without re-parsing the inner error.
|
|
436
436
|
*
|
|
437
437
|
* Pass `id` as the wire-format reference returned by the server — bare
|
|
438
438
|
* cuid on a never-updated access, composite `<base>:<serial>` otherwise.
|
|
@@ -456,8 +456,8 @@ class Connection {
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
/**
|
|
459
|
-
*
|
|
460
|
-
*
|
|
459
|
+
* Fetch an access by composite id including its full version history
|
|
460
|
+
* (oldest first). Server: `accesses.getOne ?includeHistory=true`.
|
|
461
461
|
*
|
|
462
462
|
* Useful for audit views. Pass the composite `<base>:<serial>` to
|
|
463
463
|
* inspect a specific past version (the result's `current` field then
|
package/src/Service.js
CHANGED
|
@@ -521,6 +521,40 @@ class Service {
|
|
|
521
521
|
return body;
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Resolve a completed auth-flow polling key into a `Connection`.
|
|
526
|
+
*
|
|
527
|
+
* Pairs with `Service.startAccessRequest` / `Service.pollAccessRequest`
|
|
528
|
+
* and the headless polling pattern: the calling app holds only the
|
|
529
|
+
* `key` returned by the auth-flow (not the underlying token /
|
|
530
|
+
* apiEndpoint), and uses this method to build a working `Connection`.
|
|
531
|
+
*
|
|
532
|
+
* The implementation polls `<access>/<key>` once; the call MUST be
|
|
533
|
+
* made while the access is still in the ACCEPTED state (which
|
|
534
|
+
* persists until expiry — see `expireAfter` on the access request).
|
|
535
|
+
*
|
|
536
|
+
* @param {string} key - polling key from `startAccessRequest`
|
|
537
|
+
* @returns {Promise<Connection>}
|
|
538
|
+
* @throws {PryvError} if the key is not ACCEPTED (NEED_SIGNIN, REFUSED, ERROR)
|
|
539
|
+
*/
|
|
540
|
+
async connectFromKey (key) {
|
|
541
|
+
if (!key) {
|
|
542
|
+
throw new PryvError('connectFromKey requires a key');
|
|
543
|
+
}
|
|
544
|
+
const body = await this.pollAccessRequest(key);
|
|
545
|
+
if (body.status !== 'ACCEPTED') {
|
|
546
|
+
throw new PryvError(
|
|
547
|
+
'connectFromKey: access is not ACCEPTED (status=' + body.status + ')'
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (!body.apiEndpoint) {
|
|
551
|
+
throw new PryvError(
|
|
552
|
+
'connectFromKey: ACCEPTED response missing apiEndpoint'
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
return new Connection(body.apiEndpoint, this);
|
|
556
|
+
}
|
|
557
|
+
|
|
524
558
|
/**
|
|
525
559
|
* Set a new password using a reset token (from the reset email).
|
|
526
560
|
* Pre-auth — no login token required.
|
package/src/index.d.ts
CHANGED
|
@@ -725,9 +725,9 @@ declare module 'pryv' {
|
|
|
725
725
|
): Promise<HFSeriesAddResult>;
|
|
726
726
|
/** Memoized; pass `forceRefresh: true` to bypass + refresh the cache. */
|
|
727
727
|
accessInfo(forceRefresh?: boolean): Promise<AccessInfo>;
|
|
728
|
-
/**
|
|
728
|
+
/** Update an access by composite id. */
|
|
729
729
|
updateAccess(id: string, changes: object): Promise<object>;
|
|
730
|
-
/**
|
|
730
|
+
/** Fetch an access including its full version history. */
|
|
731
731
|
getAccessWithHistory(id: string): Promise<{ access: object, current?: string, history?: object[] }>;
|
|
732
732
|
revoke(throwOnFail?: boolean, usingConnection?: Connection): Promise<any>;
|
|
733
733
|
readonly deltaTime: number;
|
package/src/index.js
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* @property {pryv.utils} utils - Exposes some utils for HTTP calls and tools to manipulate Pryv's API endpoints
|
|
12
12
|
* @property {pryv.PryvError} PryvError - Custom error class with innerObject + structured API-error fields
|
|
13
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 -
|
|
14
|
+
* @property {pryv.StaleAccessIdError} StaleAccessIdError - Thrown when a Pryv.io server rejects an `accesses.update` / `accesses.delete` with a 409 stale-resource. Refetch + retry.
|
|
15
15
|
* @property {Object} ERRORS - Catalogue of Pryv API error ids (mirrors open-pryv.io/components/errors)
|
|
16
16
|
*/
|
|
17
|
+
const Service = require('./Service');
|
|
18
|
+
|
|
17
19
|
module.exports = {
|
|
18
|
-
Service
|
|
20
|
+
Service,
|
|
19
21
|
Connection: require('./Connection'),
|
|
20
22
|
Auth: require('./Auth'),
|
|
21
23
|
Browser: require('./Browser'),
|
|
@@ -24,5 +26,25 @@ module.exports = {
|
|
|
24
26
|
MfaRequiredError: require('./lib/MfaRequiredError'),
|
|
25
27
|
StaleAccessIdError: require('./lib/StaleAccessIdError'),
|
|
26
28
|
ERRORS: require('./lib/errorIds'),
|
|
27
|
-
version: require('../package.json').version
|
|
29
|
+
version: require('../package.json').version,
|
|
30
|
+
connectFromKey
|
|
28
31
|
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Module-level convenience over `Service#connectFromKey` — builds a
|
|
35
|
+
* `Service` on the fly, fetches its info, and resolves the given
|
|
36
|
+
* auth-flow polling key into a working `Connection`.
|
|
37
|
+
*
|
|
38
|
+
* Mirrors the `pryv.connectFromKey(key, serviceInfoUrl)` shape the
|
|
39
|
+
* headless polling pattern documents.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} key - polling key from `Service.startAccessRequest`
|
|
42
|
+
* @param {string} serviceInfoUrl - URL of the platform's `/service/info`
|
|
43
|
+
* @param {Object} [serviceCustomizations] - same shape as `new Service(url, customizations)`
|
|
44
|
+
* @returns {Promise<import('./Connection')>}
|
|
45
|
+
*/
|
|
46
|
+
async function connectFromKey (key, serviceInfoUrl, serviceCustomizations) {
|
|
47
|
+
const service = new Service(serviceInfoUrl, serviceCustomizations);
|
|
48
|
+
await service.info();
|
|
49
|
+
return service.connectFromKey(key);
|
|
50
|
+
}
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
const PryvError = require('./PryvError');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Typed error surfaced when a Pryv.io server (≥ 2.0.0-pre.X) rejects
|
|
10
|
+
* an `accesses.update` or `accesses.delete` call with a 409
|
|
11
|
+
* `stale-resource` response.
|
|
12
12
|
*
|
|
13
13
|
* The composite access id `<base>:<serial>` carries the version the
|
|
14
14
|
* caller last observed. If the access has since been updated, the
|
package/src/utils.js
CHANGED
|
@@ -226,11 +226,11 @@ const utils = module.exports = {
|
|
|
226
226
|
},
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
229
|
+
* Parse a wire-format access reference into `{ base, serial }`
|
|
230
|
+
* (Pryv.io ≥ 2.0.0-pre.X). Accepts both bare cuid (`"abc123"` →
|
|
231
|
+
* `{ base: 'abc123', serial: null }`) and composite (`"abc123:3"` →
|
|
232
|
+
* `{ base: 'abc123', serial: 3 }`). Throws on malformed input.
|
|
233
|
+
* Apply this to `access.id`, `access.createdBy`,
|
|
234
234
|
* `access.modifiedBy`, and `streamIds` entries of the form
|
|
235
235
|
* `access-<base>:<serial>` from audit events.
|
|
236
236
|
* @memberof pryv.utils
|
|
@@ -256,9 +256,9 @@ const utils = module.exports = {
|
|
|
256
256
|
},
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
259
|
+
* Render an `{ base, serial }` pair back to the wire format. Bare
|
|
260
|
+
* cuid when serial is null/undefined; `<base>:<serial>` otherwise.
|
|
261
|
+
* Mostly used to construct the composite id when calling
|
|
262
262
|
* `connection.api()` for `accesses.update` / `accesses.delete`.
|
|
263
263
|
* @memberof pryv.utils
|
|
264
264
|
* @param {{ base: string, serial?: number | null }} ref
|
|
@@ -190,4 +190,78 @@ describe('[ACNX] AuthController', function () {
|
|
|
190
190
|
expect(auth.state.id).to.equal(AuthStates.LOADING); // retro-compatibility
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
|
+
|
|
194
|
+
describe('[AFLT] External listener filtering', function () {
|
|
195
|
+
it('[AFLA] AUTHORIZED state passes only { status, id, key, serviceInfo } to onStateChange', function () {
|
|
196
|
+
const seen = [];
|
|
197
|
+
const auth = new AuthController({
|
|
198
|
+
authRequest: {
|
|
199
|
+
requestingAppId: 'test-app',
|
|
200
|
+
requestedPermissions: []
|
|
201
|
+
},
|
|
202
|
+
onStateChange: (state) => seen.push(state)
|
|
203
|
+
}, service);
|
|
204
|
+
|
|
205
|
+
auth.state = {
|
|
206
|
+
status: AuthStates.AUTHORIZED,
|
|
207
|
+
key: 'abc123',
|
|
208
|
+
serviceInfo: { name: 'Test' },
|
|
209
|
+
apiEndpoint: 'https://token@example.com/',
|
|
210
|
+
username: 'alice',
|
|
211
|
+
token: 'secret-token'
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const last = seen[seen.length - 1];
|
|
215
|
+
expect(last.status).to.equal(AuthStates.AUTHORIZED);
|
|
216
|
+
expect(last.id).to.equal(AuthStates.AUTHORIZED);
|
|
217
|
+
expect(last.key).to.equal('abc123');
|
|
218
|
+
expect(last.serviceInfo).to.deep.equal({ name: 'Test' });
|
|
219
|
+
// Credentials must not leak to the calling app.
|
|
220
|
+
expect(last.apiEndpoint).to.equal(undefined);
|
|
221
|
+
expect(last.username).to.equal(undefined);
|
|
222
|
+
expect(last.token).to.equal(undefined);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('[AFLC] AUTHORIZED state from cookie-autologin (no key) passes through unchanged', function () {
|
|
226
|
+
// Mirrors `checkAutoLogin()`'s state shape: it spreads stored
|
|
227
|
+
// credentials into the AUTHORIZED state without a key, since the
|
|
228
|
+
// key is not persisted.
|
|
229
|
+
const seen = [];
|
|
230
|
+
const auth = new AuthController({
|
|
231
|
+
authRequest: {
|
|
232
|
+
requestingAppId: 'test-app',
|
|
233
|
+
requestedPermissions: []
|
|
234
|
+
},
|
|
235
|
+
onStateChange: (state) => seen.push(state)
|
|
236
|
+
}, service);
|
|
237
|
+
|
|
238
|
+
auth.state = {
|
|
239
|
+
status: AuthStates.AUTHORIZED,
|
|
240
|
+
apiEndpoint: 'https://token@example.com/',
|
|
241
|
+
username: 'alice'
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const last = seen[seen.length - 1];
|
|
245
|
+
// Backwards-compat: existing pages building Connection directly
|
|
246
|
+
// from `state.apiEndpoint` on page reload keep working.
|
|
247
|
+
expect(last.apiEndpoint).to.equal('https://token@example.com/');
|
|
248
|
+
expect(last.username).to.equal('alice');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('[AFLB] non-AUTHORIZED states pass through unchanged', function () {
|
|
252
|
+
const seen = [];
|
|
253
|
+
const auth = new AuthController({
|
|
254
|
+
authRequest: {
|
|
255
|
+
requestingAppId: 'test-app',
|
|
256
|
+
requestedPermissions: []
|
|
257
|
+
},
|
|
258
|
+
onStateChange: (state) => seen.push(state)
|
|
259
|
+
}, service);
|
|
260
|
+
|
|
261
|
+
auth.state = { status: AuthStates.ERROR, message: 'boom', error: new Error('e') };
|
|
262
|
+
const last = seen[seen.length - 1];
|
|
263
|
+
expect(last.status).to.equal(AuthStates.ERROR);
|
|
264
|
+
expect(last.message).to.equal('boom');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
193
267
|
});
|
package/test/Connection.test.js
CHANGED
|
@@ -500,7 +500,7 @@ describe('[CONX] Connection', () => {
|
|
|
500
500
|
expect(newUser.access.token).to.equal(accessInfoUser.token);
|
|
501
501
|
});
|
|
502
502
|
|
|
503
|
-
//
|
|
503
|
+
// accessInfo caching + forceRefresh.
|
|
504
504
|
|
|
505
505
|
it('[CAIC] accessInfo() memoizes — second call returns the same object reference', async () => {
|
|
506
506
|
const regexAPIandToken = /(.+):\/\/(.+)/gm;
|
|
@@ -38,6 +38,30 @@ describe('[ARQX] Service access-request init', function () {
|
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
describe('[CFKX] Service.connectFromKey', function () {
|
|
42
|
+
it('[CFKA] rejects when key is missing', async function () {
|
|
43
|
+
let caught;
|
|
44
|
+
try { await service.connectFromKey(); } catch (e) { caught = e; }
|
|
45
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('[CFKB] throws when the access is still NEED_SIGNIN', async function () {
|
|
49
|
+
this.timeout(15000);
|
|
50
|
+
const env = await service.startAccessRequest({
|
|
51
|
+
requestingAppId: 'jslib-test',
|
|
52
|
+
requestedPermissions: [{
|
|
53
|
+
streamId: 'data',
|
|
54
|
+
level: 'read',
|
|
55
|
+
defaultName: 'Test'
|
|
56
|
+
}]
|
|
57
|
+
});
|
|
58
|
+
let caught;
|
|
59
|
+
try { await service.connectFromKey(env.key); } catch (e) { caught = e; }
|
|
60
|
+
expect(caught).to.be.instanceOf(pryv.PryvError);
|
|
61
|
+
expect(caught.message).to.include('NEED_SIGNIN');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
41
65
|
describe('[APRX] Service.pollAccessRequest', function () {
|
|
42
66
|
it('[APRA] rejects when key is missing', async function () {
|
|
43
67
|
let caught;
|
package/test/utils.test.js
CHANGED
|
@@ -60,7 +60,7 @@ describe('[UTLX] utils', function () {
|
|
|
60
60
|
done();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// Composite access references.
|
|
64
64
|
|
|
65
65
|
it('[UTLF] parseAccessRef on a bare cuid returns { base, serial: null }', function () {
|
|
66
66
|
const ref = pryv.utils.parseAccessRef('abc123def456');
|