pryv 3.2.0 → 3.3.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
@@ -57,6 +57,7 @@ Other distributions available:
57
57
 
58
58
  - Socket.IO: [NPM package](https://www.npmjs.com/package/@pryv/socket.io), [README](https://github.com/pryv/lib-js/tree/master/components/pryv-socket.io#readme)
59
59
  - Monitor: [NPM package](https://www.npmjs.com/package/@pryv/monitor), [README](https://github.com/pryv/lib-js/tree/master/components/pryv-monitor#readme)
60
+ - CMC (Cross-account Messaging & Consent) client helpers: [NPM package](https://www.npmjs.com/package/@pryv/cmc), [README](https://github.com/pryv/lib-js/tree/master/components/pryv-cmc#readme)
60
61
 
61
62
 
62
63
  ### Quick example
@@ -677,6 +678,7 @@ The project is structured as a monorepo with components (a.k.a. workspaces in NP
677
678
  - `pryv`: the library
678
679
  - `pryv-socket.io`: Socket.IO add-on
679
680
  - `pryv-monitor`: Monitor add-on
681
+ - `pryv-cmc`: CMC (Cross-account Messaging & Consent) client helpers
680
682
 
681
683
 
682
684
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pryv",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Pryv JavaScript library",
5
5
  "keywords": [
6
6
  "Pryv",
package/src/index.d.ts CHANGED
@@ -1069,10 +1069,24 @@ declare module 'pryv' {
1069
1069
  token: string | null;
1070
1070
  };
1071
1071
 
1072
+ /**
1073
+ * Decomposed form of an APIEndpoint, returned by
1074
+ * `pryv.utils.decomposeAPIEndpoint`. `host` is the canonical platform
1075
+ * host (no `<username>.` subdomain prefix in subdomain-style
1076
+ * deployments) — the same identity cross-account features (CMC
1077
+ * counterparty slugs, etc.) key on.
1078
+ */
1079
+ export type DecomposedAPIEndpoint = {
1080
+ token: string | null;
1081
+ username: string | null;
1082
+ host: string;
1083
+ };
1084
+
1072
1085
  export const utils: {
1073
1086
  isBrowser(): boolean;
1074
1087
  extractTokenAndAPIEndpoint(apiEndpoint: string): TokenAndAPIEndpoint;
1075
1088
  buildAPIEndpoint(tokenAndAPI: TokenAndAPIEndpoint): string;
1089
+ decomposeAPIEndpoint(apiEndpoint: string, serviceInfoApi: string): DecomposedAPIEndpoint;
1076
1090
  browserIsMobileOrTablet(navigator?: string | Navigator): boolean;
1077
1091
  cleanURLFromPrYvParams(url: string): string;
1078
1092
  getQueryParamsFromURL(url: string): KeyValue;
@@ -1107,6 +1121,7 @@ declare module 'pryv' {
1107
1121
  isBrowser(): boolean;
1108
1122
  extractTokenAndAPIEndpoint(apiEndpoint: string): TokenAndAPIEndpoint;
1109
1123
  buildAPIEndpoint(tokenAndAPI: TokenAndAPIEndpoint): string;
1124
+ decomposeAPIEndpoint(apiEndpoint: string, serviceInfoApi: string): DecomposedAPIEndpoint;
1110
1125
  browserIsMobileOrTablet(navigator?: string | Navigator): boolean;
1111
1126
  cleanURLFromPrYvParams(url: string): string;
1112
1127
  getQueryParamsFromURL(url: string): KeyValue;
package/src/index.js CHANGED
@@ -13,7 +13,6 @@
13
13
  * @property {pryv.MfaRequiredError} MfaRequiredError - Thrown by Service.login when the platform returns an mfaToken instead of a token. Carries `.mfaToken`.
14
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
15
  * @property {Object} ERRORS - Catalogue of Pryv API error ids (mirrors open-pryv.io/components/errors)
16
- * @property {pryv.cmc} cmc - Cross-account Messaging & Consent helpers (slug + stream-id builders + parsers)
17
16
  */
18
17
  module.exports = {
19
18
  Service: require('./Service'),
@@ -25,6 +24,5 @@ module.exports = {
25
24
  MfaRequiredError: require('./lib/MfaRequiredError'),
26
25
  StaleAccessIdError: require('./lib/StaleAccessIdError'),
27
26
  ERRORS: require('./lib/errorIds'),
28
- cmc: require('./cmc'),
29
27
  version: require('../package.json').version
30
28
  };
package/src/utils.js CHANGED
@@ -127,6 +127,76 @@ const utils = module.exports = {
127
127
  return res[1] + '://' + tokenAndAPI.token + '@' + res[2];
128
128
  },
129
129
 
130
+ /**
131
+ * Decompose a Pryv apiEndpoint into `{ token, username, host }` using
132
+ * the platform's `service.info.api` URL template to invert whichever
133
+ * username placement the platform uses (subdomain vs path-style).
134
+ *
135
+ * Pryv apiEndpoints follow one of two URL shapes (the difference is
136
+ * platform-defined, encoded in `service.info.api`):
137
+ *
138
+ * subdomain template `https://{username}.<domain>/`
139
+ * endpoint `https://<token>@<username>.<domain>/`
140
+ * path-style template `https://<host>/{username}/`
141
+ * endpoint `https://<token>@<host>/<username>/`
142
+ *
143
+ * Returns the **canonical platform host** (no `<username>.` subdomain
144
+ * prefix in subdomain mode) — the identity cross-account features
145
+ * (e.g. CMC counterparty slugs) key on, regardless of which user the
146
+ * endpoint belongs to.
147
+ *
148
+ * `token` and `username` are null when the endpoint carries no token /
149
+ * when the endpoint shape doesn't match the template; `host` is
150
+ * best-effort in that case.
151
+ *
152
+ * @memberof pryv.utils
153
+ * @param {APIEndpoint} apiEndpoint e.g. 'https://t0k3n@alice.pryv.me/'
154
+ * @param {string} serviceInfoApi e.g. 'https://{username}.pryv.me/'
155
+ * from /service/info → field `api`.
156
+ * @returns {DecomposedAPIEndpoint}
157
+ *
158
+ * @example
159
+ * const conn = new pryv.Connection(apiEndpoint);
160
+ * const info = await conn.service.info();
161
+ * const me = pryv.utils.decomposeAPIEndpoint(apiEndpoint, info.api);
162
+ * // → { token: 't0k3n', username: 'alice', host: 'pryv.me' }
163
+ */
164
+ decomposeAPIEndpoint: function (apiEndpoint, serviceInfoApi) {
165
+ const { token, endpoint } = utils.extractTokenAndAPIEndpoint(apiEndpoint);
166
+ const tplIdx = serviceInfoApi.indexOf('{username}');
167
+ if (tplIdx < 0) {
168
+ // Template doesn't carry {username} — operator-defined, can't decompose.
169
+ let host = '';
170
+ try { host = new URL(endpoint).host; } catch (_e) {}
171
+ return { token, username: null, host };
172
+ }
173
+ const tplPrefix = serviceInfoApi.slice(0, tplIdx);
174
+ const tplSuffix = serviceInfoApi.slice(tplIdx + '{username}'.length);
175
+ if (!endpoint.startsWith(tplPrefix) || !endpoint.endsWith(tplSuffix)) {
176
+ let host = '';
177
+ try { host = new URL(endpoint).host; } catch (_e) {}
178
+ return { token, username: null, host };
179
+ }
180
+ const username = endpoint.slice(tplPrefix.length, endpoint.length - tplSuffix.length);
181
+ // Disambiguate by where {username} sits in the template:
182
+ // - subdomain → prefix ends with `://` (username right after scheme)
183
+ // - path-style → prefix has more after `://` (host already in prefix)
184
+ const isSubdomainTemplate = /:\/\/$/.test(tplPrefix);
185
+ let host;
186
+ if (isSubdomainTemplate) {
187
+ // tplSuffix starts with the domain (e.g. '.pryv.me/')
188
+ host = tplSuffix.replace(/^\.+/, '').replace(/\/+$/, '');
189
+ } else {
190
+ // path-style — host is in the prefix's URL
191
+ try {
192
+ host = new URL(tplPrefix).host;
193
+ } catch (_e) {
194
+ host = '';
195
+ }
196
+ }
197
+ return { token, username, host };
198
+ },
199
+
130
200
  /**
131
201
  * Check if the browser is running on a mobile device or tablet
132
202
  * @memberof pryv.utils
@@ -251,6 +321,18 @@ utils.buildPryvApiEndpoint = utils.buildAPIEndpoint;
251
321
  * @typedef {string} APIEndpoint
252
322
  */
253
323
 
324
+ /**
325
+ * Decomposed form of an APIEndpoint, returned by `decomposeAPIEndpoint`.
326
+ * `token` and `username` are `null` when the endpoint doesn't carry a
327
+ * token / doesn't match the `service.info.api` template. `host` is the
328
+ * canonical platform host (no `<username>.` subdomain prefix in
329
+ * subdomain-style deployments).
330
+ * @typedef {Object} DecomposedAPIEndpoint
331
+ * @property {string|null} token
332
+ * @property {string|null} username
333
+ * @property {string} host
334
+ */
335
+
254
336
  /**
255
337
  * Common Meta are returned by each standard call on the API https://api.pryv.com/reference/#in-method-results
256
338
  * @typedef {Object} CommonMeta
@@ -103,4 +103,41 @@ describe('[UTLX] utils', function () {
103
103
  expect(err.data.currentSerial).to.equal(2);
104
104
  expect(err.name).to.equal('StaleAccessIdError');
105
105
  });
106
+
107
+ describe('[UTLM] decomposeAPIEndpoint', function () {
108
+ it('[UTLMA] subdomain template — host strips the {username}. prefix', function () {
109
+ const r = pryv.utils.decomposeAPIEndpoint(
110
+ 'https://t0k3n@alice.pryv.me/',
111
+ 'https://{username}.pryv.me/'
112
+ );
113
+ expect(r).to.eql({ token: 't0k3n', username: 'alice', host: 'pryv.me' });
114
+ });
115
+
116
+ it('[UTLMB] path-style template — host is the literal hostname[:port]', function () {
117
+ const r = pryv.utils.decomposeAPIEndpoint(
118
+ 'http://t0k3n@127.0.0.1:3000/alice/',
119
+ 'http://127.0.0.1:3000/{username}/'
120
+ );
121
+ expect(r).to.eql({ token: 't0k3n', username: 'alice', host: '127.0.0.1:3000' });
122
+ });
123
+
124
+ it('[UTLMC] returns username:null when the endpoint doesn\'t match the template', function () {
125
+ const r = pryv.utils.decomposeAPIEndpoint(
126
+ 'https://t0k3n@somewhere.else.com/',
127
+ 'https://{username}.pryv.me/'
128
+ );
129
+ expect(r.username).to.equal(null);
130
+ expect(r.token).to.equal('t0k3n');
131
+ });
132
+
133
+ it('[UTLMD] returns token:null when endpoint carries no token', function () {
134
+ const r = pryv.utils.decomposeAPIEndpoint(
135
+ 'https://alice.pryv.me/',
136
+ 'https://{username}.pryv.me/'
137
+ );
138
+ expect(r.token).to.equal(null);
139
+ expect(r.username).to.equal('alice');
140
+ expect(r.host).to.equal('pryv.me');
141
+ });
142
+ });
106
143
  });
package/src/cmc.js DELETED
@@ -1,348 +0,0 @@
1
- /**
2
- * @license
3
- * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
- */
5
-
6
- /**
7
- * CMC (Cross-account Messaging & Consent) client-side helpers.
8
- *
9
- * Mirrors the server plugin's slug + stream-id helpers so app code can
10
- * build stream-ids deterministically without depending on the server's
11
- * private modules. See server-side `components/cmc/src/{slug,constants}.ts`.
12
- *
13
- * Stream-id model:
14
- * :_cmc: reserved root
15
- * :_cmc:inbox one-shot lifecycle (cross-app)
16
- * :_cmc:apps:<app-code>:[<path>:]chats:<counterparty-slug>
17
- * :_cmc:apps:<app-code>:[<path>:]collectors:<counterparty-slug>
18
- *
19
- * `counterparty-slug` = `<username>--<host-slug>` where host-slug is the
20
- * host with `.` replaced by `-`.
21
- *
22
- * @memberof pryv
23
- * @namespace pryv.cmc
24
- */
25
-
26
- // --- Constants ---
27
- // All :_cmc:* identifiers compose from NS. If the namespace is ever
28
- // rebranded (e.g. to ':_xchg:' or similar), changing NS alone updates
29
- // every constant + every helper that builds a stream-id.
30
- const NS = ':_cmc:';
31
- const NS_INBOX = NS + 'inbox';
32
- const NS_APPS = NS + 'apps';
33
- const NS_INTERNAL = NS + '_internal';
34
- const NS_INTERNAL_RETRIES = NS_INTERNAL + ':retries';
35
-
36
- const ET_REQUEST = 'consent/request-cmc';
37
- const ET_ACCEPT = 'consent/accept-cmc';
38
- const ET_REFUSE = 'consent/refuse-cmc';
39
- const ET_REVOKE = 'consent/revoke-cmc';
40
- const ET_CHAT = 'message/chat-cmc';
41
- const ET_SYSTEM_ALERT = 'notification/alert-cmc';
42
- const ET_SYSTEM_ACK = 'notification/ack-cmc';
43
- const ET_SYSTEM_SCOPE_REQUEST = 'consent/scope-request-cmc';
44
- const ET_SYSTEM_SCOPE_UPDATE = 'consent/scope-update-cmc';
45
-
46
- const EVENT_TYPES_LIFECYCLE = [ET_REQUEST, ET_ACCEPT, ET_REFUSE, ET_REVOKE];
47
- const EVENT_TYPES_CHAT = [ET_CHAT];
48
- const EVENT_TYPES_SYSTEM = [
49
- ET_SYSTEM_ALERT,
50
- ET_SYSTEM_ACK,
51
- ET_SYSTEM_SCOPE_REQUEST,
52
- ET_SYSTEM_SCOPE_UPDATE
53
- ];
54
-
55
- // --- Slug helpers ---
56
-
57
- const SEPARATOR = '--';
58
- const SLUG_PIECE_RE = /^[a-z0-9-]+$/;
59
-
60
- function assertNonEmpty (label, value) {
61
- if (typeof value !== 'string' || value.length === 0) {
62
- throw new Error('cmc-slug: ' + label + ' must be a non-empty string');
63
- }
64
- return value;
65
- }
66
-
67
- function assertSlugPiece (label, value) {
68
- if (!SLUG_PIECE_RE.test(value)) {
69
- throw new Error(
70
- 'cmc-slug: ' + label + ' "' + value + '" must match ' + SLUG_PIECE_RE.toString()
71
- );
72
- }
73
- if (value.includes(SEPARATOR)) {
74
- throw new Error(
75
- 'cmc-slug: ' + label + ' "' + value + '" must not contain the double-hyphen separator'
76
- );
77
- }
78
- }
79
-
80
- /**
81
- * Slugify a host: lowercase and replace `.` with `-`.
82
- */
83
- function slugifyHost (host) {
84
- assertNonEmpty('host', host);
85
- // Strip trailing port (`:3000`) — port doesn't affect cross-account
86
- // identity. Two users on the same hostname are the same platform
87
- // regardless of which port their api endpoint listens on. Mirrors
88
- // the server-side helper in open-pryv.io components/cmc/src/slug.ts.
89
- const hostNoPort = host.replace(/:\d+$/, '');
90
- return hostNoPort.toLowerCase().replace(/\./g, '-');
91
- }
92
-
93
- /**
94
- * Build a counterparty slug `<username>--<host-slug>`.
95
- *
96
- * @param {Object} params
97
- * @param {string} params.username
98
- * @param {string} params.host - full hostname (e.g. 'pryv.me')
99
- * @returns {string}
100
- */
101
- function counterpartySlug (params) {
102
- const username = assertNonEmpty('username', params.username).toLowerCase();
103
- assertSlugPiece('username', username);
104
- const hostSlug = slugifyHost(params.host);
105
- assertSlugPiece('host-slug', hostSlug);
106
- return username + SEPARATOR + hostSlug;
107
- }
108
-
109
- /**
110
- * Parse a counterparty slug back to its pieces. Note: the host-slug is
111
- * lossy — `pryv-me` could come from `pryv.me` or `pryv-me`. Store the
112
- * canonical host alongside the slug if you need it.
113
- *
114
- * @param {string} slug
115
- * @returns {{ username: string, hostSlug: string }}
116
- */
117
- function parseCounterpartySlug (slug) {
118
- assertNonEmpty('slug', slug);
119
- const pieces = slug.split(SEPARATOR);
120
- if (pieces.length !== 2) {
121
- throw new Error(
122
- 'cmc-slug: counterparty slug "' + slug + '" must have exactly 2 ' +
123
- 'double-hyphen-separated pieces, got ' + pieces.length
124
- );
125
- }
126
- for (const piece of pieces) {
127
- assertSlugPiece('slug piece', piece);
128
- }
129
- return { username: pieces[0], hostSlug: pieces[1] };
130
- }
131
-
132
- // --- Stream-id builders ---
133
-
134
- /** `<scopeStreamId>:chats` */
135
- function chatsParentUnder (scopeStreamId) {
136
- return scopeStreamId + ':chats';
137
- }
138
-
139
- /** `<scopeStreamId>:chats:<counterparty-slug>` */
140
- function chatStreamUnder (scopeStreamId, slug) {
141
- return scopeStreamId + ':chats:' + slug;
142
- }
143
-
144
- /** `<scopeStreamId>:collectors` */
145
- function collectorsParentUnder (scopeStreamId) {
146
- return scopeStreamId + ':collectors';
147
- }
148
-
149
- /** `<scopeStreamId>:collectors:<counterparty-slug>` */
150
- function collectorStreamUnder (scopeStreamId, slug) {
151
- return scopeStreamId + ':collectors:' + slug;
152
- }
153
-
154
- /**
155
- * Build the app-scope root `:_cmc:apps:<app-code>`.
156
- */
157
- function appScope (appCode) {
158
- assertNonEmpty('appCode', appCode);
159
- return NS_APPS + ':' + appCode;
160
- }
161
-
162
- // --- Classification predicates ---
163
-
164
- /** Does this stream-id live under the :_cmc: namespace? */
165
- function isCmcStreamId (streamId) {
166
- return streamId === ':_cmc' || streamId.startsWith(NS);
167
- }
168
-
169
- const APP_NESTED_PLUGIN_RE = /:_cmc:apps:[^:]+(?::[^:]+)*:(chats|collectors)(?::|$)/;
170
-
171
- /** True if this id is at or beneath chats/collectors under :_cmc:apps:*. */
172
- function isAppNestedPluginStream (streamId) {
173
- return APP_NESTED_PLUGIN_RE.test(streamId);
174
- }
175
-
176
- /**
177
- * Extract the app-code segment from `:_cmc:apps:<app-code>[:...]`.
178
- * Returns null for ids that aren't under `:_cmc:apps:`.
179
- */
180
- function getAppCode (streamId) {
181
- if (!streamId.startsWith(NS_APPS + ':')) return null;
182
- const rest = streamId.substring(NS_APPS.length + 1);
183
- const colonIdx = rest.indexOf(':');
184
- return colonIdx === -1 ? rest : rest.substring(0, colonIdx);
185
- }
186
-
187
- const CHAT_STREAM_ID_RE = /^(:_cmc:apps:[^:]+(?::[^:]+)*):chats:([a-z0-9-]+--[a-z0-9-]+)$/;
188
- const COLLECTOR_STREAM_ID_RE = /^(:_cmc:apps:[^:]+(?::[^:]+)*):collectors:([a-z0-9-]+--[a-z0-9-]+)$/;
189
-
190
- /**
191
- * Parse a chat trigger stream-id into its components.
192
- * Returns null on shape mismatch.
193
- */
194
- function parseChatStreamId (streamId) {
195
- if (typeof streamId !== 'string') return null;
196
- const m = streamId.match(CHAT_STREAM_ID_RE);
197
- if (m == null) return null;
198
- let counterparty;
199
- try {
200
- counterparty = parseCounterpartySlug(m[2]);
201
- } catch (_e) {
202
- return null;
203
- }
204
- return {
205
- appCode: getAppCode(m[1]),
206
- scopeStreamId: m[1],
207
- counterpartySlug: m[2],
208
- counterparty
209
- };
210
- }
211
-
212
- /**
213
- * Parse a collectors trigger stream-id into its components.
214
- * Returns null on shape mismatch.
215
- */
216
- function parseCollectorStreamId (streamId) {
217
- if (typeof streamId !== 'string') return null;
218
- const m = streamId.match(COLLECTOR_STREAM_ID_RE);
219
- if (m == null) return null;
220
- let counterparty;
221
- try {
222
- counterparty = parseCounterpartySlug(m[2]);
223
- } catch (_e) {
224
- return null;
225
- }
226
- return {
227
- appCode: getAppCode(m[1]),
228
- scopeStreamId: m[1],
229
- counterpartySlug: m[2],
230
- counterparty
231
- };
232
- }
233
-
234
- // --- Actor helpers (apiEndpoint → { token, username, host }) ---
235
-
236
- /**
237
- * Extract `{ token, username, host }` from a Pryv apiEndpoint, given
238
- * the platform's `service.info.api` URL template.
239
- *
240
- * Pryv apiEndpoints follow one of two URL shapes (the difference is
241
- * platform-defined, encoded in `service.info.api`):
242
- *
243
- * subdomain template `https://{username}.<domain>/`
244
- * endpoint `https://<token>@<username>.<domain>/`
245
- * path-style template `https://<host>/{username}/`
246
- * endpoint `https://<token>@<host>/<username>/`
247
- *
248
- * This helper inverts whichever template the platform serves, returning
249
- * the **canonical host** (no `<username>.` subdomain prefix in subdomain
250
- * mode) — that's the host CMC uses for cross-account identity (slugs,
251
- * counterparty matching).
252
- *
253
- * @param {string} apiEndpoint e.g. 'https://t0k3n@alice.pryv.me/'
254
- * @param {string} serviceInfoApi e.g. 'https://{username}.pryv.me/'
255
- * from /service/info → field `api`.
256
- * @returns {{ token: string|null, username: string|null, host: string }}
257
- *
258
- * @example
259
- * const conn = new pryv.Connection(apiEndpoint);
260
- * const info = await conn.service.info();
261
- * const me = pryv.cmc.extractActor(apiEndpoint, info.api);
262
- * // → { token: 't0k3n', username: 'alice', host: 'pryv.me' }
263
- */
264
- function extractActor (apiEndpoint, serviceInfoApi) {
265
- // Avoid circular require: utils is the consumer, cmc the provider.
266
- // We pull at call-time so cmc.js stays loadable without requiring
267
- // utils early.
268
- const utils = require('./utils');
269
- const { token, endpoint } = utils.extractTokenAndAPIEndpoint(apiEndpoint);
270
- // Match the api template's variable position to find the username.
271
- // Both endpoint + template are guaranteed to have a trailing slash.
272
- const tplIdx = serviceInfoApi.indexOf('{username}');
273
- if (tplIdx < 0) {
274
- // Template doesn't carry {username} — operator-defined, can't decompose.
275
- let host = '';
276
- try { host = new URL(endpoint).host; } catch (_e) {}
277
- return { token, username: null, host };
278
- }
279
- const tplPrefix = serviceInfoApi.slice(0, tplIdx);
280
- const tplSuffix = serviceInfoApi.slice(tplIdx + '{username}'.length);
281
- if (!endpoint.startsWith(tplPrefix) || !endpoint.endsWith(tplSuffix)) {
282
- let host = '';
283
- try { host = new URL(endpoint).host; } catch (_e) {}
284
- return { token, username: null, host };
285
- }
286
- const username = endpoint.slice(tplPrefix.length, endpoint.length - tplSuffix.length);
287
- // For subdomain templates, host is `<username>.<domain>` — but the
288
- // canonical CMC host is the bare `<domain>` (the platform identity,
289
- // not per-user). For path-style, host is just the literal host.
290
- // Disambiguate by where {username} sits in the template:
291
- // - subdomain → prefix ends with `://` (username right after scheme)
292
- // - path-style → prefix has more after `://` (host already in prefix)
293
- const isSubdomainTemplate = /:\/\/$/.test(tplPrefix);
294
- let host;
295
- if (isSubdomainTemplate) {
296
- // tplSuffix starts with the domain (e.g. '.pryv.me/')
297
- host = tplSuffix.replace(/^\.+/, '').replace(/\/+$/, '');
298
- } else {
299
- // path-style — host is in the prefix's URL
300
- try {
301
- host = new URL(tplPrefix).host;
302
- } catch (_e) {
303
- host = '';
304
- }
305
- }
306
- return { token, username, host };
307
- }
308
-
309
- module.exports = {
310
- // namespace constants
311
- NS,
312
- NS_INBOX,
313
- NS_APPS,
314
- NS_INTERNAL,
315
- NS_INTERNAL_RETRIES,
316
- // actor helper
317
- extractActor,
318
- // event types
319
- ET_REQUEST,
320
- ET_ACCEPT,
321
- ET_REFUSE,
322
- ET_REVOKE,
323
- ET_CHAT,
324
- ET_SYSTEM_ALERT,
325
- ET_SYSTEM_ACK,
326
- ET_SYSTEM_SCOPE_REQUEST,
327
- ET_SYSTEM_SCOPE_UPDATE,
328
- EVENT_TYPES_LIFECYCLE,
329
- EVENT_TYPES_CHAT,
330
- EVENT_TYPES_SYSTEM,
331
- // slug helpers
332
- SEPARATOR,
333
- slugifyHost,
334
- counterpartySlug,
335
- parseCounterpartySlug,
336
- // stream-id builders
337
- appScope,
338
- chatsParentUnder,
339
- chatStreamUnder,
340
- collectorsParentUnder,
341
- collectorStreamUnder,
342
- // classification + parsing
343
- isCmcStreamId,
344
- isAppNestedPluginStream,
345
- getAppCode,
346
- parseChatStreamId,
347
- parseCollectorStreamId
348
- };
package/test/cmc.test.js DELETED
@@ -1,172 +0,0 @@
1
- /**
2
- * @license
3
- * [BSD-3-Clause](https://github.com/pryv/lib-js/blob/master/LICENSE)
4
- */
5
- /* global describe, it, expect */
6
-
7
- const cmc = require('../src/cmc');
8
-
9
- describe('[CMCX] pryv.cmc client helpers', function () {
10
- describe('[CMCXC] constants', function () {
11
- it('[CMCXCA] exposes namespace + event-type constants', function () {
12
- expect(cmc.NS).to.equal(':_cmc:');
13
- expect(cmc.NS_INBOX).to.equal(':_cmc:inbox');
14
- expect(cmc.NS_APPS).to.equal(':_cmc:apps');
15
- expect(cmc.NS_INTERNAL).to.equal(':_cmc:_internal');
16
- expect(cmc.NS_INTERNAL_RETRIES).to.equal(':_cmc:_internal:retries');
17
- expect(cmc.ET_REQUEST).to.equal('consent/request-cmc');
18
- expect(cmc.ET_ACCEPT).to.equal('consent/accept-cmc');
19
- expect(cmc.ET_CHAT).to.equal('message/chat-cmc');
20
- expect(cmc.ET_SYSTEM_ALERT).to.equal('notification/alert-cmc');
21
- expect(cmc.EVENT_TYPES_LIFECYCLE).to.include('consent/request-cmc');
22
- expect(cmc.EVENT_TYPES_SYSTEM).to.include('consent/scope-update-cmc');
23
- });
24
- });
25
-
26
- describe('[CMCXS] slug helpers', function () {
27
- it('[CMCXSA] slugifyHost lowercases + replaces dots with hyphens', function () {
28
- expect(cmc.slugifyHost('PRYV.me')).to.equal('pryv-me');
29
- expect(cmc.slugifyHost('a.b.c.example.org')).to.equal('a-b-c-example-org');
30
- });
31
-
32
- it('[CMCXSB] counterpartySlug joins username + host-slug with --', function () {
33
- expect(cmc.counterpartySlug({ username: 'alice', host: 'pryv.me' }))
34
- .to.equal('alice--pryv-me');
35
- expect(cmc.counterpartySlug({ username: 'provider-a', host: 'example.com' }))
36
- .to.equal('provider-a--example-com');
37
- });
38
-
39
- it('[CMCXSC] counterpartySlug rejects invalid pieces', function () {
40
- expect(() => cmc.counterpartySlug({ username: 'has space', host: 'a.b' })).to.throw();
41
- expect(() => cmc.counterpartySlug({ username: '', host: 'a.b' })).to.throw();
42
- expect(() => cmc.counterpartySlug({ username: 'a--b', host: 'a.b' })).to.throw();
43
- });
44
-
45
- it('[CMCXSD] parseCounterpartySlug round-trips the slug', function () {
46
- const s = cmc.counterpartySlug({ username: 'alice', host: 'pryv.me' });
47
- const parsed = cmc.parseCounterpartySlug(s);
48
- expect(parsed).to.deep.equal({ username: 'alice', hostSlug: 'pryv-me' });
49
- });
50
-
51
- it('[CMCXSE] parseCounterpartySlug rejects malformed input', function () {
52
- expect(() => cmc.parseCounterpartySlug('no-separator')).to.throw();
53
- expect(() => cmc.parseCounterpartySlug('a--b--c')).to.throw();
54
- expect(() => cmc.parseCounterpartySlug('')).to.throw();
55
- });
56
- });
57
-
58
- describe('[CMCXB] stream-id builders', function () {
59
- it('[CMCXBA] appScope produces :_cmc:apps:<app-code>', function () {
60
- expect(cmc.appScope('my-app')).to.equal(':_cmc:apps:my-app');
61
- });
62
-
63
- it('[CMCXBB] chats helpers build the nested path', function () {
64
- const scope = cmc.appScope('my-app');
65
- expect(cmc.chatsParentUnder(scope)).to.equal(':_cmc:apps:my-app:chats');
66
- const slug = cmc.counterpartySlug({ username: 'alice', host: 'pryv.me' });
67
- expect(cmc.chatStreamUnder(scope, slug)).to.equal(':_cmc:apps:my-app:chats:alice--pryv-me');
68
- });
69
-
70
- it('[CMCXBC] collectors helpers build the nested path', function () {
71
- const scope = ':_cmc:apps:my-app:campaign-2026';
72
- expect(cmc.collectorsParentUnder(scope)).to.equal(':_cmc:apps:my-app:campaign-2026:collectors');
73
- expect(cmc.collectorStreamUnder(scope, 'alice--pryv-me'))
74
- .to.equal(':_cmc:apps:my-app:campaign-2026:collectors:alice--pryv-me');
75
- });
76
- });
77
-
78
- describe('[CMCXP] predicates + parsers', function () {
79
- it('[CMCXPA] isCmcStreamId true for :_cmc:* + false for non-cmc', function () {
80
- expect(cmc.isCmcStreamId(':_cmc:apps:foo')).to.equal(true);
81
- expect(cmc.isCmcStreamId(':_cmc:inbox')).to.equal(true);
82
- expect(cmc.isCmcStreamId('fertility')).to.equal(false);
83
- expect(cmc.isCmcStreamId(':_system:account:email')).to.equal(false);
84
- });
85
-
86
- it('[CMCXPB] isAppNestedPluginStream true for chats/collectors leaves', function () {
87
- expect(cmc.isAppNestedPluginStream(':_cmc:apps:my-app:chats:alice--pryv-me')).to.equal(true);
88
- expect(cmc.isAppNestedPluginStream(':_cmc:apps:my-app:collectors:alice--pryv-me')).to.equal(true);
89
- expect(cmc.isAppNestedPluginStream(':_cmc:apps:my-app:study-A:chats')).to.equal(true);
90
- expect(cmc.isAppNestedPluginStream(':_cmc:apps:my-app:study-A')).to.equal(false);
91
- expect(cmc.isAppNestedPluginStream(':_cmc:apps:my-app:chats-style-data')).to.equal(false);
92
- });
93
-
94
- it('[CMCXPC] getAppCode pulls the app segment', function () {
95
- expect(cmc.getAppCode(':_cmc:apps:my-app')).to.equal('my-app');
96
- expect(cmc.getAppCode(':_cmc:apps:my-app:campaign-2026')).to.equal('my-app');
97
- expect(cmc.getAppCode(':_cmc:inbox')).to.equal(null);
98
- expect(cmc.getAppCode('fertility')).to.equal(null);
99
- });
100
-
101
- it('[CMCXPD] parseChatStreamId extracts scope + counterparty', function () {
102
- const r = cmc.parseChatStreamId(':_cmc:apps:my-app:campaign-2026:chats:alice--pryv-me');
103
- expect(r.appCode).to.equal('my-app');
104
- expect(r.scopeStreamId).to.equal(':_cmc:apps:my-app:campaign-2026');
105
- expect(r.counterpartySlug).to.equal('alice--pryv-me');
106
- expect(r.counterparty).to.deep.equal({ username: 'alice', hostSlug: 'pryv-me' });
107
- });
108
-
109
- it('[CMCXPE] parseChatStreamId returns null for non-chat ids', function () {
110
- expect(cmc.parseChatStreamId(':_cmc:inbox')).to.equal(null);
111
- expect(cmc.parseChatStreamId(':_cmc:apps:my-app:collectors:foo--bar')).to.equal(null);
112
- expect(cmc.parseChatStreamId('arbitrary-stream')).to.equal(null);
113
- });
114
-
115
- it('[CMCXPF] parseCollectorStreamId extracts scope + counterparty', function () {
116
- const r = cmc.parseCollectorStreamId(':_cmc:apps:my-app:collectors:alice--pryv-me');
117
- expect(r.appCode).to.equal('my-app');
118
- expect(r.scopeStreamId).to.equal(':_cmc:apps:my-app');
119
- expect(r.counterpartySlug).to.equal('alice--pryv-me');
120
- });
121
-
122
- it('[CMCXPG] parseCollectorStreamId returns null for non-collector ids', function () {
123
- expect(cmc.parseCollectorStreamId(':_cmc:apps:my-app:chats:foo--bar')).to.equal(null);
124
- expect(cmc.parseCollectorStreamId(':_cmc:apps:my-app:collectors:no-separator')).to.equal(null);
125
- });
126
- });
127
-
128
- describe('[CMCXI] integration with pryv module', function () {
129
- it('[CMCXIA] exposed as pryv.cmc', function () {
130
- const pryv = require('../src');
131
- expect(pryv.cmc).to.exist;
132
- expect(pryv.cmc.counterpartySlug).to.be.a('function');
133
- expect(pryv.cmc.NS).to.equal(':_cmc:');
134
- });
135
- });
136
-
137
- describe('[CMCXA] extractActor', function () {
138
- it('[CMCXAA] subdomain template — host strips the {username}. prefix', function () {
139
- const r = cmc.extractActor(
140
- 'https://t0k3n@alice.pryv.me/',
141
- 'https://{username}.pryv.me/'
142
- );
143
- expect(r).to.deep.equal({ token: 't0k3n', username: 'alice', host: 'pryv.me' });
144
- });
145
-
146
- it('[CMCXAB] path-style template — host is the literal hostname[:port]', function () {
147
- const r = cmc.extractActor(
148
- 'http://t0k3n@127.0.0.1:3000/alice/',
149
- 'http://127.0.0.1:3000/{username}/'
150
- );
151
- expect(r).to.deep.equal({ token: 't0k3n', username: 'alice', host: '127.0.0.1:3000' });
152
- });
153
-
154
- it('[CMCXAC] returns username:null when the endpoint doesn\'t match the template', function () {
155
- const r = cmc.extractActor(
156
- 'https://t0k3n@somewhere.else.com/',
157
- 'https://{username}.pryv.me/'
158
- );
159
- expect(r.username).to.equal(null);
160
- });
161
-
162
- it('[CMCXAD] returns token:null when endpoint carries no token', function () {
163
- const r = cmc.extractActor(
164
- 'https://alice.pryv.me/',
165
- 'https://{username}.pryv.me/'
166
- );
167
- expect(r.token).to.equal(null);
168
- expect(r.username).to.equal('alice');
169
- expect(r.host).to.equal('pryv.me');
170
- });
171
- });
172
- });