@dreamforgestudiollc/emet 0.0.0-stage → 0.2.6

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.
Files changed (114) hide show
  1. package/.env.example +189 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CHARTER.md +137 -0
  4. package/LICENSE +22 -0
  5. package/README.md +868 -2
  6. package/docs/AUTHORSHIP.md +26 -0
  7. package/docs/BENCHMARKS.md +261 -0
  8. package/docs/BOOTSTRAP.md +127 -0
  9. package/docs/BOTS.md +83 -0
  10. package/docs/COVERAGE.md +64 -0
  11. package/docs/DEPLOY.md +251 -0
  12. package/docs/DISCIPLINES.md +29 -0
  13. package/docs/GUARDRAILS-AND-MONORAILS.md +108 -0
  14. package/docs/GUIDED-SETUP.md +301 -0
  15. package/docs/PERMISSIONS.md +121 -0
  16. package/docs/ROADMAP.md +62 -0
  17. package/docs/SETUP.md +265 -0
  18. package/docs/SUBKEYS.md +59 -0
  19. package/gems/artifact-index.json +12 -0
  20. package/gems/bot-team-backup/floor.json +15 -0
  21. package/gems/bot-team-backup/gem.json +43 -0
  22. package/gems/bot-team-backup/guides/BOT_TEAM_BACKUP.md +27 -0
  23. package/gems/bot-team-backup/guides/BOT_TEAM_RECOVERY.md +23 -0
  24. package/gems/fieldnote/floor.json +14 -0
  25. package/gems/fieldnote/gem.json +70 -0
  26. package/gems/fieldnote/runes/fieldnote.js +4 -0
  27. package/gems/lean-six-sigma/CHANGELOG.md +10 -0
  28. package/gems/lean-six-sigma/README.md +92 -0
  29. package/gems/lean-six-sigma/floor.json +31 -0
  30. package/gems/lean-six-sigma/gem.json +1296 -0
  31. package/gems/lean-six-sigma/guides/ASSUMPTIONS.md +45 -0
  32. package/gems/lean-six-sigma/guides/BELTS.md +52 -0
  33. package/gems/lean-six-sigma/guides/DMADV.md +56 -0
  34. package/gems/lean-six-sigma/guides/DMAIC.md +127 -0
  35. package/gems/lean-six-sigma/guides/FORMULAS.md +330 -0
  36. package/gems/lean-six-sigma/guides/LEAN.md +97 -0
  37. package/gems/lean-six-sigma/guides/TEMPLATES.md +279 -0
  38. package/gems/lean-six-sigma/runes/lss_capability.js +132 -0
  39. package/gems/lean-six-sigma/runes/lss_control_limits.js +240 -0
  40. package/gems/lean-six-sigma/runes/lss_dpmo_sigma.js +81 -0
  41. package/gems/lean-six-sigma/runes/lss_hypothesis.js +298 -0
  42. package/gems/lean-six-sigma/runes/lss_sample_size.js +81 -0
  43. package/gems/neurendipity/floor.json +65 -0
  44. package/gems/neurendipity/gem.json +42 -0
  45. package/gems/neurendipity/guides/AUTISTIC_BURNOUT.md +81 -0
  46. package/gems/neurendipity/guides/NEURODIVERGENT_SUPPORT.md +287 -0
  47. package/guides/README.md +3 -0
  48. package/package.json +76 -4
  49. package/server.json +79 -0
  50. package/src/aleph.js +395 -0
  51. package/src/annotations.js +170 -0
  52. package/src/approve-throttle.js +54 -0
  53. package/src/atlas-role.js +58 -0
  54. package/src/attribution.js +555 -0
  55. package/src/conformance.js +688 -0
  56. package/src/content_analyzer.js +450 -0
  57. package/src/database.js +812 -0
  58. package/src/disciplines.js +26 -0
  59. package/src/dispatch.js +382 -0
  60. package/src/env.js +97 -0
  61. package/src/floor.js +228 -0
  62. package/src/gaps.js +509 -0
  63. package/src/gem-check.js +1359 -0
  64. package/src/guides.js +75 -0
  65. package/src/host-reach.js +23 -0
  66. package/src/http-remote.js +313 -0
  67. package/src/index.js +397 -0
  68. package/src/invites.js +167 -0
  69. package/src/layers.js +449 -0
  70. package/src/lessons-to-checks.js +366 -0
  71. package/src/logger.js +300 -0
  72. package/src/metadata.js +61 -0
  73. package/src/module-path.js +32 -0
  74. package/src/mongo-connect.js +91 -0
  75. package/src/named-items.js +208 -0
  76. package/src/oauth-provider.js +838 -0
  77. package/src/owner-approvals.js +281 -0
  78. package/src/persons.js +136 -0
  79. package/src/provenance.js +257 -0
  80. package/src/redact.js +237 -0
  81. package/src/request-log.js +124 -0
  82. package/src/rune-dispatch.js +360 -0
  83. package/src/rune-registry.js +71 -0
  84. package/src/rune-scope.js +42 -0
  85. package/src/scopes.js +100 -0
  86. package/src/security-headers.js +26 -0
  87. package/src/session-graph.js +947 -0
  88. package/src/session.js +1097 -0
  89. package/src/setup.js +1580 -0
  90. package/src/socket.js +545 -0
  91. package/src/startup-pages.js +371 -0
  92. package/src/startup-payload.js +129 -0
  93. package/src/subkey-store.js +357 -0
  94. package/src/subkeys.js +947 -0
  95. package/src/tools.js +4508 -0
  96. package/src/transcript-session.js +106 -0
  97. package/src/validation.js +1187 -0
  98. package/src/version.js +2 -0
  99. package/templates/BOARD.md +72 -0
  100. package/templates/BOTHANDOFF.md +57 -0
  101. package/templates/CHARACTER.md +30 -0
  102. package/templates/HANDOFF.md +63 -0
  103. package/templates/IDENTITY.md +29 -0
  104. package/templates/INDEX.md +82 -0
  105. package/templates/INSTALL.md +29 -0
  106. package/templates/PROCEDURE.md +39 -0
  107. package/templates/ROSTER.md +43 -0
  108. package/templates/SEED.md +65 -0
  109. package/templates/STAGING.md +37 -0
  110. package/templates/STANDARD.md +99 -0
  111. package/templates/THREAD.md +43 -0
  112. package/templates/USER.md +32 -0
  113. package/verification/load-gate/corpus.json +1765 -0
  114. package/verification/load-gate/load-gate.mjs +1246 -0
package/src/redact.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ *
5
+ * Licensed under the MIT License - see LICENSE
6
+ *
7
+ * Secret redaction at the write path.
8
+ *
9
+ * Why this exists (2026-09-07): an OAuth passphrase was pasted into a chat and a
10
+ * screenshot during a connector re-add, and the session transcript that would
11
+ * carry it verbatim was minutes away from being written to the store. A rule
12
+ * that says "do not paste secrets" is an instruction; a scrubber the content
13
+ * passes through before it lands anywhere is a control. This module is the
14
+ * control. It sits inside the single write path (THE DOOR, procedural ref 280):
15
+ * memory saves, transcript saves, document writes, and log output all pass
16
+ * through it, so an instance cannot opt out by forgetting.
17
+ *
18
+ * What it masks, in order:
19
+ * 1. Literal values of secret-bearing environment variables (anything whose
20
+ * name contains PASSPHRASE, PASSWORD, SECRET, TOKEN, API_KEY, PRIVATE_KEY,
21
+ * PEPPER, or MONGODB_URI), plus the user:password pair inside any URI among them.
22
+ * These are exact-string matches and cannot false-positive.
23
+ * 2. Credentials embedded in URIs: scheme://user:password@host.
24
+ * 3. Bearer tokens and JWTs.
25
+ * 4. key/value pairs whose key names a secret (passphrase, password, secret,
26
+ * token, api_key, client_secret, access_token, refresh_token, authorization)
27
+ * followed by : or = and a value. The key survives, the value does not.
28
+ *
29
+ * What it deliberately does NOT mask: hex digests (sha256 values are 64 hex
30
+ * chars and are part of the integrity record), ids, and ordinary prose. Rule 4
31
+ * can mask a word in prose that follows "token:" - that is accepted. Masking a
32
+ * word costs a word; missing a secret costs the secret. (An English prefix
33
+ * hyphenated onto a key word - "re-authorization:" - is not treated as a key;
34
+ * that one masked prose on 2026-09-07 and is excluded.)
35
+ *
36
+ * The mask is visible on purpose: '*****' says "something was here and was
37
+ * removed", which is the honest record. A silent drop would be a summary.
38
+ *
39
+ * The hit count reports CHANGES. Text that already carries the mask is matched
40
+ * and left as it is, uncounted - otherwise every read-modify-write of a
41
+ * document re-reports the masks it already held (observed 2026-09-07, meta ref 83).
42
+ */
43
+
44
+ export const MASK = '*****';
45
+
46
+ const SECRET_ENV_NAME = /(PASSPHRASE|PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|PRIVATE[_-]?KEY|PEPPER|MONGODB_URI)/i;
47
+ const MIN_LITERAL_LENGTH = 6;
48
+
49
+ // Registered literal secrets. A Set so re-registering is a no-op.
50
+ const literals = new Set();
51
+
52
+ function escapeRegex(s) {
53
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
54
+ }
55
+
56
+ /**
57
+ * Registers one literal secret value. Anything shorter than MIN_LITERAL_LENGTH
58
+ * is ignored - masking "abc" everywhere would shred ordinary text.
59
+ * If the value is a URI with credentials, the user:password pair is registered
60
+ * as well, so a URI quoted with a different host or query still masks.
61
+ */
62
+ export function registerSecret(value) {
63
+ if (typeof value !== 'string') return false;
64
+ const v = value.trim();
65
+ if (v.length < MIN_LITERAL_LENGTH) return false;
66
+ literals.add(v);
67
+ literalOrder = null;
68
+ const m = v.match(/^[a-z][a-z0-9+.-]*:\/\/([^/@\s]+)@/i);
69
+ if (m && m[1].includes(':')) {
70
+ const [, pass] = m[1].split(/:(.*)/s);
71
+ if (pass && pass.length >= MIN_LITERAL_LENGTH) literals.add(pass);
72
+ }
73
+ return true;
74
+ }
75
+
76
+ /** Registers every secret-bearing value in an env object (defaults to process.env). */
77
+ export function registerSecretsFromEnv(env = process.env) {
78
+ let n = 0;
79
+ for (const [k, v] of Object.entries(env)) {
80
+ if (SECRET_ENV_NAME.test(k) && registerSecret(v)) n++;
81
+ }
82
+ return n;
83
+ }
84
+
85
+ /** Test seam. Not for production use. */
86
+ export function _clearRegisteredSecrets() {
87
+ literals.clear();
88
+ literalOrder = null;
89
+ }
90
+
91
+ // Longest-first order, computed once per registry change rather than once per
92
+ // write. The registry changes at startup and effectively never again; sorting
93
+ // it on every scrub was work repeated for no reason.
94
+ let literalOrder = null;
95
+ function orderedLiterals() {
96
+ if (literalOrder === null) literalOrder = [...literals].sort((a, b) => b.length - a.length);
97
+ return literalOrder;
98
+ }
99
+
100
+ export function registeredSecretCount() {
101
+ return literals.size;
102
+ }
103
+
104
+ // A necessary-condition prefilter for the key/value rule. Every alternative in
105
+ // KEY_NAMES contains one of these substrings, so text without any of them
106
+ // cannot match - and skipping the full pattern then is free correctness.
107
+ const KEY_HINT = /pass|secret|token|key|authorization|credential/i;
108
+
109
+ // Pattern rules. Each replaces with a function so the hit count is exact.
110
+ // `pre` is a cheap NECESSARY condition: a substring the full pattern cannot
111
+ // match without. It never decides a match - it only decides whether the
112
+ // expensive scan is worth starting.
113
+ const KEY_NAMES = '(?:pass(?:phrase|word|wd)|secret|token|api[_-]?key|client_secret|access_token|refresh_token|authorization|credential)';
114
+ const RULES = [
115
+ // scheme://user:password@host -> scheme://*****:*****@host
116
+ { name: 'uri-credentials', pre: (t) => t.includes('://'),
117
+ re: /([a-z][a-z0-9+.-]*:\/\/)([^/@\s:]+):([^/@\s]+)@/gi, sub: (_m, scheme) => `${scheme}${MASK}:${MASK}@` },
118
+ // Bearer <token>
119
+ { name: 'bearer', pre: (t) => t.includes('Bearer'),
120
+ re: /\b(Bearer)\s+[A-Za-z0-9._~+/=-]{8,}/g, sub: (_m, w) => `${w} ${MASK}` },
121
+ // JWT
122
+ { name: 'jwt', pre: (t) => t.includes('eyJ'),
123
+ re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, sub: () => MASK },
124
+ // Delegated client sub-keys (EMET-0001). The full key, the session handle
125
+ // and the claim nonce. A bare key_id (32 hex, no prefix) is an index and
126
+ // stays visible. Lengths are exact so a shorter hex id is not masked.
127
+ { name: 'subkey', pre: (t) => t.includes('emk_') || t.includes('emh_') || t.includes('emn_'),
128
+ re: /(?<![A-Za-z0-9_])(?:emk_[0-9a-f]{32}_[A-Za-z0-9_-]{43}|emh_[A-Za-z0-9_-]{43}|emn_[A-Za-z0-9_-]{43})(?![A-Za-z0-9_-])/g,
129
+ sub: () => MASK },
130
+ // key: value / key = value / "key": "value" (key survives, value is masked)
131
+ { name: 'key-value',
132
+ // Every alternative in KEY_NAMES contains one of these substrings, and the
133
+ // rule also requires a ':' or '='. Both are NECESSARY conditions, so
134
+ // failing this check cannot skip a match the full pattern would have found.
135
+ pre: (t) => KEY_HINT.test(t) && (t.includes(':') || t.includes('=')),
136
+ // The scheme words of an Authorization header are not the secret; the
137
+ // bearer rule above has already masked what followed them.
138
+ // G5 (2026-09-07): the prefix class lets a header name like x-api-key
139
+ // through, but it also let "re-authorization:" in prose count as a key and
140
+ // mask the word after it (thread §32). An English prefix joined by a hyphen
141
+ // is not a key name: re-, pre-, de-, un-, non- are excluded right before
142
+ // the key word.
143
+ // G6 (2026-09-07): the value stops at a backtick and does not swallow a
144
+ // trailing period or closing parenthesis - prose like `passphrase: x`. kept
145
+ // losing its closing backtick and full stop. A secret that itself ends in
146
+ // "." or ")" keeps that one character visible; accepted.
147
+ re: new RegExp(`(["']?\\b[\\w.-]*(?<!\\b(?:re|pre|de|un|non)-)${KEY_NAMES}["']?\\s*[:=]\\s*)(["']?)(?!(?:Bearer|Basic|Digest)\\b)(?=[^\\s"'\`,;}\\]]{4})([^\\s"'\`,;}\\]]*[^\\s"'\`,;}\\].)])([.)]*)(\\2)`, 'gi'),
148
+ sub: (_m, key, q, _v, trail, q2) => `${key}${q}${MASK}${trail}${q2}` }
149
+ ];
150
+
151
+ /**
152
+ * Redacts a string. Returns { text, hits } where hits is the number of
153
+ * replacements made. Non-strings are returned unchanged with hits 0.
154
+ */
155
+ export function redactWithReport(input) {
156
+ if (typeof input !== 'string' || input.length === 0) return { text: input, hits: 0 };
157
+ let text = input;
158
+ let hits = 0;
159
+ // Literals first: exact, longest first so a value that contains another is
160
+ // masked whole rather than leaving a fragment.
161
+ //
162
+ // These are literal strings, so they need no regex at all. The previous
163
+ // version compiled one RegExp per literal PER CALL and scanned the whole body
164
+ // with it whether or not the literal was present. indexOf answers "is it
165
+ // here" far more cheaply than a regex scan, and split/join is the same
166
+ // operation as a global replace on an escaped literal - identical output,
167
+ // identical hit count.
168
+ for (const lit of orderedLiterals()) {
169
+ if (!text.includes(lit)) continue;
170
+ const parts = text.split(lit);
171
+ hits += parts.length - 1;
172
+ text = parts.join(MASK);
173
+ }
174
+ // G4 (2026-09-07): a hit is a CHANGE. Text that already carries the mask
175
+ // ("passphrase: *****") matches the key/value rule and is replaced with
176
+ // itself; counting that made every read-modify-write of a document report
177
+ // the masks it already held as new redactions. The receipt's `redacted` is
178
+ // the number of spans this write changed - nothing else.
179
+ for (const rule of RULES) {
180
+ // The prefilter is a necessary condition for the pattern, so skipping on a
181
+ // miss cannot change the output. It exists because the scrubber was paying
182
+ // nearly full price on documents containing no secrets at all: measured
183
+ // 2026-09-08, a clean 300 KB body cost 6.40 ms against 7.25 ms for a
184
+ // secret-bearing one. The cost was scanning, not replacing.
185
+ if (rule.pre && !rule.pre(text)) continue;
186
+ text = text.replace(rule.re, (...args) => {
187
+ const out = rule.sub(...args);
188
+ if (out !== args[0]) hits++;
189
+ return out;
190
+ });
191
+ }
192
+ return { text, hits };
193
+ }
194
+
195
+ /** Convenience: the redacted string only. */
196
+ export function redactText(input) {
197
+ return redactWithReport(input).text;
198
+ }
199
+
200
+ const SECRET_KEY = new RegExp(`^[\\w.-]*${KEY_NAMES}$`, 'i');
201
+
202
+ /**
203
+ * Deep-redacts any value: strings are scrubbed; object keys that name a secret
204
+ * have their value masked outright regardless of content; arrays and plain
205
+ * objects are walked. Other types pass through. Returns { value, hits }.
206
+ */
207
+ export function redactDeep(value, _seen = new WeakSet()) {
208
+ if (typeof value === 'string') {
209
+ const r = redactWithReport(value);
210
+ return { value: r.text, hits: r.hits };
211
+ }
212
+ if (Array.isArray(value)) {
213
+ let hits = 0;
214
+ const out = value.map(v => { const r = redactDeep(v, _seen); hits += r.hits; return r.value; });
215
+ return { value: out, hits };
216
+ }
217
+ if (value && typeof value === 'object') {
218
+ if (_seen.has(value)) return { value, hits: 0 };
219
+ _seen.add(value);
220
+ if (value instanceof Date || Buffer.isBuffer?.(value)) return { value, hits: 0 };
221
+ let hits = 0;
222
+ const out = {};
223
+ for (const [k, v] of Object.entries(value)) {
224
+ if (SECRET_KEY.test(k) && v !== null && v !== undefined && v !== '' && typeof v !== 'object') {
225
+ // G4: an already-masked value is left as it is and not counted.
226
+ if (v === MASK) { out[k] = v; } else { out[k] = MASK; hits++; }
227
+ } else {
228
+ const r = redactDeep(v, _seen); hits += r.hits; out[k] = r.value;
229
+ }
230
+ }
231
+ return { value: out, hits };
232
+ }
233
+ return { value, hits: 0 };
234
+ }
235
+
236
+ // Load-time registration: the process's own secrets are always known to it.
237
+ registerSecretsFromEnv();
@@ -0,0 +1,124 @@
1
+ /**
2
+ * EMET - Enduring Memory & Epistemic Tiers
3
+ * Copyright (c) 2026 Dreamforge Studio LLC
4
+ *
5
+ * Licensed under the MIT License - see LICENSE
6
+ *
7
+ * Request / auth logging for the HTTP transport (work list H4, 2026-09-07).
8
+ *
9
+ * Why: on 2026-09-07 a connector was added at the wrong URL, the browser
10
+ * authorization ran to completion, the client then POSTed to `/` and got a 404,
11
+ * and the user saw "an error". Railway's logs held three startup lines and
12
+ * nothing else. A server that cannot say what it answered cannot be diagnosed
13
+ * without reproducing the failure by hand - which is what happened.
14
+ *
15
+ * What is logged: one line per request on the OAuth and MCP paths, plus any
16
+ * request anywhere that ends 4xx/5xx (so a POST to a path that does not exist
17
+ * is visible). Fields: method, path, status, latency, client ip, and the
18
+ * identifying-but-not-secret parts of the exchange - client_id (a CIMD URL or
19
+ * an opaque registration id), grant_type, response_type, resource, the JSON-RPC
20
+ * method, tool name and the NAMES of the tool's parameters on /mcp (never their
21
+ * values - added 2026-09-27 so parameter use can be measured), and a reason on failures.
22
+ *
23
+ * What is never logged: request or response bodies, authorization codes,
24
+ * tokens, code verifiers, passphrases, or query strings as a whole. The line
25
+ * is still passed through the redaction scrubber before it is written -
26
+ * belt and braces; a control that depends on the author remembering is a rule.
27
+ */
28
+
29
+ import { redactText } from './redact.js';
30
+
31
+ // Paths always logged, whatever the status.
32
+ export const LOGGED_PATH = /^\/(?:authorize|token|register|revoke|oauth\/approve|mcp|\.well-known\/[^\s]*)$/;
33
+
34
+ const CLIP = 160;
35
+ const clip = (v) => String(v).slice(0, CLIP);
36
+
37
+ /** Whether a finished request should produce a log line. */
38
+ export function shouldLog(path, statusCode) {
39
+ return LOGGED_PATH.test(path) || statusCode >= 400;
40
+ }
41
+
42
+ /**
43
+ * Builds the log entry for one finished request. Pure: takes the request
44
+ * fields it needs rather than the request object, so it is testable without
45
+ * express. `body` is whatever the parser left on req.body (may be undefined
46
+ * on a 401 that stopped before parsing), `query` is req.query.
47
+ */
48
+ export function requestLogEntry({ method, path, statusCode, ms, ip, query = {}, body, wwwAuthenticate, reason }) {
49
+ const entry = { t: new Date().toISOString(), req: `${method} ${path}`, status: statusCode, ms: Math.round(ms) };
50
+ if (ip) entry.ip = ip;
51
+
52
+ const q = (query && typeof query === 'object') ? query : {};
53
+ const b = (body && typeof body === 'object' && !Array.isArray(body)) ? body : {};
54
+
55
+ const clientId = q.client_id ?? b.client_id;
56
+ if (clientId !== undefined) entry.client = clip(clientId);
57
+ if (b.grant_type) entry.grant = clip(b.grant_type);
58
+ if (q.response_type) entry.response_type = clip(q.response_type);
59
+ const resource = q.resource ?? b.resource;
60
+ if (resource !== undefined && resource !== null && resource !== '') entry.resource = clip(resource);
61
+ if (q.redirect_uri ?? b.redirect_uri) {
62
+ try { entry.redirect_host = new URL(q.redirect_uri ?? b.redirect_uri).host; } catch { entry.redirect_host = 'invalid'; }
63
+ }
64
+
65
+ if (path === '/mcp') {
66
+ if (Array.isArray(body)) entry.rpc = `batch(${body.length})`;
67
+ else if (b.method) {
68
+ entry.rpc = clip(b.method);
69
+ if (b.method === 'tools/call' && b.params && b.params.name) {
70
+ entry.tool = clip(b.params.name);
71
+ // Parameter NAMES only (Alex, 2026-09-27, code audit 1.6) - values never.
72
+ const a = b.params.arguments;
73
+ if (a && typeof a === 'object' && !Array.isArray(a)) entry.params = Object.keys(a).sort().map(clip);
74
+ }
75
+ }
76
+ }
77
+
78
+ if (reason) entry.reason = clip(reason);
79
+ else if (statusCode === 401 && wwwAuthenticate) {
80
+ const m = String(wwwAuthenticate).match(/error="([^"]+)"/);
81
+ if (m) entry.reason = m[1];
82
+ }
83
+
84
+ return entry;
85
+ }
86
+
87
+ /** The rendered line. Scrubbed even though nothing secret should be in it. */
88
+ export function formatRequestLog(entry) {
89
+ return '[emet][req] ' + redactText(JSON.stringify(entry));
90
+ }
91
+
92
+ /**
93
+ * Express middleware. Installed before the auth router so it sees every
94
+ * request; logs on `finish`, when the status and any parsed body are known.
95
+ */
96
+ export function requestLogger({ enabled = true, write = (line) => console.error(line) } = {}) {
97
+ return function emetRequestLog(req, res, next) {
98
+ if (!enabled) return next();
99
+ const t0 = process.hrtime.bigint();
100
+ // Capture the path NOW. The SDK's auth router mounts /authorize, /token,
101
+ // /register and the well-known documents as sub-routers, and express
102
+ // strips the mount prefix from req.path while a sub-router is handling
103
+ // the request - at `finish` it can read as '/' with status 200, which the
104
+ // filter would then skip. Found live on the first deploy (2026-09-07): the
105
+ // /.well-known line was the one missing from the log.
106
+ const path = String(req.originalUrl || req.url || '/').split('?')[0] || '/';
107
+ res.on('finish', () => {
108
+ try {
109
+ if (!shouldLog(path, res.statusCode)) return;
110
+ const ms = Number(process.hrtime.bigint() - t0) / 1e6;
111
+ const entry = requestLogEntry({
112
+ method: req.method, path, statusCode: res.statusCode, ms, ip: req.ip,
113
+ query: req.query, body: req.body,
114
+ wwwAuthenticate: res.getHeader && res.getHeader('www-authenticate'),
115
+ reason: res.locals && res.locals.reason
116
+ });
117
+ write(formatRequestLog(entry));
118
+ } catch (e) {
119
+ write('[emet][req] log failed: ' + (e && e.message));
120
+ }
121
+ });
122
+ next();
123
+ };
124
+ }