@mindful-web/marko-web-identity-x 1.80.2 → 1.83.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
@@ -184,9 +184,26 @@ simply cannot authorize this mutation, and the fix is to stop using it — backf
184
184
  via `setUnverifiedAppUserData` (org-token, but writes *unverified* data, so it is a semantic change
185
185
  worth deciding deliberately).
186
186
 
187
- `idxGoogleStepMs` records elapsed ms at the start of each step, making the route self-profiling:
188
- `FACET idxGoogleStep SELECT max(idxGoogleStepMs)` shows where the ~12s on a *successful* sign-in
189
- goes, and a request that dies mid-flight still reports how far it got. Responds `{ ok, requiresUserInput,
187
+ **Timing comes in two parts, deliberately.** `idxGoogleStepMs` is elapsed-at-last-step and
188
+ **last-write-wins** — on a completed request it is effectively total route time, and on one that dies
189
+ mid-flight it says how far in it got. It cannot attribute time to a step: every `setStep` overwrites
190
+ it, which is why the per-step breakdown it was originally meant to provide never actually survived
191
+ (confirmed in production — the query returned one value per transaction, not a profile).
192
+
193
+ `idxMs<Step>` is the real breakdown: one attribute per step (`idxMsVerifyCredential`,
194
+ `idxMsImpersonateAppUser`, `idxMsHooksLoginLinkSent`, `idxMsHooksAuthenticationSuccess`, …), each
195
+ holding how long **that step** took, emitted when the next step begins. `complete` has no duration of
196
+ its own — it is a marker, not work. The durations sum to `idxGoogleStepMs`. About ten attributes, well
197
+ inside New Relic's per-transaction budget.
198
+
199
+ ```
200
+ SELECT average(idxMsHooksAuthenticationSuccess), average(idxMsImpersonateAppUser),
201
+ average(idxMsHooksLoginLinkSent), average(idxGoogleStepMs)
202
+ FROM Transaction WHERE name LIKE '%__idx/google%'
203
+ ```
204
+
205
+ Both hook steps sit on the blocking path and Omeda shows a ~4s tail, so this is what says whether
206
+ they own the remaining latency. Responds `{ ok, requiresUserInput,
190
207
  redirectTo, user, entity, additionalEventData, … }`; the client redirects to `/user/profile` when
191
208
  input is required.
192
209
 
package/README.md CHANGED
@@ -150,6 +150,24 @@ import IdentityX from '@mindful-web/marko-web-identity-x/browser';
150
150
  +});
151
151
  ```
152
152
 
153
+ ## Cookie lifetimes
154
+
155
+ All cookie durations in this package are expressed in **milliseconds**, because that is the unit
156
+ express's `res.cookie` `maxAge` option expects. Env vars and config keys that carry a duration are
157
+ suffixed `_MS` / `Ms` so the unit is unambiguous at the call site.
158
+
159
+ | Variable | Applies to | Default | Notes |
160
+ | - | - | - | - |
161
+ | `IDX_CONTENT_ACCESS_MAXAGE_MS` | `__idx_form_<surveyId>_<contentId>` (content access) | `86400000` (1 day) | |
162
+ | `IDX_CONTENT_DOWNLOAD_MAXAGE_MS` | `__idx_form_content-download_<contentId>` | `86400000` (1 day) | |
163
+ | `IDX_CONTENT_ACCESS_MAXAGE` | — | — | **Deprecated** alias of the `_MS` var above. |
164
+ | `IDX_CONTENT_DOWNLOAD_MAXAGE` | — | — | **Deprecated** alias of the `_MS` var above. |
165
+
166
+ The unsuffixed names are still honored, but avoid them in new deployments: `MAXAGE` reads as the HTTP
167
+ `Max-Age` directive, which is in *seconds*, so a value of `86400` intended as "one day" silently
168
+ produced an 86-second cookie. Non-numeric or non-positive values are ignored with a warning and the
169
+ default applies, rather than sending `NaN` into `res.cookie` (which throws on every affected request).
170
+
153
171
  ## Vue Event emission
154
172
 
155
173
  This package emits the following events via the EventBus/global Vue root. Each payload will be an
@@ -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.80.2/components/access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/access.marko",
56
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/comment-stream.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/comment-stream.marko",
94
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/context.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/context.marko",
52
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-access.marko",
182
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-authenticate.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-authenticate.marko",
84
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-change-email.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-change-email.marko",
62
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-login.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-login.marko",
95
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-logout.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-logout.marko",
32
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-profile.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-profile.marko",
87
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-register.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/form-register.marko",
28
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/google-init.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/google-init.marko"
242
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/google-sign-in-button.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/google-sign-in-button.marko"
43
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/identify.marko",
53
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.2/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.83.0/components/subscribe.marko",
77
77
  component: "./subscribe.marko",
78
78
  tags: [
79
79
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -1,15 +1,10 @@
1
1
  const { get, getAsObject } = require('@mindful-web/object-path');
2
-
3
- const cookieNamePrefix = '__idx_form';
4
- // default is one day
5
- const maxAge = process.env.IDX_CONTENT_ACCESS_MAXAGE
6
- ? Number(process.env.IDX_CONTENT_ACCESS_MAXAGE)
7
- : (24 * 60 * 60 * 1000);
2
+ const { accessMaxAge, buildName } = require('../utils/content-form-cookie');
8
3
 
9
4
  const contentAccessState = ({ res, content }) => {
10
5
  // Handle setting of contentAccessState Object
11
6
  const { surveyType, surveyId } = getAsObject(content, 'gating');
12
- const cookieName = `${cookieNamePrefix}_${surveyId}_${content.id}`;
7
+ const cookieName = buildName({ surveyId, contentId: content.id });
13
8
 
14
9
  const setFormDisplay = ({ req }) => {
15
10
  res.locals.contentAccessState.displayForm = !get(req, `cookies.${cookieName}`);
@@ -20,7 +15,7 @@ const contentAccessState = ({ res, content }) => {
20
15
  };
21
16
 
22
17
  if (surveyType === 'idx') {
23
- const cookie = { name: cookieName, maxAge };
18
+ const cookie = { name: cookieName, maxAge: accessMaxAge() };
24
19
  res.locals.contentAccessState.formId = surveyId;
25
20
  res.locals.contentAccessState.cookie = cookie;
26
21
  }
@@ -1,14 +1,10 @@
1
1
  const { get } = require('@mindful-web/object-path');
2
-
3
- const cookieNamePrefix = '__idx_form';
4
- const maxAge = process.env.IDX_CONTENT_DOWNLOAD_MAXAGE
5
- ? Number(process.env.IDX_CONTENT_DOWNLOAD_MAXAGE)
6
- : (24 * 60 * 60 * 1000);
2
+ const { DOWNLOAD_SURVEY_ID, buildName, downloadMaxAge } = require('../utils/content-form-cookie');
7
3
 
8
4
  const contentDownloadState = ({ res, content }) => {
9
5
  // Handle setting of contentDownloadState Object
10
- const surveyId = 'content-download';
11
- const cookieName = `${cookieNamePrefix}_${surveyId}_${content.id}`;
6
+ const surveyId = DOWNLOAD_SURVEY_ID;
7
+ const cookieName = buildName({ surveyId, contentId: content.id });
12
8
 
13
9
  const setFormDisplay = ({ req }) => {
14
10
  res.locals.contentDownloadState.displayForm = !get(req, `cookies.${cookieName}`);
@@ -17,7 +13,7 @@ const contentDownloadState = ({ res, content }) => {
17
13
  displayForm: false,
18
14
  setFormDisplay,
19
15
  };
20
- const cookie = { name: cookieName, maxAge };
16
+ const cookie = { name: cookieName, maxAge: downloadMaxAge() };
21
17
  res.locals.contentDownloadState.formId = surveyId;
22
18
  res.locals.contentDownloadState.cookie = cookie;
23
19
  };
package/middleware.js CHANGED
@@ -17,8 +17,11 @@ module.exports = (config) => asyncRoute(async (req, res, next) => {
17
17
 
18
18
  const cookie = service.getIdentity(res);
19
19
 
20
- // Don't overwrite an existing cookie
21
- if (cookie) return next();
20
+ // Don't overwrite an existing cookie, but do extend its expiration.
21
+ if (cookie) {
22
+ service.refreshIdentityCookie();
23
+ return next();
24
+ }
22
25
 
23
26
  // Set cookie for logged in users
24
27
  if (service.token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindful-web/marko-web-identity-x",
3
- "version": "1.80.2",
3
+ "version": "1.83.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>",
@@ -13,9 +13,9 @@
13
13
  "test": "yarn compile --no-clean && yarn lint"
14
14
  },
15
15
  "dependencies": {
16
- "@mindful-web/marko-web-gtm": "^1.80.2",
17
- "@mindful-web/object-path": "^1.80.2",
18
- "@mindful-web/utils": "^1.80.2",
16
+ "@mindful-web/marko-web-gtm": "^1.83.0",
17
+ "@mindful-web/object-path": "^1.83.0",
18
+ "@mindful-web/utils": "^1.83.0",
19
19
  "apollo-cache-inmemory": "^1.6.6",
20
20
  "apollo-client": "^2.6.10",
21
21
  "apollo-link-context": "^1.0.20",
@@ -38,5 +38,5 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "gitHead": "59e2be57d4905ab42c5acfa590c270a8c12bfce7"
41
+ "gitHead": "2b694183f136fdd763ae0d1bc47843c5bf6e9290"
42
42
  }
package/routes/access.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const gql = require('graphql-tag');
2
2
  const { asyncRoute } = require('@mindful-web/utils');
3
+ const { accessMaxAge, isValidName } = require('../utils/content-form-cookie');
3
4
 
4
5
  const mutation = gql`
5
6
  mutation StoreContentAccessSubmission($input: CreateContentAccessSubmissionMutationInput!) {
@@ -13,14 +14,24 @@ module.exports = asyncRoute(async (req, res) => {
13
14
  /** @type {import('../middleware').IdentityXRequest} */
14
15
  const { body, apollo, identityX } = req;
15
16
  const { contentId, payload, cookie } = body;
17
+
18
+ // Validate BEFORE the mutation: this previously destructured `cookie` blindly and passed its
19
+ // `maxAge` straight to `res.cookie`, so a missing key or non-numeric max age threw a 500 *after*
20
+ // the submission had already committed. The cookie's lifetime is never taken from the request --
21
+ // only its name, which must match one this app would have issued for this content.
22
+ const COOKIE_NAME = cookie && cookie.name;
23
+ if (!isValidName(COOKIE_NAME, contentId)) {
24
+ res.status(400).json({ ok: false, message: 'A valid content access cookie name is required.' });
25
+ return;
26
+ }
27
+
16
28
  const input = {
17
29
  contentId,
18
30
  payload,
19
31
  ipAddress: req.ip,
20
32
  };
21
- const { name: COOKIE_NAME, maxAge } = cookie;
22
33
  await apollo.mutate({ mutation, variables: { input } });
23
- res.cookie(COOKIE_NAME, true, { maxAge, httpOnly: false });
34
+ res.cookie(COOKIE_NAME, true, { maxAge: accessMaxAge(), httpOnly: false });
24
35
  const entity = await identityX.generateEntityId();
25
36
  res.json({
26
37
  ok: true,
@@ -1,5 +1,6 @@
1
1
  const gql = require('graphql-tag');
2
2
  const { asyncRoute } = require('@mindful-web/utils');
3
+ const { DOWNLOAD_SURVEY_ID, buildName, downloadMaxAge } = require('../utils/content-form-cookie');
3
4
 
4
5
  const mutation = gql`
5
6
  mutation StoreContentDownloadSubmission($input: CreateContentDownloadSubmissionMutationInput!) {
@@ -12,15 +13,26 @@ const mutation = gql`
12
13
  module.exports = asyncRoute(async (req, res) => {
13
14
  /** @type {import('../middleware').IdentityXRequest} */
14
15
  const { body, apollo, identityX } = req;
15
- const { contentId, payload, cookie } = body;
16
+ const { contentId, payload } = body;
17
+
18
+ // Validate BEFORE the mutation: this previously destructured `cookie` from the body blindly and
19
+ // passed its `maxAge` straight to `res.cookie`, so a missing key or non-numeric max age threw a
20
+ // 500 *after* the submission had already committed. Unlike the access form, the download form's
21
+ // survey id is a fixed constant, so the cookie is derived entirely server-side and the body's
22
+ // `cookie` is ignored.
23
+ if (!contentId) {
24
+ res.status(400).json({ ok: false, message: 'A content id is required.' });
25
+ return;
26
+ }
27
+
16
28
  const input = {
17
29
  contentId,
18
30
  payload,
19
31
  ipAddress: req.ip,
20
32
  };
21
- const { name: COOKIE_NAME, maxAge } = cookie;
22
33
  await apollo.mutate({ mutation, variables: { input } });
23
- res.cookie(COOKIE_NAME, true, { maxAge, httpOnly: false });
34
+ const COOKIE_NAME = buildName({ surveyId: DOWNLOAD_SURVEY_ID, contentId });
35
+ res.cookie(COOKIE_NAME, true, { maxAge: downloadMaxAge(), httpOnly: false });
24
36
  const entity = await identityX.generateEntityId();
25
37
  res.json({
26
38
  ok: true,
package/routes/google.js CHANGED
@@ -105,18 +105,44 @@ module.exports = asyncRoute(async (req, res) => {
105
105
  // the request dies is the step that failed, so an error arrives already
106
106
  // localized instead of being a bare GraphQL message to reverse-engineer.
107
107
  //
108
- // `idxGoogleStepMs` is the elapsed time when each step *began*, which makes the
109
- // route self-profiling: `FACET idxGoogleStep` with `max(idxGoogleStepMs)` shows
110
- // where the time goes, and a request that dies mid-flight still reports how far
111
- // it got and when. Sign-ins here average ~12s even when they succeed, and
112
- // without this there is no way to tell which of the ~10 sequential upstream
113
- // calls owns that.
108
+ // Timing comes in two parts, because they answer different questions.
109
+ //
110
+ // `idxGoogleStepMs` is elapsed-at-last-step: last-write-wins, so on a completed
111
+ // request it is effectively total route time, and on one that dies mid-flight it
112
+ // says how far in it got. Useful, but it cannot attribute time to a *step* —
113
+ // every `setStep` overwrites the previous value, so the breakdown it was
114
+ // originally meant to provide never survived.
115
+ //
116
+ // `idxMs<Step>` fixes that: one attribute per step, holding how long that step
117
+ // took. Emitted when the *next* step starts, since that is when the previous
118
+ // one is known to be over. `complete` therefore has no duration of its own,
119
+ // which is correct — it is a marker, not work. Roughly ten attributes, well
120
+ // inside New Relic's per-transaction custom attribute budget.
121
+ //
122
+ // SELECT average(idxMsHooksAuthenticationSuccess), average(idxMsImpersonateAppUser),
123
+ // average(idxMsHooksLoginLinkSent)
124
+ // FROM Transaction WHERE name LIKE '%__idx/google%'
125
+ //
126
+ // With Omeda calls showing a ~4s tail and both hook steps sitting on the
127
+ // blocking path, this is what says whether they own the remaining latency.
114
128
  const startedAt = Date.now();
115
129
  const addAttrs = config.get('addRequestAttributes') || (() => {});
116
- const setStep = (step) => addAttrs({
117
- idxGoogleStep: step,
118
- idxGoogleStepMs: Date.now() - startedAt,
119
- });
130
+
131
+ // 'hooks-login-link-sent' -> 'idxMsHooksLoginLinkSent'
132
+ const attrNameForStep = (step) => `idxMs${step.split('-').map((part) => (
133
+ part.charAt(0).toUpperCase() + part.slice(1)
134
+ )).join('')}`;
135
+
136
+ let currentStep;
137
+ let currentStepStartedAt = startedAt;
138
+ const setStep = (step) => {
139
+ const now = Date.now();
140
+ // Close out the step being left, not the one being entered.
141
+ if (currentStep) addAttrs({ [attrNameForStep(currentStep)]: now - currentStepStartedAt });
142
+ currentStep = step;
143
+ currentStepStartedAt = now;
144
+ addAttrs({ idxGoogleStep: step, idxGoogleStepMs: now - startedAt });
145
+ };
120
146
 
121
147
  const { clientId, missingFieldsBehavior = 'profile-gate' } = config.getAsObject('googleAuth');
122
148
  // `surface` is the GIS `select_by` bucket ('button' | 'prompt'), sent by the
package/service.js CHANGED
@@ -24,6 +24,9 @@ const tokenCookie = require('./utils/token-cookie');
24
24
  const isEmpty = (v) => v == null || v === '';
25
25
  const isFn = (v) => typeof v === 'function';
26
26
  const IDENTITY_COOKIE_NAME = '__idx_idt';
27
+ // Express's `res.cookie` maxAge is in **milliseconds**. This was previously expressed in seconds,
28
+ // which silently reduced the cookie's lifetime from one year to ~8.76 hours.
29
+ const IDENTITY_COOKIE_MAX_AGE = 1000 * 60 * 60 * 24 * 365;
27
30
 
28
31
  const { log } = console;
29
32
 
@@ -294,12 +297,29 @@ class IdentityX {
294
297
  */
295
298
  setIdentityCookie(id) {
296
299
  const options = {
297
- maxAge: 60 * 60 * 24 * 365,
300
+ maxAge: IDENTITY_COOKIE_MAX_AGE,
298
301
  httpOnly: false,
299
302
  };
300
303
  this.res.cookie(IDENTITY_COOKIE_NAME, id, options);
301
304
  }
302
305
 
306
+ /**
307
+ * Extends the expiration of an already-persisted identity cookie, so an active visitor's
308
+ * identity only lapses after a genuine year of absence.
309
+ *
310
+ * Deliberately reads `req.cookies` directly rather than using `getIdentity`: that falls back to
311
+ * parsing the outgoing `Set-Cookie` header, and express *appends* cookies rather than replacing
312
+ * them, so refreshing off it would emit a duplicate for a cookie set earlier in this request.
313
+ *
314
+ * @returns {Boolean} Whether the cookie was refreshed.
315
+ */
316
+ refreshIdentityCookie() {
317
+ const id = get(this.req, `cookies.${IDENTITY_COOKIE_NAME}`);
318
+ if (!id || this.res.headersSent) return false;
319
+ this.setIdentityCookie(id);
320
+ return true;
321
+ }
322
+
303
323
  /**
304
324
  * @param {Object} o
305
325
  * @param {String} o.identifier
@@ -0,0 +1,64 @@
1
+ const { resolveDurationMS } = require('@mindful-web/utils');
2
+
3
+ const COOKIE_NAME_PREFIX = '__idx_form';
4
+ const DEFAULT_MAX_AGE = 24 * 60 * 60 * 1000; // 1 day, in ms (express maxAge is ms)
5
+ const DOWNLOAD_SURVEY_ID = 'content-download';
6
+
7
+ /**
8
+ * Resolves the content-access cookie max age, in milliseconds.
9
+ *
10
+ * `IDX_CONTENT_ACCESS_MAXAGE` is a deprecated alias: "MAXAGE" reads as the HTTP `Max-Age`
11
+ * directive, which is in *seconds*, so an operator setting `86400` for "one day" silently got 86
12
+ * seconds. The `_MS` suffix makes the unit explicit; the old name still works.
13
+ */
14
+ const accessMaxAge = () => resolveDurationMS({
15
+ value: process.env.IDX_CONTENT_ACCESS_MAXAGE_MS || process.env.IDX_CONTENT_ACCESS_MAXAGE,
16
+ defaultValue: DEFAULT_MAX_AGE,
17
+ name: 'IDX_CONTENT_ACCESS_MAXAGE_MS',
18
+ });
19
+
20
+ /**
21
+ * Resolves the content-download cookie max age, in milliseconds. See `accessMaxAge` on the
22
+ * deprecated `IDX_CONTENT_DOWNLOAD_MAXAGE` alias.
23
+ */
24
+ const downloadMaxAge = () => resolveDurationMS({
25
+ value: process.env.IDX_CONTENT_DOWNLOAD_MAXAGE_MS || process.env.IDX_CONTENT_DOWNLOAD_MAXAGE,
26
+ defaultValue: DEFAULT_MAX_AGE,
27
+ name: 'IDX_CONTENT_DOWNLOAD_MAXAGE_MS',
28
+ });
29
+
30
+ /**
31
+ * @param {Object} params
32
+ * @param {String} params.surveyId
33
+ * @param {String|Number} params.contentId
34
+ * @returns {String} The cookie name, e.g. `__idx_form_content-download_12345`
35
+ */
36
+ const buildName = ({ surveyId, contentId }) => `${COOKIE_NAME_PREFIX}_${surveyId}_${contentId}`;
37
+
38
+ /**
39
+ * Whether a client-supplied cookie name is one this app would have issued for `contentId`.
40
+ *
41
+ * The access form's cookie name is derived from the content's `gating.surveyId`, which the submit
42
+ * route does not have (only `contentId` is posted), so the name still round-trips through the
43
+ * browser. Validating it against the expected shape prevents a caller from writing an
44
+ * arbitrarily-named cookie on `/`, without costing an extra GraphQL round trip on the submit path.
45
+ *
46
+ * @param {String} name
47
+ * @param {String|Number} contentId
48
+ * @returns {Boolean}
49
+ */
50
+ const isValidName = (name, contentId) => {
51
+ if (typeof name !== 'string' || !contentId) return false;
52
+ const escaped = `${contentId}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
+ return new RegExp(`^${COOKIE_NAME_PREFIX}_[\\w-]+_${escaped}$`).test(name);
54
+ };
55
+
56
+ module.exports = {
57
+ COOKIE_NAME_PREFIX,
58
+ DEFAULT_MAX_AGE,
59
+ DOWNLOAD_SURVEY_ID,
60
+ accessMaxAge,
61
+ buildName,
62
+ downloadMaxAge,
63
+ isValidName,
64
+ };