@mindful-web/marko-web-identity-x 1.79.4 → 1.79.7

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
@@ -119,6 +119,35 @@ login/authenticate pages get the button with **no per-site edit**. For the site-
119
119
  document layout, gated on `!req.identityX.token`. The button tag can also be placed in a content
120
120
  meter / gate.
121
121
 
122
+ **Telemetry.** Two seams. Neither needs site configuration:
123
+ - `config.addRequestAttributes` — called with a flat object as the route progresses
124
+ (`idxGoogleStep`, `idxUserId`, `idxCreatedNewUser`, `idxRequiresUserInput`, `idxGoogleSurface`,
125
+ `idxGoogleMissingFieldsBehavior`). **Defaults to `utils/add-custom-attributes.js`**, which tags
126
+ the current New Relic transaction, so the attributes ride on the `Transaction` *and* any
127
+ `TransactionError` it produces — a 500 says which step failed instead of being a bare GraphQL
128
+ message. That default resolves the agent **only if the app already loaded it** (checks
129
+ `require.cache`, never requires it cold — requiring `newrelic` would *start* an agent on sites
130
+ that don't use it), so it arrives fleet-wide on a dep bump and is inert everywhere else. Pass a
131
+ function to override, or a no-op to disable. Deliberately **not** `noticeError`: that records a
132
+ second error next to the one the error handler already captures and doubles the counts.
133
+ - `config.onHookError` — non-fatal errors, now called as `(error, customAttributes)` to match
134
+ `newrelic.noticeError`. The Google `sub` linkage failures report through it with their step and
135
+ user id, which keeps them distinguishable from fatal errors on the same route (they arrive on a
136
+ 200).
137
+
138
+ Client-side, `fail()` fires a p1events `Identity / Authenticate Failed` event mirroring the success
139
+ event. There is no New Relic Browser agent on the monorail sites, so without it a client-side
140
+ failure reaches no backend at all. The originating surface (`button` | `prompt`, from GIS
141
+ `select_by`) is posted to the route as `body.surface` — both surfaces otherwise share a route, a
142
+ `loginSource` and an impersonation method, so nothing server-side could tell them apart.
143
+
144
+ **Failure handling.** All client-side failures funnel through `fail()` in `google-init.marko`: it
145
+ always `console.error`s, writes to `#google-sign-in-error` when that element is on the page (it is
146
+ rendered by `google-sign-in-button.marko`, so it exists only where the button does), and otherwise
147
+ — the site-wide floating prompt case — redirects to the login endpoint with `?redirectTo=`. Never
148
+ let a failure be a silent no-op: GIS closes the prompt on its own, so the user reads "nothing
149
+ happened" as success and only discovers they're anonymous on the next page load.
150
+
122
151
  **CSS.** `.google-sign-in` styles ship in `marko-web-theme-monorail/scss/components/_identity-x.scss`
123
152
  (with the rest of the auth CSS) — no separate import.
124
153
 
@@ -99,6 +99,18 @@ export default {
99
99
  this.error = null;
100
100
  this.isLoading = true;
101
101
  const { currentDisplayName, body, stream } = this;
102
+ const {
103
+ description,
104
+ identifier,
105
+ title,
106
+ url,
107
+ } = stream;
108
+ const validStreamFields = {
109
+ description,
110
+ identifier,
111
+ title,
112
+ url,
113
+ };
102
114
  try {
103
115
  const additionalEventData = {
104
116
  ...(this.contentEntity && { ent: this.contentEntity }),
@@ -106,7 +118,7 @@ export default {
106
118
  const res = await post('/comment', {
107
119
  displayName: currentDisplayName,
108
120
  body,
109
- stream,
121
+ stream: validStreamFields,
110
122
  additionalEventData,
111
123
  });
112
124
  const data = await res.json();
@@ -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.79.4/components/access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/access.marko",
56
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/comment-stream.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/comment-stream.marko",
94
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/context.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/context.marko",
52
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-access.marko",
182
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-authenticate.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-authenticate.marko",
84
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-change-email.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-change-email.marko",
62
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-login.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-login.marko",
95
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-logout.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-logout.marko",
32
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-profile.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-profile.marko",
87
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-register.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/form-register.marko",
28
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/components/form-register.marko",
29
29
  tags: [
30
30
  "./form-login.marko"
31
31
  ]
@@ -2,6 +2,7 @@ $ const { req } = out.global;
2
2
  $ const config = req.identityX && req.identityX.config;
3
3
  $ const { clientId, autoPrompt } = config ? config.getAsObject('googleAuth') : {};
4
4
  $ const callbackName = 'handleGoogleOneTapCredential';
5
+ $ const loginUrl = config ? config.getEndpointFor('login') : '/user/login';
5
6
  $ const initScript = `
6
7
  (function() {
7
8
  window.${callbackName} = function(response) {
@@ -12,24 +13,94 @@ $ const initScript = `
12
13
  // GIS 'select_by' distinguishes the surface: a 'btn*' value means the
13
14
  // rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
14
15
  // prompt. Used to label the p1events authentication event accordingly.
15
- var eventLabel = String(response.select_by || '').indexOf('btn') === 0
16
- ? 'Google Sign In'
17
- : 'Google One Tap';
16
+ var isButton = String(response.select_by || '').indexOf('btn') === 0;
17
+ var eventLabel = isButton ? 'Google Sign In' : 'Google One Tap';
18
+
19
+ // Every failure path routes through here. The site-wide floating One-Tap
20
+ // prompt renders outside any form, so there is no '#google-sign-in-error'
21
+ // element on the page — previously that meant a failed sign-in did nothing
22
+ // at all: GIS closed the prompt, the user assumed success, and only found
23
+ // out they were still anonymous on the next page load. Always log, and
24
+ // fall back to the login page when there's nowhere to show the message.
25
+ var authenticated = false;
26
+ function fail(message, detail) {
27
+ if (window.console && window.console.error) {
28
+ window.console.error('[identity-x google-auth] ' + message, detail);
29
+ }
30
+ // There is no New Relic Browser agent on these sites, so a client-side
31
+ // failure reaches no backend at all unless we send one. p1events is
32
+ // already on every page -- mirror the success event so failed sign-ins
33
+ // are countable next to successful ones rather than invisible.
34
+ if (window.p1events) {
35
+ window.p1events('track', {
36
+ category: 'Identity',
37
+ action: 'Authenticate Failed',
38
+ label: eventLabel,
39
+ props: {
40
+ actionSource: 'google-one-tap',
41
+ surface: isButton ? 'button' : 'prompt',
42
+ message: message,
43
+ status: detail && detail.status ? detail.status : null,
44
+ },
45
+ });
46
+ }
47
+ // The session cookie is already set — a throw in the post-success
48
+ // analytics/redirect code must not bounce an authenticated user to login.
49
+ if (authenticated) return;
50
+ if (errorEl) {
51
+ errorEl.textContent = message;
52
+ errorEl.style.display = 'block';
53
+ return;
54
+ }
55
+ var returnTo = window.location.pathname + window.location.search;
56
+ window.location.href = ${JSON.stringify(loginUrl)} + '?redirectTo=' + encodeURIComponent(returnTo);
57
+ }
18
58
 
19
59
  fetch('/__idx/google', {
20
60
  method: 'POST',
21
61
  headers: { 'Content-Type': 'application/json' },
22
- body: JSON.stringify({ credential: response.credential, redirectTo: redirectTo }),
62
+ body: JSON.stringify({
63
+ credential: response.credential,
64
+ redirectTo: redirectTo,
65
+ // Lets the server tag its telemetry with the originating surface --
66
+ // both post here and are otherwise indistinguishable server-side.
67
+ surface: isButton ? 'button' : 'prompt',
68
+ }),
23
69
  })
24
- .then(function(res) { return res.json(); })
25
- .then(function(data) {
70
+ // Parse defensively: a 5xx from the idx router returns
71
+ // { error, status, message } (no 'ok'), and an upstream proxy can return
72
+ // HTML that would otherwise blow up res.json() into the generic catch.
73
+ .then(function(res) {
74
+ return res.text().then(function(text) {
75
+ var parsed = {};
76
+ try { parsed = JSON.parse(text); } catch (e) { /* non-JSON response */ }
77
+ return { status: res.status, redirected: res.redirected, url: res.url, data: parsed };
78
+ });
79
+ })
80
+ .then(function(result) {
81
+ var data = result.data;
26
82
  if (!data.ok) {
27
- if (errorEl) {
28
- errorEl.textContent = data.error || 'Google sign-in failed. Please try again.';
29
- errorEl.style.display = 'block';
83
+ // A 301/302 on this POST (host canonicalization — apex -> www, or
84
+ // http -> https) is re-issued by the browser as a GET with the body
85
+ // dropped. The idx router only registers POST /__idx/google, so the
86
+ // redirected request falls through to a 404 and the sign-in is lost.
87
+ // Call that out by name rather than reporting a generic failure.
88
+ if (result.redirected) {
89
+ fail(
90
+ 'Google sign-in failed: the request was redirected to ' + result.url
91
+ + ', which turns it into a GET and drops the credential. '
92
+ + 'Serve this page from the canonical host.',
93
+ { status: result.status }
94
+ );
95
+ return;
30
96
  }
97
+ fail(
98
+ data.error || data.message || 'Google sign-in failed. Please try again.',
99
+ { status: result.status }
100
+ );
31
101
  return;
32
102
  }
103
+ authenticated = true;
33
104
  if (window.p1events) {
34
105
  // De-anonymize the session to the app user, then record the
35
106
  // authentication — mirrors the identity-x conversion events.
@@ -65,11 +136,8 @@ $ const initScript = `
65
136
  window.location.href = data.redirectTo || window.location.href;
66
137
  }
67
138
  })
68
- .catch(function() {
69
- if (errorEl) {
70
- errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
71
- errorEl.style.display = 'block';
72
- }
139
+ .catch(function(e) {
140
+ fail('An error occurred during Google sign-in. Please try again.', e);
73
141
  });
74
142
  };
75
143
  })();
@@ -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.79.4/components/google-init.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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");
@@ -18,6 +18,8 @@ function render(input, out, __component, component, state) {
18
18
 
19
19
  const callbackName = 'handleGoogleOneTapCredential';
20
20
 
21
+ const loginUrl = config ? config.getEndpointFor('login') : '/user/login';
22
+
21
23
  const initScript = `
22
24
  (function() {
23
25
  window.${callbackName} = function(response) {
@@ -28,24 +30,94 @@ function render(input, out, __component, component, state) {
28
30
  // GIS 'select_by' distinguishes the surface: a 'btn*' value means the
29
31
  // rendered "Sign in with Google" button, 'user*'/'auto' means the One-Tap
30
32
  // prompt. Used to label the p1events authentication event accordingly.
31
- var eventLabel = String(response.select_by || '').indexOf('btn') === 0
32
- ? 'Google Sign In'
33
- : 'Google One Tap';
33
+ var isButton = String(response.select_by || '').indexOf('btn') === 0;
34
+ var eventLabel = isButton ? 'Google Sign In' : 'Google One Tap';
35
+
36
+ // Every failure path routes through here. The site-wide floating One-Tap
37
+ // prompt renders outside any form, so there is no '#google-sign-in-error'
38
+ // element on the page — previously that meant a failed sign-in did nothing
39
+ // at all: GIS closed the prompt, the user assumed success, and only found
40
+ // out they were still anonymous on the next page load. Always log, and
41
+ // fall back to the login page when there's nowhere to show the message.
42
+ var authenticated = false;
43
+ function fail(message, detail) {
44
+ if (window.console && window.console.error) {
45
+ window.console.error('[identity-x google-auth] ' + message, detail);
46
+ }
47
+ // There is no New Relic Browser agent on these sites, so a client-side
48
+ // failure reaches no backend at all unless we send one. p1events is
49
+ // already on every page -- mirror the success event so failed sign-ins
50
+ // are countable next to successful ones rather than invisible.
51
+ if (window.p1events) {
52
+ window.p1events('track', {
53
+ category: 'Identity',
54
+ action: 'Authenticate Failed',
55
+ label: eventLabel,
56
+ props: {
57
+ actionSource: 'google-one-tap',
58
+ surface: isButton ? 'button' : 'prompt',
59
+ message: message,
60
+ status: detail && detail.status ? detail.status : null,
61
+ },
62
+ });
63
+ }
64
+ // The session cookie is already set — a throw in the post-success
65
+ // analytics/redirect code must not bounce an authenticated user to login.
66
+ if (authenticated) return;
67
+ if (errorEl) {
68
+ errorEl.textContent = message;
69
+ errorEl.style.display = 'block';
70
+ return;
71
+ }
72
+ var returnTo = window.location.pathname + window.location.search;
73
+ window.location.href = ${JSON.stringify(loginUrl)} + '?redirectTo=' + encodeURIComponent(returnTo);
74
+ }
34
75
 
35
76
  fetch('/__idx/google', {
36
77
  method: 'POST',
37
78
  headers: { 'Content-Type': 'application/json' },
38
- body: JSON.stringify({ credential: response.credential, redirectTo: redirectTo }),
79
+ body: JSON.stringify({
80
+ credential: response.credential,
81
+ redirectTo: redirectTo,
82
+ // Lets the server tag its telemetry with the originating surface --
83
+ // both post here and are otherwise indistinguishable server-side.
84
+ surface: isButton ? 'button' : 'prompt',
85
+ }),
39
86
  })
40
- .then(function(res) { return res.json(); })
41
- .then(function(data) {
87
+ // Parse defensively: a 5xx from the idx router returns
88
+ // { error, status, message } (no 'ok'), and an upstream proxy can return
89
+ // HTML that would otherwise blow up res.json() into the generic catch.
90
+ .then(function(res) {
91
+ return res.text().then(function(text) {
92
+ var parsed = {};
93
+ try { parsed = JSON.parse(text); } catch (e) { /* non-JSON response */ }
94
+ return { status: res.status, redirected: res.redirected, url: res.url, data: parsed };
95
+ });
96
+ })
97
+ .then(function(result) {
98
+ var data = result.data;
42
99
  if (!data.ok) {
43
- if (errorEl) {
44
- errorEl.textContent = data.error || 'Google sign-in failed. Please try again.';
45
- errorEl.style.display = 'block';
100
+ // A 301/302 on this POST (host canonicalization — apex -> www, or
101
+ // http -> https) is re-issued by the browser as a GET with the body
102
+ // dropped. The idx router only registers POST /__idx/google, so the
103
+ // redirected request falls through to a 404 and the sign-in is lost.
104
+ // Call that out by name rather than reporting a generic failure.
105
+ if (result.redirected) {
106
+ fail(
107
+ 'Google sign-in failed: the request was redirected to ' + result.url
108
+ + ', which turns it into a GET and drops the credential. '
109
+ + 'Serve this page from the canonical host.',
110
+ { status: result.status }
111
+ );
112
+ return;
46
113
  }
114
+ fail(
115
+ data.error || data.message || 'Google sign-in failed. Please try again.',
116
+ { status: result.status }
117
+ );
47
118
  return;
48
119
  }
120
+ authenticated = true;
49
121
  if (window.p1events) {
50
122
  // De-anonymize the session to the app user, then record the
51
123
  // authentication — mirrors the identity-x conversion events.
@@ -81,11 +153,8 @@ function render(input, out, __component, component, state) {
81
153
  window.location.href = data.redirectTo || window.location.href;
82
154
  }
83
155
  })
84
- .catch(function() {
85
- if (errorEl) {
86
- errorEl.textContent = 'An error occurred during Google sign-in. Please try again.';
87
- errorEl.style.display = 'block';
88
- }
156
+ .catch(function(e) {
157
+ fail('An error occurred during Google sign-in. Please try again.', e);
89
158
  });
90
159
  };
91
160
  })();
@@ -108,5 +177,5 @@ marko_template._ = marko_renderer(render, {
108
177
  });
109
178
 
110
179
  marko_template.meta = {
111
- id: "/@mindful-web/marko-web-identity-x$1.79.4/components/google-init.marko"
180
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/components/google-init.marko"
112
181
  };
@@ -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.79.4/components/google-sign-in-button.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/google-sign-in-button.marko"
43
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/identify.marko",
53
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.79.7/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.79.4/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.79.7/components/subscribe.marko",
77
77
  component: "./subscribe.marko",
78
78
  tags: [
79
79
  "@mindful-web/marko-web/components/browser-component.marko",
package/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const { get, getAsArray, getAsObject } = require('@mindful-web/object-path');
2
2
  const { isFunction: isFn } = require('@mindful-web/utils');
3
3
  const validHooks = require('./hooks');
4
+ const defaultAddRequestAttributes = require('./utils/add-custom-attributes');
4
5
 
5
6
  const { log } = console;
6
7
 
@@ -15,7 +16,17 @@ class IdentityXConfiguration {
15
16
  * @param {string[]} [options.requiredCreateFieldRows] Required fields, when creating a new user.
16
17
  * @param {string[]} [options.activeCustomFieldIds] Limit displayed custom fields, if present.
17
18
  * @param {string[]} [options.hiddenFields] The fields to hide from the profile.
18
- * @param {function} [options.onHookError]
19
+ * @param {function} [options.onHookError] Reporter for non-fatal errors. Called as
20
+ * `(error, customAttributes)` — matches `newrelic.noticeError`.
21
+ * @param {function} [options.addRequestAttributes] Sink for per-request telemetry,
22
+ * called with a flat object of primitives. **Defaults to tagging the current New
23
+ * Relic transaction** when the app has already loaded the agent (every marko-web
24
+ * site does, at boot) — so the attributes ride on both the `Transaction` and any
25
+ * `TransactionError` it produces, and a 500 arrives already saying which step
26
+ * failed and for whom. No site configuration required; inert without New Relic.
27
+ * Deliberately not `noticeError` — that would record a second error alongside the
28
+ * one the error handler already captures, and double the counts. Pass your own
29
+ * function to override, or a no-op to disable.
19
30
  * @param {object} [options.googleAuth] Google Sign-In config. When `clientId` is
20
31
  * set, the One-Tap prompt / "Sign in with Google" button render in the auth
21
32
  * templates and `POST /__idx/google` accepts credentials. Dormant otherwise.
@@ -40,6 +51,7 @@ class IdentityXConfiguration {
40
51
  gtmUserFields = {},
41
52
  googleAuth = {},
42
53
  onHookError,
54
+ addRequestAttributes,
43
55
  ...rest
44
56
  } = {}) {
45
57
  if (!appId) throw new Error('Unable to configure IdentityX: no Application ID was provided.');
@@ -58,13 +70,18 @@ class IdentityXConfiguration {
58
70
  booleanQuestionsLabel,
59
71
  gtmUserFields,
60
72
  googleAuth,
61
- onHookError: (e) => {
73
+ // Extra args are forwarded so callers can pass structured context —
74
+ // `newrelic.noticeError(error, customAttributes)` takes it natively.
75
+ onHookError: (e, ...args) => {
62
76
  if (process.env.NODE_ENV === 'development') {
63
- log('ERROR IN IDENTITY-X HOOK', e);
77
+ log('ERROR IN IDENTITY-X HOOK', e, ...args);
64
78
  if (e.networkError) log('Network Error', get(e, 'networkError.result.errors.0'));
65
79
  }
66
- if (isFn(onHookError)) onHookError(e);
80
+ if (isFn(onHookError)) onHookError(e, ...args);
67
81
  },
82
+ addRequestAttributes: isFn(addRequestAttributes)
83
+ ? addRequestAttributes
84
+ : defaultAddRequestAttributes,
68
85
  ...rest,
69
86
  };
70
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindful-web/marko-web-identity-x",
3
- "version": "1.79.4",
3
+ "version": "1.79.7",
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>",
@@ -38,5 +38,5 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "3a97ebfa3e5e9078d1a31a6060d410c60b1220d6"
41
+ "gitHead": "cd09a3d725bbcb04b8a1ffae70164981bbe1305e"
42
42
  }
package/routes/google.js CHANGED
@@ -14,6 +14,16 @@ const UPDATE_OWN_APP_USER = gql`
14
14
 
15
15
  const isEmpty = (v) => v == null || v === '';
16
16
 
17
+ // Cache one OAuth2Client per client id. `verifyIdToken` fetches (and caches on
18
+ // the *instance*) Google's public signing certs, so constructing a client per
19
+ // request re-fetched them on every sign-in — turning a transient network blip
20
+ // or rate-limit at Google into a failed sign-in.
21
+ const oauthClients = new Map();
22
+ const getOAuthClient = (clientId) => {
23
+ if (!oauthClients.has(clientId)) oauthClients.set(clientId, new OAuth2Client(clientId));
24
+ return oauthClients.get(clientId);
25
+ };
26
+
17
27
  const LOGIN_SOURCE = 'google-one-tap';
18
28
 
19
29
  // Namespace under which the Google account id (the JWT `sub`) is stored as an
@@ -50,7 +60,21 @@ module.exports = asyncRoute(async (req, res) => {
50
60
  const { config } = identityX;
51
61
  const onError = config.get('onHookError') || defaultOnError;
52
62
 
63
+ // Tag the transaction as the route progresses. Whatever step is recorded when
64
+ // the request dies is the step that failed, so an error arrives already
65
+ // localized instead of being a bare GraphQL message to reverse-engineer.
66
+ const addAttrs = config.get('addRequestAttributes') || (() => {});
67
+ const setStep = (step) => addAttrs({ idxGoogleStep: step });
68
+
53
69
  const { clientId, missingFieldsBehavior = 'profile-gate' } = config.getAsObject('googleAuth');
70
+ // `surface` is the GIS `select_by` bucket ('button' | 'prompt'), sent by the
71
+ // client. Both surfaces share a route, a loginSource and an impersonation
72
+ // method, so without it there is no way to tell from the server whether the
73
+ // rendered button works independently of the One-Tap prompt.
74
+ addAttrs({
75
+ idxGoogleSurface: body.surface === 'button' ? 'button' : 'prompt',
76
+ idxGoogleMissingFieldsBehavior: missingFieldsBehavior,
77
+ });
54
78
 
55
79
  if (!clientId) {
56
80
  res.status(400).json({ ok: false, error: 'Google Sign-In is not configured for this site.' });
@@ -62,7 +86,8 @@ module.exports = asyncRoute(async (req, res) => {
62
86
  }
63
87
 
64
88
  // Verify the Google JWT and extract user info.
65
- const oauthClient = new OAuth2Client(clientId);
89
+ setStep('verify-credential');
90
+ const oauthClient = getOAuthClient(clientId);
66
91
  const ticket = await oauthClient.verifyIdToken({ idToken: credential, audience: clientId });
67
92
  const googlePayload = ticket.getPayload();
68
93
  const {
@@ -75,11 +100,14 @@ module.exports = asyncRoute(async (req, res) => {
75
100
  if (!email) throw new Error('Google credential did not include an email address.');
76
101
 
77
102
  // Find or create the identity-x user.
103
+ setStep('load-app-user');
78
104
  let appUser = await identityX.loadAppUserByEmail(email);
79
105
  const createdNewUser = !appUser;
80
106
  if (!appUser) {
107
+ setStep('create-app-user');
81
108
  appUser = await identityX.createAppUser({ email });
82
109
  }
110
+ addAttrs({ idxUserId: appUser.id, idxCreatedNewUser: createdNewUser });
83
111
 
84
112
  // Persist the Google account id (the JWT `sub`) as an idx external id so the
85
113
  // user is linked to its Google identity. Email stays the primary key; this is
@@ -92,6 +120,7 @@ module.exports = asyncRoute(async (req, res) => {
92
120
  && extId.identifier.value === googleSub
93
121
  ));
94
122
  if (!alreadyLinked) {
123
+ setStep('link-google-sub');
95
124
  try {
96
125
  // If this sub is already attached to a different user (e.g. the Google
97
126
  // account's email changed and a new user was created under it), report
@@ -104,7 +133,7 @@ module.exports = asyncRoute(async (req, res) => {
104
133
  if (existing && existing.id !== appUser.id) {
105
134
  onError(new Error(
106
135
  `Google sub already linked to idx user ${existing.id}, not ${appUser.id}`,
107
- ));
136
+ ), { idxGoogleStep: 'link-google-sub', idxUserId: appUser.id, idxConflictUserId: existing.id });
108
137
  } else {
109
138
  await identityX.addExternalUserId({
110
139
  userId: appUser.id,
@@ -114,11 +143,14 @@ module.exports = asyncRoute(async (req, res) => {
114
143
  }
115
144
  } catch (e) {
116
145
  // Supplementary linkage — never block authentication on a failure here.
117
- onError(e);
146
+ // Reported with context so these stay distinguishable from fatal errors
147
+ // on the same route (they arrive on a 200).
148
+ onError(e, { idxGoogleStep: 'link-google-sub', idxUserId: appUser.id });
118
149
  }
119
150
  }
120
151
  }
121
152
 
153
+ setStep('impersonate-app-user');
122
154
  // Impersonate the user with verify: true — marks them as verified and returns
123
155
  // a session token, bypassing the normal email-link flow.
124
156
  const { data: impersonateData } = await identityX.client.mutate({
@@ -140,9 +172,13 @@ module.exports = asyncRoute(async (req, res) => {
140
172
 
141
173
  // Re-init the Apollo client with the user's own session token so that
142
174
  // updateOwnAppUser is authenticated as this user (not as the org admin).
143
- identityX.setToken(authToken);
175
+ // `setToken` takes the token *string* — passing the `{ id, value }` object
176
+ // produced an `AppUser [object Object]` authorization header, which failed
177
+ // every subsequent user-authenticated call (see updateOwnAppUser below).
178
+ identityX.setToken(authToken.value);
144
179
 
145
180
  // Set name fields from Google as verified data if not already present.
181
+ setStep('update-own-app-user');
146
182
  const needsNameUpdate = !appUser.givenName || !appUser.familyName;
147
183
  if (needsNameUpdate && (givenName || familyName)) {
148
184
  const nameInput = {};
@@ -155,6 +191,7 @@ module.exports = asyncRoute(async (req, res) => {
155
191
  }
156
192
 
157
193
  // Load the refreshed user so hooks and field checks have up-to-date data.
194
+ setStep('load-refreshed-user');
158
195
  const user = await identityX.findUserById(appUser.id);
159
196
  const entity = await identityX.generateEntityId({ userId: appUser.id });
160
197
 
@@ -169,12 +206,14 @@ module.exports = asyncRoute(async (req, res) => {
169
206
  ];
170
207
  requiresUserInput = requiredFields.some((key) => isEmpty(user[key]));
171
208
  }
209
+ addAttrs({ idxRequiresUserInput: requiresUserInput });
172
210
 
173
211
  // Fire onLoginLinkSent before onAuthenticationSuccess so that hooks wired to
174
212
  // it (e.g. Omeda rapid-identify and subscription sync) run for Google users
175
213
  // the same way they do for email-link users. This also populates
176
214
  // user.externalIds with the Omeda encryptedCustomerId so the subsequent
177
215
  // onAuthenticationSuccess hook can set oly_enc_id in the response.
216
+ setStep('hooks-login-link-sent');
178
217
  await callHooksFor(identityX, 'onLoginLinkSent', {
179
218
  req,
180
219
  source: LOGIN_SOURCE,
@@ -185,6 +224,7 @@ module.exports = asyncRoute(async (req, res) => {
185
224
  // the onLoginLinkSent hook (Omeda rapid-identify).
186
225
  const userAfterLinkSent = await identityX.findUserById(appUser.id);
187
226
 
227
+ setStep('hooks-authentication-success');
188
228
  await callHooksFor(identityX, 'onAuthenticationSuccess', {
189
229
  req,
190
230
  res,
@@ -194,6 +234,7 @@ module.exports = asyncRoute(async (req, res) => {
194
234
  additionalEventData: { createdNewUser, requiresUserInput },
195
235
  });
196
236
 
237
+ setStep('complete');
197
238
  res.json({
198
239
  ok: true,
199
240
  requiresUserInput,
package/service.js CHANGED
@@ -355,8 +355,9 @@ class IdentityX {
355
355
  }
356
356
 
357
357
  /**
358
+ * Sets the active app-user token and re-inits the Apollo client with it.
358
359
  *
359
- * @param {*} param0
360
+ * @param {String} token The token *value* (not the `{ id, value }` object).
360
361
  */
361
362
  setToken(token) {
362
363
  this.token = token;
@@ -379,7 +380,11 @@ class IdentityX {
379
380
  });
380
381
  const { token } = data.impersonateAppUser;
381
382
  tokenCookie.setTo(this.res, token.value);
382
- this.setToken(token);
383
+ // `setToken` expects the token string (as the constructor supplies from the
384
+ // cookie) — passing the `{ id, value }` object here produced an
385
+ // `AppUser [object Object]` authorization header on the re-inited client,
386
+ // and left `this.token` as an object for `logoutAppUser`.
387
+ this.setToken(token.value);
383
388
  }
384
389
 
385
390
  async logoutAppUser() {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Default sink for `config.addRequestAttributes` — tags the current New Relic
3
+ * transaction, with no configuration and no New Relic dependency.
4
+ *
5
+ * The agent is used **only when the application has already loaded it**. Every
6
+ * marko-web site requires its `newrelic.js` at boot, so by the time a request
7
+ * is served the module is in `require.cache` and `require('newrelic')` returns
8
+ * that same initialized singleton. Requiring it unconditionally would instead
9
+ * *start* an agent on sites that don't use New Relic, which is why this checks
10
+ * the cache rather than just try/catching the require.
11
+ *
12
+ * Net effect: attributes appear fleet-wide on a dependency bump alone, and the
13
+ * whole thing is inert on any site without New Relic. A site can still pass its
14
+ * own `addRequestAttributes` to override.
15
+ */
16
+
17
+ let resolvedPath;
18
+ let lookupFailed = false;
19
+ let agent = null;
20
+
21
+ const getAgent = () => {
22
+ if (agent) return agent;
23
+ if (lookupFailed) return null;
24
+ if (!resolvedPath) {
25
+ try {
26
+ resolvedPath = require.resolve('newrelic');
27
+ } catch (e) {
28
+ // Not installed — nothing to report to.
29
+ lookupFailed = true;
30
+ return null;
31
+ }
32
+ }
33
+ // `newrelic` is intentionally NOT a dependency of this package: declaring it
34
+ // would install it on every monorail site, and requiring an uninitialized
35
+ // agent starts one. We only ever pick up an instance the host app already
36
+ // created, which is why the cache check above guards this require.
37
+ // eslint-disable-next-line global-require, import/no-extraneous-dependencies
38
+ if (require.cache[resolvedPath]) agent = require('newrelic');
39
+ return agent;
40
+ };
41
+
42
+ module.exports = (attributes) => {
43
+ const nr = getAgent();
44
+ if (!nr || typeof nr.addCustomAttributes !== 'function') return;
45
+ try {
46
+ nr.addCustomAttributes(attributes);
47
+ } catch (e) {
48
+ // Telemetry must never break a request.
49
+ }
50
+ };