@mindful-web/marko-web-identity-x 1.96.3 → 1.105.1

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
 
@@ -698,6 +699,52 @@ Submitted **answer values are never tracked** anywhere — `fieldKeys` and the m
698
699
  still exclude `custom-boolean` — no longer for the renderer bug, but because the gate forces
699
700
  required columns and a required checkbox can only submit `true` (see the boot-check table).
700
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
+
701
748
  ## Fleet / rollout
702
749
 
703
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.96.3/components/access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/access.marko",
56
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/comment-stream.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/comment-stream.marko",
94
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/context.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/context.marko",
52
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-access.marko",
182
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-authenticate.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-authenticate.marko",
84
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-change-email.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-change-email.marko",
62
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-login.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-login.marko",
95
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-logout.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-logout.marko",
32
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-profile.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-profile.marko",
87
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-register.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/form-register.marko",
28
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/google-init.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/google-init.marko"
242
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/google-sign-in-button.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/google-sign-in-button.marko"
43
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/identify.marko",
53
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/progressive-profile-form.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/progressive-profile-form.marko",
140
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.105.1/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.96.3",
3
+ "version": "1.105.1",
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": "3b66b74c73d0d784f2baf47a506f705012071923"
46
+ "gitHead": "e6054405dc7918b784fd8cb8b8b0042dd251f248"
47
47
  }
@@ -40,7 +40,9 @@ module.exports = asyncRoute(async (req, res) => {
40
40
  const { token: authToken, user, loginSource } = data.loginAppUser;
41
41
  const additionalEventData = {
42
42
  ...getAsObject(body, 'additionalEventData'),
43
- ...getAsObject(data, 'loginAppUer.AdditionContext'),
43
+ // The login's own event data (displayOverlay, newsletterSignupType, …), stored on the login
44
+ // token by sendLoginLink. Was read from a misspelled path, so hooks never received it.
45
+ ...getAsObject(data, 'loginAppUser.additionalContext'),
44
46
  };
45
47
 
46
48
  // call authentication hooks
@@ -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,67 @@
1
+ const { describe, it } = require('mocha');
2
+ const { expect } = require('chai');
3
+ const route = require('../../routes/authenticate');
4
+
5
+ const run = async ({ additionalContext, bodyEventData } = {}) => {
6
+ const hookArgs = [];
7
+ let json;
8
+ const identityX = {
9
+ client: {
10
+ mutate: async () => ({
11
+ data: {
12
+ loginAppUser: {
13
+ token: { id: 'tok', value: 'tok-value' },
14
+ user: { id: 'user-1' },
15
+ loginSource: 'content_meter_login',
16
+ additionalContext,
17
+ },
18
+ },
19
+ }),
20
+ },
21
+ config: {
22
+ hooks: {
23
+ onAuthenticationSuccess: [{ fn: (args) => hookArgs.push(args), shouldAwait: true }],
24
+ },
25
+ get: () => undefined,
26
+ getAppId: () => 'app-1',
27
+ },
28
+ getOrgUserApiToken: () => 'org-token',
29
+ setIdentityCookie: () => {},
30
+ generateEntityId: async () => 'entity-1',
31
+ findUserById: async (id) => ({ id }),
32
+ };
33
+ const req = { identityX, body: { token: 'login-token', additionalEventData: bodyEventData } };
34
+ const res = { cookie: () => {}, json: (value) => { json = value; } };
35
+ let error;
36
+ await route(req, res, (e) => { error = e; });
37
+ if (error) throw error;
38
+ return { hookArgs, json };
39
+ };
40
+
41
+ describe('routes/authenticate', () => {
42
+ it('passes the login token context to onAuthenticationSuccess', async () => {
43
+ // The login's own event data is what lets the hook attribute the Verify Email behavior to
44
+ // the same source as Log In (e.g. a metered-gate overlay). It was read from a misspelled
45
+ // path, so the hook never received it.
46
+ const { hookArgs, json } = await run({
47
+ additionalContext: { displayOverlay: true, actionSource: 'content_meter_login', source: 'content_meter_login' },
48
+ });
49
+ expect(hookArgs).to.have.length(1);
50
+ expect(hookArgs[0].loginSource).to.equal('content_meter_login');
51
+ expect(hookArgs[0].additionalEventData).to.include({ displayOverlay: true, actionSource: 'content_meter_login' });
52
+ expect(json.additionalEventData).to.include({ displayOverlay: true });
53
+ });
54
+
55
+ it('lets the token context win over event data posted by the authenticate page', async () => {
56
+ const { hookArgs } = await run({
57
+ additionalContext: { newsletterSignupType: 'pushdown' },
58
+ bodyEventData: { newsletterSignupType: 'footer', fromPage: true },
59
+ });
60
+ expect(hookArgs[0].additionalEventData).to.include({ newsletterSignupType: 'pushdown', fromPage: true });
61
+ });
62
+
63
+ it('tolerates a token with no stored context', async () => {
64
+ const { hookArgs } = await run({ bodyEventData: { fromPage: true } });
65
+ expect(hookArgs[0].additionalEventData).to.deep.equal({ fromPage: true });
66
+ });
67
+ });