pryv 3.14.2 → 3.15.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/README.md CHANGED
@@ -131,6 +131,13 @@ Here is an implementation of the [Pryv.io authentication process](https://api.pr
131
131
  optIn: ['location'] // offered UNticked; the user opts in
132
132
  // a permission in neither list is optional and shown pre-selected
133
133
  },
134
+ // optional (core 2.0.0-rc.32+): cross-account messaging invites the
135
+ // user answers in the same sign-in (1 to 8). The ACCEPTED state then
136
+ // carries `cmcInvites`, one outcome per invite ({ acceptEventId, ... },
137
+ // { declined: true } or { reason }), a hint: the requester learns the
138
+ // truth from its own inbox. Declining a mandatory one refuses the sign-in
139
+ // (REFUSED, reasonId 'REFUSED_MANDATORY_CONSENT'). Ignored by older cores.
140
+ // cmcInvites: [{ capabilityUrl: invite.capabilityUrl, mandatory: true }],
134
141
  clientData: {
135
142
  'app-web-auth:description': {
136
143
  'type': 'note/txt', 'content': 'This is a consent message.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pryv",
3
- "version": "3.14.2",
3
+ "version": "3.15.0",
4
4
  "description": "Pryv JavaScript library",
5
5
  "keywords": [
6
6
  "Pryv",
package/src/Service.js CHANGED
@@ -524,6 +524,8 @@ class Service {
524
524
  // all-or-nothing, which a caller may want to know before showing the
525
525
  // approve link.
526
526
  if (body.consent != null) envelope.consent = body.consent;
527
+ // Same for consent invites: echoed only by a core that understood them.
528
+ if (body.cmcInvites != null) envelope.cmcInvites = body.cmcInvites;
527
529
  // polling by key (pollAccessRequest, connectFromKey) then reaches the
528
530
  // core that holds the request
529
531
  pollUrls.remember(envelope.key, envelope.poll);
package/src/index.d.ts CHANGED
@@ -891,6 +891,8 @@ declare module 'pryv' {
891
891
  pollRateMs: number;
892
892
  /** Echoed only by a core that understood `authRequest.consent`. */
893
893
  consent?: AuthRequestConsentForm;
894
+ /** Echoed only by a core that understood `authRequest.cmcInvites`. */
895
+ cmcInvites?: AuthCmcInvite[];
894
896
  }>;
895
897
  /**
896
898
  * Poll an access request once, by its full poll URL (recommended) or its
@@ -982,6 +984,35 @@ declare module 'pryv' {
982
984
  >;
983
985
  };
984
986
 
987
+ /**
988
+ * A cross-account messaging invite the user is asked to answer in the same
989
+ * authorisation request (`authRequest.cmcInvites`): the capability URL the
990
+ * app received, whether declining it refuses the whole request, and for
991
+ * which account it is accepted (`'target'`: the account the access is
992
+ * granted for, when the user acts for an account it manages).
993
+ */
994
+ export type AuthRequestCmcInvite = {
995
+ capabilityUrl: string;
996
+ mandatory?: boolean;
997
+ for?: 'self' | 'target';
998
+ };
999
+
1000
+ /** An invite as the core echoes it back, with its defaults filled in. */
1001
+ export type AuthCmcInvite = {
1002
+ capabilityUrl: string;
1003
+ mandatory: boolean;
1004
+ for: 'self' | 'target';
1005
+ };
1006
+
1007
+ /**
1008
+ * What the auth page did with one invite, in the ACCEPTED body (one per
1009
+ * invite, in order). A hint: the requester learns the truth from its inbox.
1010
+ */
1011
+ export type AuthCmcInviteOutcome =
1012
+ | { acceptEventId: string; dataGrantAccessId?: string; acceptedFor?: 'self' }
1013
+ | { declined: true }
1014
+ | { reason: string };
1015
+
985
1016
  export type States =
986
1017
  | 'ERROR'
987
1018
  | 'LOADING'
@@ -1025,6 +1056,8 @@ declare module 'pryv' {
1025
1056
  defaultName: string;
1026
1057
  }>;
1027
1058
  consent?: AuthRequestConsentForm;
1059
+ /** Present only when the request carried `cmcInvites` the core understood. */
1060
+ cmcInvites?: AuthCmcInvite[];
1028
1061
  requestingAppId: string;
1029
1062
  returnURL?: string | null;
1030
1063
  serviceInfo?: ServiceInfo;
@@ -1055,6 +1088,8 @@ declare module 'pryv' {
1055
1088
  profile?: AuthProfile;
1056
1089
  /** One-time credential hand-off key (shared-secret delivery), in place of `token`. */
1057
1090
  handoff?: { type: 'shared-secret'; key: string };
1091
+ /** What the auth page did with each invite of the request (a hint). */
1092
+ cmcInvites?: AuthCmcInviteOutcome[];
1058
1093
  };
1059
1094
  SIGNOUT: {};
1060
1095
  SWITCHING: {
@@ -1089,6 +1124,7 @@ declare module 'pryv' {
1089
1124
  requestingAppId: string;
1090
1125
  requestedPermissions: AuthRequestedPermission[];
1091
1126
  consent?: AuthRequestConsentForm;
1127
+ cmcInvites?: AuthCmcInvite[];
1092
1128
  lang?: string;
1093
1129
  returnURL?: string;
1094
1130
  clientData?: KeyValue;
@@ -1125,6 +1161,13 @@ declare module 'pryv' {
1125
1161
  languageCode?: string;
1126
1162
  requestedPermissions: AuthRequestedPermission[];
1127
1163
  consent?: AuthRequestConsent;
1164
+ /**
1165
+ * Cross-account messaging invites the user answers in the same
1166
+ * sign-in (1 to 8). The ACCEPTED state carries one outcome per invite;
1167
+ * a declined mandatory invite ends the request REFUSED with
1168
+ * `reasonId: 'REFUSED_MANDATORY_CONSENT'`. An older core ignores the field.
1169
+ */
1170
+ cmcInvites?: AuthRequestCmcInvite[];
1128
1171
  /**
1129
1172
  * Where the sign-in happens: `'auto#'` (default, also when unset or
1130
1173
  * `false`) opens a popup on desktop and redirects on a phone or tablet;
@@ -41,6 +41,8 @@ class MfaRequiredError extends PryvError {
41
41
  this.id = (apiErr && apiErr.id) || 'mfa-required';
42
42
  this.status = response && response.status;
43
43
  this.response = { body, status: response && response.status };
44
+ // Read by name, but not printed with the error (like `innerObject`).
45
+ Object.defineProperty(this, 'mfaToken', { enumerable: false });
44
46
 
45
47
  if (Error.captureStackTrace) {
46
48
  Error.captureStackTrace(this, MfaRequiredError);
@@ -35,6 +35,10 @@ class PryvError extends Error {
35
35
  this.status = undefined;
36
36
  /** @type {{ body: any, status: number }|undefined} Raw response */
37
37
  this.response = undefined;
38
+ // `innerObject` and `response` hold raw platform answers: still readable and
39
+ // writable, but not enumerable, so `console.log(err)` does not print them.
40
+ Object.defineProperty(this, 'innerObject', { enumerable: false });
41
+ Object.defineProperty(this, 'response', { enumerable: false });
38
42
 
39
43
  if (Error.captureStackTrace) {
40
44
  Error.captureStackTrace(this, PryvError);
@@ -71,4 +71,14 @@ describe('[PERX] PryvError', function () {
71
71
  expect(error.status).to.equal(502);
72
72
  expect(error.response).to.deep.equal({ body: undefined, status: 502 });
73
73
  });
74
+
75
+ it('[PERJ] innerObject and response are readable but not enumerable (not printed with the error)', function () {
76
+ const error = new PryvError('Outer', { id: 'invalid-access-token', secret: 'tok' });
77
+ error.response = { body: { secret: 'tok' }, status: 401 };
78
+ expect(error.innerObject.id).to.equal('invalid-access-token');
79
+ expect(error.response.status).to.equal(401);
80
+ expect(Object.keys(error)).to.not.include('innerObject');
81
+ expect(Object.keys(error)).to.not.include('response');
82
+ expect(require('util').inspect(error)).to.not.include('tok');
83
+ });
74
84
  });
@@ -108,4 +108,20 @@ describe('[ARQC] auth-request consent sidecar', function () {
108
108
  expect(envOld.key).to.equal('k2');
109
109
  expect(envOld).to.not.have.property('consent');
110
110
  });
111
+
112
+ it('[LCI3] posts cmcInvites verbatim and returns the echo a core sends, nothing when it does not', async function () {
113
+ const invites = [{ capabilityUrl: 'https://cap@doctor.test.local/', mandatory: true }];
114
+ const echo = [{ capabilityUrl: 'https://cap@doctor.test.local/', mandatory: true, for: 'self' }];
115
+ stubFetch({ key: 'k3', authUrl: 'https://auth/', poll: 'https://poll/k3', poll_rate_ms: 1000, cmcInvites: echo });
116
+ const service = new pryv.Service('https://reg.test.local/service/info');
117
+ const env = await service.startAccessRequest({ ...REQUEST, cmcInvites: invites });
118
+ expect(posted[0].cmcInvites).to.deep.equal(invites);
119
+ expect(env.cmcInvites).to.deep.equal(echo);
120
+ if (realFetch) { global.fetch = realFetch; realFetch = null; }
121
+
122
+ stubFetch({ key: 'k4', authUrl: 'https://auth/', poll: 'https://poll/k4', poll_rate_ms: 1000 });
123
+ const older = new pryv.Service('https://reg.test.local/service/info');
124
+ const envOld = await older.startAccessRequest({ ...REQUEST, cmcInvites: invites });
125
+ expect(envOld).to.not.have.property('cmcInvites');
126
+ });
111
127
  });
@@ -115,5 +115,18 @@ describe('[MFLX] Service MFA', function () {
115
115
  );
116
116
  expect(err.method).to.equal(undefined);
117
117
  });
118
+
119
+ it('[MFEE] mfaToken and the raw response are readable but not printed with the error', function () {
120
+ const err = new pryv.MfaRequiredError(
121
+ 'tok-secret-4',
122
+ { status: 200 },
123
+ { mfaToken: 'tok-secret-4' }
124
+ );
125
+ expect(err.mfaToken).to.equal('tok-secret-4');
126
+ expect(err.response.body.mfaToken).to.equal('tok-secret-4');
127
+ expect(Object.keys(err)).to.not.include('mfaToken');
128
+ expect(Object.keys(err)).to.not.include('response');
129
+ expect(require('util').inspect(err)).to.not.include('tok-secret-4');
130
+ });
118
131
  });
119
132
  });