@mindful-web/marko-web-identity-x 1.92.0 → 1.98.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/CLAUDE.md CHANGED
@@ -5,7 +5,8 @@ Web sites. Owns the whole authenticated-user surface: login / register / authent
5
5
  / profile / logout, content access + download gating, comments, and the server-side IdentityX API
6
6
  client. Add-ons layer on top: `marko-web-omeda-identity-x` (wraps this and adds Omeda hooks —
7
7
  how most sites install it), `marko-web-auth0-identity-x`, `marko-web-identity-x-mailchimp`. Google
8
- Sign-In (One-Tap + button) is **built in** here (see below).
8
+ Sign-In (One-Tap + button) is **built in** here (see below). The opt-in Identity Bridge sync
9
+ (`integrations/identity-bridge-sync`, see below) reports member events to Mindful Identity Bridge.
9
10
 
10
11
  ## Config model — the `IdentityXConfiguration` instance (`config.js`)
11
12
 
@@ -423,7 +424,10 @@ No coverage is lost: every field the fleet-wide impact analysis measured is a se
423
424
  `buildContentSource()`. **Definitions come from server config, never the body**, so a client
424
425
  cannot widen what gets written by posting extra rows. Drops an empty select rather than sending
425
426
  `optionIds: []`, which the writer reads as *unset* — the one way this feature could destroy data
426
- despite the fill-blanks-only rule.
427
+ despite the fill-blanks-only rule. `buildContentSource()` sends `appContextId` (falling back to
428
+ the app id) as `websiteChannelId`, which is what Insights scopes submissions by. watt, cox and
429
+ industrial-media share one app across their sites with a context per site; sending the app id
430
+ there gave WATT's first 44 submissions a channel no site maps to, and Insights showed 0.
427
431
  - `routes/progressive-profile.js` — `POST /__idx/progressive-profile` (+ `/dismiss`). A thin proxy;
428
432
  it owns *trust*, not writes.
429
433
  - `components/progressive-profile-form.marko` (`<marko-web-identity-x-progressive-profile-form>`)
@@ -695,6 +699,52 @@ Submitted **answer values are never tracked** anywhere — `fieldKeys` and the m
695
699
  still exclude `custom-boolean` — no longer for the renderer bug, but because the gate forces
696
700
  required columns and a required checkbox can only submit `true` (see the boot-check table).
697
701
 
702
+ ## Identity Bridge sync (`integrations/identity-bridge-sync`) — added 2026-10
703
+
704
+ Opt-in hook installer that reports member events to Mindful Identity Bridge's `POST /:tenantKey/sync`
705
+ (`apis/identity-bridge` in mindful-reporting, tracked in parameter1/mindful-reporting#304). Identity
706
+ Bridge decides which third-party providers act on each event (eMagazines today). **This package holds
707
+ no provider logic, config or keys** — it replaced mindful-web#363, which ran eMagazines from here.
708
+
709
+ ```js
710
+ const installIdentityBridgeSync = require('@mindful-web/marko-web-identity-x/integrations/identity-bridge-sync');
711
+ installIdentityBridgeSync(idxConfig, { websiteId: process.env.SITE_ID });
712
+ ```
713
+
714
+ | Hook | Event sent |
715
+ |---|---|
716
+ | `onAuthenticationSuccess` | `{ event: 'authenticated', websiteId, user, loginSource }` |
717
+ | `onUserProfileUpdate` | `{ event: 'profile-updated', websiteId, user }` |
718
+ | `onChangeEmailSuccess` | `{ event: 'email-changed', websiteId, user /* new email */, oldEmail }` |
719
+
720
+ `user` is `{ id, email, givenName, familyName, street, city, regionCode, postalCode }` from the
721
+ IdentityX app user.
722
+
723
+ - **Send values as they are.** The server normalizes `null`/`''`/whitespace to unset and lowercases
724
+ emails. Don't clean them here.
725
+ - **Don't filter here.** Every login is sent, `change-email` included (`change-email-confirm.js`
726
+ fires `onAuthenticationSuccess` right after `onChangeEmailSuccess`). The server skips those logins
727
+ and applies the website's `loginSources`. Adding a filter here would split one rule across two repos.
728
+ - **All three hooks use `shouldAwait: false`.** The server runs synchronously and returns 502 when a
729
+ provider call fails, and the client throws on any non-2xx. So failures reach `onHookError` without
730
+ blocking the IdentityX request.
731
+ - **The client comes from the request.** The hooks call `req.mindful.identityBridge.sync()`, which
732
+ the `@mindful-web/mindful` service middleware sets when given `identityBridge`
733
+ (`packages/mindful/identity-bridge.js`). Each hook throws a clear error when the client is missing.
734
+ ⚠️ The site must install that middleware **before** its IdentityX routes:
735
+
736
+ ```js
737
+ identityBridge: {
738
+ apiKey: process.env.MINDFUL_IDENTITY_BRIDGE_API_KEY,
739
+ tenantKey: 'allured', // the Mindful tenant key, NOT the site's TENANT_KEY (e.g. `indm_multi`)
740
+ },
741
+ ```
742
+
743
+ - `websiteId` is the CMS website id (`SITE_ID`). The server returns 400 if that id doesn't belong to
744
+ the tenant. A website with no providers configured gets `{ data: [] }`.
745
+ - Specs: `test/integrations/identity-bridge-sync.spec.js` (hooks) and `packages/mindful/test/` (client
746
+ + middleware).
747
+
698
748
  ## Fleet / rollout
699
749
 
700
750
  Not tracked in-repo. The Google-auth architecture change + fleet rollout plan lives outside the repo
package/README.md CHANGED
@@ -264,3 +264,40 @@ The Google account id (JWT `sub`) is persisted as a supplementary idx external i
264
264
  `{ provider: 'google', tenant: 'oauth', type: 'account' }`; email stays the primary key.
265
265
  Non-fatal linkage errors report through the config's `onHookError`.
266
266
  | `identity-x-comment-stream-errored` | `{ label, message: '...', ...additionalEventData }`
267
+
268
+ ## Identity Bridge sync
269
+
270
+ Opt-in, and **off until installed.** It reports logins, profile updates and email changes to Mindful
271
+ Identity Bridge, which passes them on to the third-party providers configured for the website (e.g.
272
+ eMagazines). Every decision is made server side. This package sends the events and nothing else.
273
+
274
+ 1. Give the Mindful service middleware an Identity Bridge client. Install it **before** the IdentityX
275
+ routes, because the hooks read `req.mindful.identityBridge`:
276
+
277
+ ```js
278
+ const mindfulService = require('@mindful-web/mindful/marko-web/middleware/service');
279
+
280
+ app.use(mindfulService({
281
+ namespace: '<tenant>/<site>',
282
+ identityBridge: {
283
+ apiKey: process.env.MINDFUL_IDENTITY_BRIDGE_API_KEY,
284
+ tenantKey: 'allured', // the Mindful tenant key, not the site's TENANT_KEY
285
+ },
286
+ }));
287
+ ```
288
+
289
+ `identityBridge` also takes `timeout` (ms, default 15000) and `url` (default
290
+ `https://apis.internal.mindfulcms.com/identity-bridge`). Without it, `req.mindful.identityBridge`
291
+ is `null`.
292
+
293
+ 2. Install the hooks on the IdentityX config:
294
+
295
+ ```js
296
+ const installIdentityBridgeSync = require('@mindful-web/marko-web-identity-x/integrations/identity-bridge-sync');
297
+
298
+ installIdentityBridgeSync(idxConfig, { websiteId: process.env.SITE_ID });
299
+ ```
300
+
301
+ The `onAuthenticationSuccess`, `onUserProfileUpdate` and `onChangeEmailSuccess` hooks are not
302
+ awaited. A failed sync, including a missing client on the request, is reported through the config's
303
+ `onHookError`.
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/access.marko",
6
6
  marko_component = require("./access.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -53,7 +53,7 @@ marko_template._ = marko_renderer(render, {
53
53
  }, marko_component);
54
54
 
55
55
  marko_template.meta = {
56
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/access.marko",
56
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/access.marko",
57
57
  component: "./access.marko",
58
58
  tags: [
59
59
  "@mindful-web/marko-core/components/resolve.marko"
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/comment-stream.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/comment-stream.marko",
6
6
  marko_component = require("./comment-stream.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -91,7 +91,7 @@ marko_template._ = marko_renderer(render, {
91
91
  }, marko_component);
92
92
 
93
93
  marko_template.meta = {
94
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/comment-stream.marko",
94
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/comment-stream.marko",
95
95
  component: "./comment-stream.marko",
96
96
  tags: [
97
97
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/context.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/context.marko",
6
6
  marko_component = require("./context.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
@@ -49,7 +49,7 @@ marko_template._ = marko_renderer(render, {
49
49
  }, marko_component);
50
50
 
51
51
  marko_template.meta = {
52
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/context.marko",
52
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/context.marko",
53
53
  component: "./context.marko",
54
54
  tags: [
55
55
  "@mindful-web/marko-core/components/resolve.marko"
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-access.marko",
6
6
  marko_component = require("./form-access.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
@@ -179,7 +179,7 @@ marko_template._ = marko_renderer(render, {
179
179
  }, marko_component);
180
180
 
181
181
  marko_template.meta = {
182
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-access.marko",
182
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-access.marko",
183
183
  component: "./form-access.marko",
184
184
  tags: [
185
185
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-authenticate.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-authenticate.marko",
6
6
  marko_component = require("./form-authenticate.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -81,7 +81,7 @@ marko_template._ = marko_renderer(render, {
81
81
  }, marko_component);
82
82
 
83
83
  marko_template.meta = {
84
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-authenticate.marko",
84
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-authenticate.marko",
85
85
  component: "./form-authenticate.marko",
86
86
  tags: [
87
87
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-change-email.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-change-email.marko",
6
6
  marko_component = require("./form-change-email.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -59,7 +59,7 @@ marko_template._ = marko_renderer(render, {
59
59
  }, marko_component);
60
60
 
61
61
  marko_template.meta = {
62
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-change-email.marko",
62
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-change-email.marko",
63
63
  component: "./form-change-email.marko",
64
64
  tags: [
65
65
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-login.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-login.marko",
6
6
  marko_component = require("./form-login.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -92,7 +92,7 @@ marko_template._ = marko_renderer(render, {
92
92
  }, marko_component);
93
93
 
94
94
  marko_template.meta = {
95
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-login.marko",
95
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-login.marko",
96
96
  component: "./form-login.marko",
97
97
  tags: [
98
98
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-logout.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-logout.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
8
8
  defaultValue = module_defaultValue.default || module_defaultValue,
@@ -29,7 +29,7 @@ marko_template._ = marko_renderer(render, {
29
29
  });
30
30
 
31
31
  marko_template.meta = {
32
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-logout.marko",
32
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-logout.marko",
33
33
  tags: [
34
34
  "@mindful-web/marko-web/components/browser-component.marko"
35
35
  ]
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-profile.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-profile.marko",
6
6
  marko_component = require("./form-profile.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -84,7 +84,7 @@ marko_template._ = marko_renderer(render, {
84
84
  }, marko_component);
85
85
 
86
86
  marko_template.meta = {
87
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-profile.marko",
87
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-profile.marko",
88
88
  component: "./form-profile.marko",
89
89
  tags: [
90
90
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/form-register.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/form-register.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  marko_assign = require("marko/dist/runtime/helpers/assign"),
8
8
  marko_web_identity_x_form_login_template = require("./form-login.marko"),
@@ -25,7 +25,7 @@ marko_template._ = marko_renderer(render, {
25
25
  });
26
26
 
27
27
  marko_template.meta = {
28
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/form-register.marko",
28
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/form-register.marko",
29
29
  tags: [
30
30
  "./form-login.marko"
31
31
  ]
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/google-init.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/google-init.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  marko_attr = require("marko/dist/runtime/html/helpers/attr"),
8
8
  marko_str = require("marko/dist/runtime/helpers/to-string");
@@ -239,5 +239,5 @@ marko_template._ = marko_renderer(render, {
239
239
  });
240
240
 
241
241
  marko_template.meta = {
242
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/google-init.marko"
242
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/google-init.marko"
243
243
  };
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/google-sign-in-button.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/google-sign-in-button.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
8
8
  marko_escapeXml = helpers_escape_xml.x,
@@ -40,5 +40,5 @@ marko_template._ = marko_renderer(render, {
40
40
  });
41
41
 
42
42
  marko_template.meta = {
43
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/google-sign-in-button.marko"
43
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/google-sign-in-button.marko"
44
44
  };
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/identify.marko",
6
6
  marko_component = require("./identify.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -50,7 +50,7 @@ marko_template._ = marko_renderer(render, {
50
50
  }, marko_component);
51
51
 
52
52
  marko_template.meta = {
53
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/identify.marko",
53
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/identify.marko",
54
54
  component: "./identify.marko",
55
55
  tags: [
56
56
  "@mindful-web/marko-web-gtm/components/push.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/non-auth-identify.marko",
6
6
  marko_component = require("./non-auth-identify.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
@@ -45,7 +45,7 @@ marko_template._ = marko_renderer(render, {
45
45
  }, marko_component);
46
46
 
47
47
  marko_template.meta = {
48
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/non-auth-identify.marko",
49
49
  component: "./non-auth-identify.marko",
50
50
  tags: [
51
51
  "@mindful-web/marko-core/components/resolve.marko"
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/progressive-profile-form.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/progressive-profile-form.marko",
6
6
  marko_component = require("./progressive-profile-form.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
@@ -137,7 +137,7 @@ marko_template._ = marko_renderer(render, {
137
137
  }, marko_component);
138
138
 
139
139
  marko_template.meta = {
140
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/progressive-profile-form.marko",
140
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/progressive-profile-form.marko",
141
141
  component: "./progressive-profile-form.marko",
142
142
  tags: [
143
143
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.92.0/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.98.0/components/subscribe.marko",
6
6
  marko_component = require("./subscribe.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -73,7 +73,7 @@ marko_template._ = marko_renderer(render, {
73
73
  }, marko_component);
74
74
 
75
75
  marko_template.meta = {
76
- id: "/@mindful-web/marko-web-identity-x$1.92.0/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.98.0/components/subscribe.marko",
77
77
  component: "./subscribe.marko",
78
78
  tags: [
79
79
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Maps an IdentityX app user to the Identity Bridge `user`. Values are sent as they are: the server
3
+ * normalizes `null`/empty fields and lowercases the email.
4
+ *
5
+ * @param {object} user The IdentityX app user
6
+ */
7
+ const toSyncUser = (user) => ({
8
+ id: user.id,
9
+ email: user.email,
10
+ givenName: user.givenName,
11
+ familyName: user.familyName,
12
+ street: user.street,
13
+ city: user.city,
14
+ regionCode: user.regionCode,
15
+ postalCode: user.postalCode,
16
+ });
17
+
18
+ /**
19
+ * @param {import('express').Request} req
20
+ */
21
+ const getClient = (req) => {
22
+ const client = req && req.mindful && req.mindful.identityBridge;
23
+ if (!client) throw new Error('No Identity Bridge client on the request. Pass `identityBridge` to the Mindful service middleware.');
24
+ return client;
25
+ };
26
+
27
+ /**
28
+ * Reports IdentityX member events to Mindful Identity Bridge (`POST /:tenantKey/sync`), which
29
+ * decides which third-party providers act on them. Nothing provider-specific lives here: login
30
+ * source filtering, `change-email` logins and subscription checks are all handled server side.
31
+ *
32
+ * Requires the `@mindful-web/mindful` service middleware, configured with `identityBridge`, to run
33
+ * before the IdentityX routes so `req.mindful.identityBridge` is set.
34
+ *
35
+ * None of the hooks are awaited, so a slow or failed sync never blocks the IdentityX request.
36
+ * Failures are reported through the config's `onHookError`.
37
+ *
38
+ * @param {import("../../config")} idxConfig
39
+ * @param {object} options
40
+ * @param {string} options.websiteId The site's CMS website ID (`SITE_ID`)
41
+ */
42
+ module.exports = (idxConfig, { websiteId } = {}) => {
43
+ if (!idxConfig || !idxConfig.addHook) throw new Error('Unable to install the Identity Bridge sync hooks.');
44
+ if (!websiteId) throw new Error('A websiteId is required to install the Identity Bridge sync hooks.');
45
+
46
+ idxConfig.addHook({
47
+ name: 'onAuthenticationSuccess',
48
+ shouldAwait: false,
49
+ fn: async ({ req, user, loginSource }) => getClient(req).sync({
50
+ event: 'authenticated',
51
+ websiteId,
52
+ loginSource,
53
+ user: toSyncUser(user),
54
+ }),
55
+ });
56
+
57
+ idxConfig.addHook({
58
+ name: 'onUserProfileUpdate',
59
+ shouldAwait: false,
60
+ fn: async ({ req, user }) => getClient(req).sync({
61
+ event: 'profile-updated',
62
+ websiteId,
63
+ user: toSyncUser(user),
64
+ }),
65
+ });
66
+
67
+ idxConfig.addHook({
68
+ name: 'onChangeEmailSuccess',
69
+ shouldAwait: false,
70
+ fn: async ({ req, user, oldEmail }) => getClient(req).sync({
71
+ event: 'email-changed',
72
+ websiteId,
73
+ oldEmail,
74
+ user: toSyncUser(user),
75
+ }),
76
+ });
77
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindful-web/marko-web-identity-x",
3
- "version": "1.92.0",
3
+ "version": "1.98.0",
4
4
  "description": "Marko Wrapper for IdentityX",
5
5
  "repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-identity-x",
6
6
  "author": "Josh Worden <josh@parameter1.com>",
@@ -43,5 +43,5 @@
43
43
  "chai": "^4.3.7",
44
44
  "mocha": "^6.2.3"
45
45
  },
46
- "gitHead": "1a2c523f3c3cfa5ff4a18d3900a31a7260a39b9e"
46
+ "gitHead": "606c85315b404db6d84a08cb0a0ae7a059ae1fa4"
47
47
  }
@@ -0,0 +1,162 @@
1
+ const { describe, it } = require('mocha');
2
+ const { expect } = require('chai');
3
+ const install = require('../../integrations/identity-bridge-sync');
4
+
5
+ /**
6
+ * Installs the hooks against a stub config and returns them by name.
7
+ */
8
+ const installHooks = (options = { websiteId: 'website-1' }) => {
9
+ const hooks = [];
10
+ install({ addHook: (hook) => hooks.push(hook) }, options);
11
+ return hooks.reduce((o, hook) => ({ ...o, [hook.name]: hook }), {});
12
+ };
13
+
14
+ /**
15
+ * A request carrying a stubbed Identity Bridge client that records every event.
16
+ */
17
+ const buildReq = () => {
18
+ const events = [];
19
+ const identityBridge = {
20
+ sync: async (event) => {
21
+ events.push(event);
22
+ return [];
23
+ },
24
+ };
25
+ return { req: { mindful: { identityBridge } }, events };
26
+ };
27
+
28
+ const rejectionOf = async (promise) => {
29
+ try {
30
+ await promise;
31
+ } catch (e) {
32
+ return e;
33
+ }
34
+ return null;
35
+ };
36
+
37
+ const user = {
38
+ id: 'user-1',
39
+ email: 'jane@example.com',
40
+ givenName: 'Jane',
41
+ familyName: null,
42
+ street: '',
43
+ city: 'Chicago',
44
+ regionCode: 'US-IL',
45
+ postalCode: '60601',
46
+ verified: true,
47
+ customAttributes: { foo: 'bar' },
48
+ };
49
+
50
+ // Sent as IdentityX has it, `null` and `''` included; the server normalizes them.
51
+ const syncUser = {
52
+ id: 'user-1',
53
+ email: 'jane@example.com',
54
+ givenName: 'Jane',
55
+ familyName: null,
56
+ street: '',
57
+ city: 'Chicago',
58
+ regionCode: 'US-IL',
59
+ postalCode: '60601',
60
+ };
61
+
62
+ describe('integrations/identity-bridge-sync', () => {
63
+ describe('install', () => {
64
+ it('registers non-awaited login, profile update and email change hooks', () => {
65
+ const hooks = [];
66
+ install({ addHook: (hook) => hooks.push(hook) }, { websiteId: 'website-1' });
67
+ expect(hooks.map(({ name }) => name)).to.deep.equal([
68
+ 'onAuthenticationSuccess',
69
+ 'onUserProfileUpdate',
70
+ 'onChangeEmailSuccess',
71
+ ]);
72
+ hooks.forEach((hook) => expect(hook.shouldAwait).to.equal(false));
73
+ });
74
+
75
+ it('throws without an IdentityX config', () => {
76
+ expect(() => install()).to.throw('Unable to install the Identity Bridge sync hooks.');
77
+ });
78
+
79
+ it('throws without a website id', () => {
80
+ expect(() => installHooks({})).to.throw('A websiteId is required to install the Identity Bridge sync hooks.');
81
+ });
82
+ });
83
+
84
+ describe('onAuthenticationSuccess', () => {
85
+ it('sends an authenticated event with the login source', async () => {
86
+ const { req, events } = buildReq();
87
+ await installHooks().onAuthenticationSuccess.fn({ req, user, loginSource: 'contentGate' });
88
+ expect(events).to.deep.equal([{
89
+ event: 'authenticated',
90
+ websiteId: 'website-1',
91
+ loginSource: 'contentGate',
92
+ user: syncUser,
93
+ }]);
94
+ });
95
+
96
+ it('sends every login source, including change-email, for the server to filter', async () => {
97
+ const { req, events } = buildReq();
98
+ await installHooks().onAuthenticationSuccess.fn({ req, user, loginSource: 'change-email' });
99
+ expect(events).to.have.lengthOf(1);
100
+ expect(events[0].loginSource).to.equal('change-email');
101
+ });
102
+ });
103
+
104
+ describe('onUserProfileUpdate', () => {
105
+ it('sends a profile-updated event', async () => {
106
+ const { req, events } = buildReq();
107
+ await installHooks().onUserProfileUpdate.fn({ req, user, actionSource: 'profile' });
108
+ expect(events).to.deep.equal([{
109
+ event: 'profile-updated',
110
+ websiteId: 'website-1',
111
+ user: syncUser,
112
+ }]);
113
+ });
114
+ });
115
+
116
+ describe('onChangeEmailSuccess', () => {
117
+ it('sends an email-changed event with the new email on the user', async () => {
118
+ const { req, events } = buildReq();
119
+ await installHooks().onChangeEmailSuccess.fn({
120
+ req,
121
+ user: { ...user, email: 'jane.new@example.com' },
122
+ oldEmail: 'jane@example.com',
123
+ });
124
+ expect(events).to.deep.equal([{
125
+ event: 'email-changed',
126
+ websiteId: 'website-1',
127
+ oldEmail: 'jane@example.com',
128
+ user: { ...syncUser, email: 'jane.new@example.com' },
129
+ }]);
130
+ });
131
+ });
132
+
133
+ describe('without a client on the request', () => {
134
+ ['onAuthenticationSuccess', 'onUserProfileUpdate', 'onChangeEmailSuccess'].forEach((name) => {
135
+ it(`${name} throws a clear error`, async () => {
136
+ const hooks = installHooks();
137
+ const reqs = [{}, { mindful: {} }, { mindful: { identityBridge: null } }];
138
+ const errors = await Promise.all(reqs.map((req) => rejectionOf(hooks[name].fn({
139
+ req,
140
+ user,
141
+ oldEmail: 'jane@example.com',
142
+ }))));
143
+ errors.forEach((error) => {
144
+ expect(error).to.be.an('error');
145
+ expect(error.message).to.equal('No Identity Bridge client on the request. Pass `identityBridge` to the Mindful service middleware.');
146
+ });
147
+ });
148
+ });
149
+ });
150
+
151
+ it('passes on a failed sync so onHookError reports it', async () => {
152
+ const req = {
153
+ mindful: {
154
+ identityBridge: {
155
+ sync: async () => { throw new Error('Identity Bridge sync failed (502): emagazines'); },
156
+ },
157
+ },
158
+ };
159
+ const error = await rejectionOf(installHooks().onUserProfileUpdate.fn({ req, user }));
160
+ expect(error.message).to.equal('Identity Bridge sync failed (502): emagazines');
161
+ });
162
+ });
@@ -0,0 +1,29 @@
1
+ const { describe, it } = require('mocha');
2
+ const { expect } = require('chai');
3
+ const { buildContentSource } = require('../../utils/build-progressive-profile-payload');
4
+
5
+ const config = ({ appId, appContextId } = {}) => ({
6
+ getAppId: () => appId,
7
+ get: (path) => (path === 'appContextId' ? appContextId : undefined),
8
+ });
9
+
10
+ describe('utils/build-progressive-profile-payload buildContentSource', () => {
11
+ it('uses the app id as the website channel on a single-app tenant', () => {
12
+ const source = buildContentSource({ config: config({ appId: 'app-1' }), contentId: '15836353' });
13
+ expect(source.websiteChannelId).to.equal('app-1');
14
+ });
15
+
16
+ it('uses the app context id when the site has one', () => {
17
+ // A tenant whose sites share one app (watt): the shared app id is no site's channel.
18
+ const source = buildContentSource({
19
+ config: config({ appId: 'shared-app', appContextId: 'site-context' }),
20
+ contentId: '15836353',
21
+ });
22
+ expect(source.websiteChannelId).to.equal('site-context');
23
+ });
24
+
25
+ it('parses the content id as an integer', () => {
26
+ const source = buildContentSource({ config: config({ appId: 'app-1' }), contentId: '15836353' });
27
+ expect(source.integerId).to.equal(15836353);
28
+ });
29
+ });
@@ -99,8 +99,10 @@ module.exports = ({ body, definitions = [] } = {}) => {
99
99
  * digits (true of all 442 content routes across the 18 website repos). A mismatch therefore
100
100
  * surfaces as a `UserInputError` rather than silently recording a wrong `postId`.
101
101
  *
102
- * `websiteChannelId` is the idx app id: the two are the same value, confirmed against the tenancy
103
- * `websites` map and the audience `attributes.websiteChannelIds` data.
102
+ * `websiteChannelId` is the site's audience website channel. On a single-app tenant that is the idx
103
+ * app id. A tenant whose sites share one app (watt, cox, industrial-media) gives each site its own
104
+ * app context instead, and the context id is the channel: sending the shared app id there stamps
105
+ * every site's submissions with a channel no site maps to, so Insights counts zero.
104
106
  *
105
107
  * @param {object} params
106
108
  * @param {import('../config')} params.config
@@ -112,6 +114,6 @@ module.exports.buildContentSource = ({ config, contentId, actionSource } = {}) =
112
114
  // `website.tenant`.
113
115
  legacyTenantKey: process.env.TENANT_KEY,
114
116
  integerId: Number.parseInt(contentId, 10),
115
- websiteChannelId: config.getAppId(),
117
+ websiteChannelId: config.get('appContextId') || config.getAppId(),
116
118
  ...(actionSource && { actionSource }),
117
119
  });