@mindful-web/marko-web-identity-x 1.80.3 → 1.83.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
@@ -99,16 +99,29 @@ path of authentication. To compare upstreams:
99
99
  **`MindfulApiCall` is sampled.** Unsampled it ran ~55k events/hour / ~40M per month on the Fusable
100
100
  fleet, 88.9% of it `GetActiveAppContext` + `CheckContentAccess` — per-page-load context calls that
101
101
  are uniformly fast and analytically useless. Kept: **every non-`ok` outcome**, **every `ok` call
102
- >= `MINDFUL_API_CALL_SLOW_MS`** (default 250ms — deliberately under Omeda's observed p95 of 282ms, so
103
- ordinary Omeda latency stays visible while fast idx calls fall away), and **1 in
104
- `MINDFUL_API_CALL_SAMPLE_RATE`** (default 100) of the rest. Each event carries `sampleRate` = the
105
- number of real calls it represents, so `sum(sampleRate)` recovers true volume; `sampleRate = 1` means
106
- the call was kept because it failed or was slow.
102
+ >= `MINDFUL_API_CALL_SLOW_MS`** (default 250ms) *whose operation is not in*
103
+ **`MINDFUL_API_CALL_NO_SLOW_CAPTURE`** (default `GetActiveAppContext,CheckContentAccess`), and **1 in
104
+ `MINDFUL_API_CALL_SAMPLE_RATE`** (default 100) of everything else. Each event carries `sampleRate` =
105
+ the number of real calls it represents, so `sum(sampleRate)` recovers true volume; `sampleRate = 1`
106
+ means the call was kept because it failed or was slow.
107
+
108
+ **Why the exclusion list exists.** The 250ms threshold alone did not hold. It was tuned against a
109
+ `GetActiveAppContext` p95 of 206ms measured over only **1.4 hours**; over 48h that operation cleared
110
+ 250ms constantly and produced **96.3% of all kept events** (259,448 of 269,322 — 5,625/hour), so the
111
+ sampler barely applied to the one operation generating the volume. Excluding the two per-page-load
112
+ context operations from *slow*-capture drops them through to the 1-in-N sampler instead. Raising the
113
+ threshold to ~1s was the alternative and is worse: it costs fidelity on every *other* operation to
114
+ fix two. Errors and timeouts are unaffected — `GetActiveAppContext` contributed 28 errors and the
115
+ only observed timeout (15,009ms against the 15s budget), and all of it still flows.
107
116
 
108
117
  Consequences when querying: `average(ms)` over everything **skews high** (fast calls are
109
118
  under-represented by design) — filter `WHERE sampleRate = 1` for the error/slow tail, or
110
- `WHERE sampleRate > 1` for a fast-call view. Set `MINDFUL_API_CALL_SAMPLE_RATE=1` to record
111
- everything again during an investigation, no deploy required.
119
+ `WHERE sampleRate > 1` for a fast-call view. For the excluded operations the skew runs the *other*
120
+ way: every success is sampled regardless of duration, so their `sampleRate = 1` rows are errors and
121
+ timeouts **only**, never a slow tail — use `sampleRate > 1` with `percentile(ms, 95)` for their
122
+ latency. Set `MINDFUL_API_CALL_SAMPLE_RATE=1` to record everything again during an investigation, or
123
+ `MINDFUL_API_CALL_NO_SLOW_CAPTURE=` (explicitly empty) to restore slow-capture for every operation —
124
+ neither needs a deploy. An *unset* `NO_SLOW_CAPTURE` means the default list, not "exclude nothing".
112
125
 
113
126
  It wraps `client.query`/`client.mutate`, **not** `fetch` — `@parameter1/omeda-graphql-client` sets
114
127
  `fetch` *after* spreading the caller's `linkConfig`, so a wrapped transport cannot be injected from
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.3/components/access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/access.marko",
56
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/comment-stream.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/comment-stream.marko",
94
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/context.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/context.marko",
52
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-access.marko",
182
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-authenticate.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-authenticate.marko",
84
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-change-email.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-change-email.marko",
62
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-login.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-login.marko",
95
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-logout.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-logout.marko",
32
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-profile.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-profile.marko",
87
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-register.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/form-register.marko",
28
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/google-init.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/google-init.marko"
242
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/google-sign-in-button.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/google-sign-in-button.marko"
43
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/identify.marko",
53
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.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.80.3/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.83.1/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.3",
3
+ "version": "1.83.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>",
@@ -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.1",
17
+ "@mindful-web/object-path": "^1.83.1",
18
+ "@mindful-web/utils": "^1.83.1",
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": "02de6be94c16fd962d5c94b37b9f09a151142f48"
41
+ "gitHead": "c9507129d0e30847cb2b2a941cb383746d1fd501"
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/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
+ };