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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pryv",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "Pryv JavaScript library",
5
5
  "keywords": [
6
6
  "Pryv",
@@ -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.stateChangeListeners.push(this.settings.onStateChange);
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 Plan 66 `accessUpdated` server-push events. A failed
81
- * server fetch leaves any prior cached value intact.
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
- * 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.
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
- * Plan 66: fetch an access by composite id including its full version
460
- * history (oldest first). Server: `accesses.getOne ?includeHistory=true`.
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
- /** Plan 66: update an access by composite id. */
728
+ /** Update an access by composite id. */
729
729
  updateAccess(id: string, changes: object): Promise<object>;
730
- /** Plan 66: fetch an access including its full version history. */
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 - Plan 66: thrown when a Pryv.io server rejects an `accesses.update` / `accesses.delete` with a 409 stale-resource. Refetch + retry.
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: require('./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
- * Plan 66: typed error surfaced when a Pryv.io server (≥ 2.0.0-pre.X)
10
- * rejects an `accesses.update` or `accesses.delete` call with a
11
- * 409 `stale-resource` response.
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
- * Plan 66 (open-pryv.io 2.0.0-pre.X): parse a wire-format access
230
- * reference into `{ base, serial }`. Accepts both bare cuid
231
- * (`"abc123"` → `{ base: 'abc123', serial: null }`) and composite
232
- * (`"abc123:3"` → `{ base: 'abc123', serial: 3 }`). Throws on
233
- * malformed input. Apply this to `access.id`, `access.createdBy`,
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
- * Plan 66: render an `{ base, serial }` pair back to the wire
260
- * format. Bare cuid when serial is null/undefined; `<base>:<serial>`
261
- * otherwise. Mostly used to construct the composite id when calling
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
  });
@@ -500,7 +500,7 @@ describe('[CONX] Connection', () => {
500
500
  expect(newUser.access.token).to.equal(accessInfoUser.token);
501
501
  });
502
502
 
503
- // Plan 66 — accessInfo caching + forceRefresh.
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;
@@ -60,7 +60,7 @@ describe('[UTLX] utils', function () {
60
60
  done();
61
61
  });
62
62
 
63
- // Plan 66 — composite access references.
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');