@rubytech/create-maxy-code 0.1.460 → 0.1.462

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 (202) hide show
  1. package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
  2. package/dist/__tests__/launchd-plist.test.js +26 -0
  3. package/dist/cron-registration.js +30 -8
  4. package/dist/index.js +50 -9
  5. package/dist/launchd-plist.js +1 -1
  6. package/dist/uninstall.js +9 -3
  7. package/package.json +1 -1
  8. package/payload/platform/lib/graph-mcp/dist/index.d.ts +2 -1
  9. package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-mcp/dist/index.js +14 -22
  11. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
  12. package/payload/platform/lib/graph-mcp/src/index.ts +15 -22
  13. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +8 -7
  14. package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -1
  15. package/payload/platform/lib/mcp-spawn-tee/dist/index.js +19 -19
  16. package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
  17. package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +34 -5
  18. package/payload/platform/lib/mcp-spawn-tee/src/index.ts +22 -19
  19. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +35 -17
  20. package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
  21. package/payload/platform/lib/mcp-stderr-tee/dist/index.js +41 -48
  22. package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
  23. package/payload/platform/lib/mcp-stderr-tee/src/index.ts +42 -51
  24. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +27 -0
  25. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  26. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +49 -0
  27. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  28. package/payload/platform/lib/storage-broker/dist/audit.d.ts +11 -0
  29. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  30. package/payload/platform/lib/storage-broker/dist/audit.js +16 -0
  31. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  32. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +18 -0
  33. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  34. package/payload/platform/lib/storage-broker/dist/cf-exec.js +77 -0
  35. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  36. package/payload/platform/lib/storage-broker/dist/house-scoped-token.js +3 -0
  37. package/payload/platform/lib/storage-broker/dist/house-scoped-token.js.map +1 -1
  38. package/payload/platform/lib/storage-broker/dist/registry.d.ts +1 -1
  39. package/payload/platform/lib/storage-broker/dist/registry.d.ts.map +1 -1
  40. package/payload/platform/lib/storage-broker/dist/registry.js.map +1 -1
  41. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +32 -1
  42. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +61 -0
  43. package/payload/platform/lib/storage-broker/src/audit.ts +45 -0
  44. package/payload/platform/lib/storage-broker/src/cf-exec.ts +91 -0
  45. package/payload/platform/lib/storage-broker/src/house-scoped-token.ts +3 -0
  46. package/payload/platform/lib/storage-broker/src/registry.ts +5 -1
  47. package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +19 -19
  48. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +22 -11
  49. package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +19 -19
  50. package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +19 -19
  51. package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
  52. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
  53. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +54 -40
  54. package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +169 -39
  55. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
  56. package/payload/platform/plugins/cloudflare/mcp/dist/index.js +5 -5
  57. package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
  58. package/payload/platform/plugins/cloudflare/mcp/package.json +1 -1
  59. package/payload/platform/plugins/cloudflare/references/hosting-sites.md +2 -0
  60. package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +1 -1
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +21 -19
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
  63. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +18 -12
  64. package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +14 -5
  65. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +2 -0
  66. package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +19 -19
  67. package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +19 -19
  68. package/payload/platform/plugins/docs/references/admin-session.md +3 -3
  69. package/payload/platform/plugins/docs/references/cloudflare.md +2 -0
  70. package/payload/platform/plugins/docs/references/outlook-guide.md +11 -4
  71. package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
  72. package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +19 -19
  73. package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +19 -19
  74. package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +19 -19
  75. package/payload/platform/plugins/memory/PLUGIN.md +1 -1
  76. package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +19 -19
  77. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +2 -2
  78. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
  79. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  80. package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
  81. package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +19 -19
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.d.ts +2 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.d.ts.map +1 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js +238 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js.map +1 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/index.js +9 -5
  87. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  88. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +58 -9
  89. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  90. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +113 -19
  91. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  92. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +5 -2
  93. package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +19 -19
  94. package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +19 -19
  95. package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +19 -19
  96. package/payload/platform/plugins/storage-broker/PLUGIN.md +9 -1
  97. package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +19 -19
  98. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -1
  99. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +16 -5
  100. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -1
  101. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +49 -0
  102. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  103. package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +19 -19
  104. package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +19 -19
  105. package/payload/platform/plugins/voice-mirror/PLUGIN.md +1 -1
  106. package/payload/platform/plugins/voice-mirror/lib/mcp-spawn-tee/index.js +19 -19
  107. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  108. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +98 -9
  109. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  110. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  111. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
  112. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  113. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +110 -0
  114. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +6 -3
  115. package/payload/platform/plugins/web-designer/PLUGIN.md +1 -1
  116. package/payload/platform/plugins/web-designer/commands/website.md +6 -0
  117. package/payload/platform/plugins/web-designer/skills/web-design/references/deploy.md +12 -0
  118. package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +19 -19
  119. package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +19 -19
  120. package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +19 -19
  121. package/payload/platform/scripts/__tests__/logs-rotate.test.sh +260 -0
  122. package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
  123. package/payload/platform/scripts/logs-read.sh +31 -0
  124. package/payload/platform/scripts/logs-rotate.sh +207 -0
  125. package/payload/platform/scripts/resume-tunnel.sh +27 -0
  126. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  127. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  128. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  129. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  130. package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
  131. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  132. package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
  133. package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
  134. package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
  135. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  136. package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
  137. package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
  138. package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
  139. package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
  140. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
  141. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
  142. package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
  143. package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
  144. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  145. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +39 -1
  146. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  147. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -3
  148. package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
  149. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
  150. package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
  151. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
  152. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
  153. package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
  154. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
  155. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
  156. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
  157. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
  158. package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
  159. package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
  160. package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
  161. package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
  162. package/payload/server/{chunk-JXWFVE5X.js → chunk-BBSPFN45.js} +65 -0
  163. package/payload/server/public/assets/{AdminLoginScreens-BFbpkEUS.js → AdminLoginScreens-Bed8SDxY.js} +1 -1
  164. package/payload/server/public/assets/{AdminShell-CdOGObaZ.js → AdminShell-BLh9O6dn.js} +1 -1
  165. package/payload/server/public/assets/{Checkbox-RFD_F0c0.js → Checkbox-BC_KSbpS.js} +1 -1
  166. package/payload/server/public/assets/{admin-CYGdL-1O.js → admin-DfWgJVI3.js} +1 -1
  167. package/payload/server/public/assets/{browser-DVfa_plj.js → browser-C0vkv-hH.js} +1 -1
  168. package/payload/server/public/assets/{calendar-JC_Dxa4n.js → calendar-_pfHw0Hp.js} +1 -1
  169. package/payload/server/public/assets/chat-CufidiBQ.js +1 -0
  170. package/payload/server/public/assets/chevron-left-BnpVBu51.js +1 -0
  171. package/payload/server/public/assets/data-CR-fbPWr.js +1 -0
  172. package/payload/server/public/assets/{graph-hBUrRSkr.js → graph-BZiudUGr.js} +1 -1
  173. package/payload/server/public/assets/{graph-labels-CHh6zs8t.js → graph-labels-BzmmJwCR.js} +1 -1
  174. package/payload/server/public/assets/{maximize-2-DjxE4rm9.js → maximize-2-Bhdsv0U-.js} +1 -1
  175. package/payload/server/public/assets/{operator-BqU9SQuu.js → operator-Cj0B_usT.js} +1 -1
  176. package/payload/server/public/assets/page-84Fdwiu_.js +32 -0
  177. package/payload/server/public/assets/{page-DiQhU6lD.js → page-8AB3jUa_.js} +1 -1
  178. package/payload/server/public/assets/{public-Cu_eALgY.js → public-BkNW9e9T.js} +1 -1
  179. package/payload/server/public/assets/{rotate-ccw-F9DsSTSJ.js → rotate-ccw-D_5cETGt.js} +1 -1
  180. package/payload/server/public/assets/{tasks-CE-D0f50.js → tasks-EyKLXINi.js} +1 -1
  181. package/payload/server/public/assets/{time-entry-format-D2rsWhov.js → time-entry-format-DAwmmR3i.js} +1 -1
  182. package/payload/server/public/assets/{useCopyFeedback-QxOmoobV.js → useCopyFeedback-rOWd_lzl.js} +1 -1
  183. package/payload/server/public/assets/{useSelectionMode-RIzNqFjV.js → useSelectionMode-DDmLmPph.js} +1 -1
  184. package/payload/server/public/assets/{useSubAccountSwitcher-BW_fK1Nt.css → useSubAccountSwitcher-pu0jjLCz.css} +1 -1
  185. package/payload/server/public/assets/{useVoiceRecorder-WrWurSYI.js → useVoiceRecorder-azG8zSUI.js} +1 -1
  186. package/payload/server/public/browser.html +5 -5
  187. package/payload/server/public/calendar.html +6 -6
  188. package/payload/server/public/chat.html +12 -12
  189. package/payload/server/public/data.html +10 -10
  190. package/payload/server/public/graph.html +11 -11
  191. package/payload/server/public/index.html +13 -13
  192. package/payload/server/public/operator.html +14 -14
  193. package/payload/server/public/public.html +12 -12
  194. package/payload/server/public/tasks.html +5 -5
  195. package/payload/server/server.js +581 -209
  196. package/payload/server/{src-4F37OHLK.js → src-YLGAZY36.js} +5 -1
  197. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +0 -73
  198. package/payload/server/public/assets/chat-Yf8Nkop8.js +0 -1
  199. package/payload/server/public/assets/chevron-left-CAiAUBxO.js +0 -1
  200. package/payload/server/public/assets/data-hqdYXaEn.js +0 -1
  201. package/payload/server/public/assets/page-B4c4_4HP.js +0 -32
  202. /package/payload/server/public/assets/{useSubAccountSwitcher-wo_qGW-I.js → useSubAccountSwitcher-CU-lOxTd.js} +0 -0
@@ -13,14 +13,15 @@
13
13
  *
14
14
  * Claude Code CLI → shim (this file) = node running <real-entry> in-process
15
15
  * stdin/stdout : untouched (JSON-RPC channel)
16
- * stderr : process.stderr.write teed → per-date + per-session + passthrough
16
+ * stderr : process.stderr.write teed → raw sink + passthrough
17
17
  *
18
- * Destinations (Task 706) — unchanged:
19
- * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` per-date raw (back-compat;
20
- * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
21
- * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
22
- * lines. Authoritative sink. Absent when SESSION_ID is unset (enumeration
23
- * spawn)then the per-date file is the fallback.
18
+ * Destinations:
19
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — raw stderr + lifecycle lines.
20
+ * The sole raw sink. Task 1721 retired the per-date twin: its date was
21
+ * pinned at spawn, no reader ever parsed it, and mcp-stderr-tee wrote the
22
+ * same file, so every line landed twice.
23
+ * - `${LOG_DIR}/mcp-<name>-nosession.log` — the same, for a spawn with
24
+ * no SESSION_ID (enumeration). One fixed file per server.
24
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
25
26
  * the [mcp-helper] lifecycle lines.
26
27
  *
@@ -49,13 +50,13 @@ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
50
  const ENTRY = process.argv[2];
50
51
  const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
52
  const spawnStamp = Date.now();
52
- // Per-session raw + lifecycle log (Task 706). Empty SESSION_ID (enumeration
53
- // spawn) undefined, and the per-date file is the fallback.
54
- const perSessionPath = LOG_DIR && SESSION_ID
55
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
- : undefined;
57
- const perDatePath = LOG_DIR
58
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
53
+ // The one raw sink (Task 1721). A sessionless spawn (rc-daemon stamps
54
+ // SESSION_ID='' rc-daemon.ts:513 while still stamping LOG_DIR) shares one
55
+ // fixed file per server rather than a date-named one: the date was never read
56
+ // by any reader and named a process lifetime, not a day. Interleaved spawns in
57
+ // the nosession file are separated by the [mcp-helper] op=spawn pid= lines.
58
+ const rawSinkPath = LOG_DIR
59
+ ? (0, node_path_1.resolve)(LOG_DIR, SESSION_ID ? `mcp-${SERVER_NAME}-${SESSION_ID}.log` : `mcp-${SERVER_NAME}-nosession.log`)
59
60
  : undefined;
60
61
  // Rolling tail of entry stderr for op=exit. Capped so a chatty server cannot
61
62
  // grow the buffer without bound.
@@ -66,7 +67,7 @@ let exitEmitted = false;
66
67
  let exitSignal;
67
68
  // The real stderr writer, captured before the tee replaces it. Lifecycle lines
68
69
  // and stderr passthrough both go through this so they are never re-teed into
69
- // the per-date sink nor recursed back into the patched writer.
70
+ // the raw sink nor recursed back into the patched writer.
70
71
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
71
72
  // LOG_DIR is created once, lazily, on the first successful append — not per
72
73
  // chunk. teeWrite runs on the server's own stderr hot path now, so a per-call
@@ -115,7 +116,7 @@ function postToServerLog(suffix, level) {
115
116
  // log-ingest route rejects newlines).
116
117
  function emitLifecycle(suffix, level) {
117
118
  const line = `[mcp-helper] ${suffix}\n`;
118
- appendSafe(perSessionPath, line);
119
+ appendSafe(rawSinkPath, line);
119
120
  try {
120
121
  rawStderrWrite(line);
121
122
  }
@@ -127,12 +128,11 @@ if (!ENTRY) {
127
128
  process.exit(2);
128
129
  }
129
130
  // Replace process.stderr.write with a tee: mirror every server stderr byte to
130
- // the per-date and per-session sinks, keep a rolling tail for op=exit, emit
131
+ // the raw sink, keep a rolling tail for op=exit, emit
131
132
  // op=boot on the first bytes, then pass the write through to the real stderr.
132
133
  const teeWrite = ((...args) => {
133
134
  const chunk = args[0];
134
- appendSafe(perDatePath, chunk); // per-date raw (back-compat)
135
- appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
135
+ appendSafe(rawSinkPath, chunk); // raw sink (Task 1721)
136
136
  const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
137
137
  stderrTail = (stderrTail + text).slice(-TAIL_CAP);
138
138
  if (!bootEmitted) {
@@ -13,14 +13,15 @@
13
13
  *
14
14
  * Claude Code CLI → shim (this file) = node running <real-entry> in-process
15
15
  * stdin/stdout : untouched (JSON-RPC channel)
16
- * stderr : process.stderr.write teed → per-date + per-session + passthrough
16
+ * stderr : process.stderr.write teed → raw sink + passthrough
17
17
  *
18
- * Destinations (Task 706) — unchanged:
19
- * - `${LOG_DIR}/mcp-<name>-stderr-<date>.log` per-date raw (back-compat;
20
- * the in-process mcp-stderr-tee and the [mcp-init-error] probe read it).
21
- * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — per-session raw + lifecycle
22
- * lines. Authoritative sink. Absent when SESSION_ID is unset (enumeration
23
- * spawn)then the per-date file is the fallback.
18
+ * Destinations:
19
+ * - `${LOG_DIR}/mcp-<name>-<SESSION_ID>.log` — raw stderr + lifecycle lines.
20
+ * The sole raw sink. Task 1721 retired the per-date twin: its date was
21
+ * pinned at spawn, no reader ever parsed it, and mcp-stderr-tee wrote the
22
+ * same file, so every line landed twice.
23
+ * - `${LOG_DIR}/mcp-<name>-nosession.log` — the same, for a spawn with
24
+ * no SESSION_ID (enumeration). One fixed file per server.
24
25
  * - `server.log` via the loopback log-ingest route — best-effort mirror of
25
26
  * the [mcp-helper] lifecycle lines.
26
27
  *
@@ -49,13 +50,13 @@ const PLATFORM_PORT = process.env.PLATFORM_PORT;
49
50
  const ENTRY = process.argv[2];
50
51
  const SESSION_ID8 = SESSION_ID ? SESSION_ID.slice(0, 8) : "—";
51
52
  const spawnStamp = Date.now();
52
- // Per-session raw + lifecycle log (Task 706). Empty SESSION_ID (enumeration
53
- // spawn) undefined, and the per-date file is the fallback.
54
- const perSessionPath = LOG_DIR && SESSION_ID
55
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-${SESSION_ID}.log`)
56
- : undefined;
57
- const perDatePath = LOG_DIR
58
- ? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}-stderr-${new Date(spawnStamp).toISOString().slice(0, 10)}.log`)
53
+ // The one raw sink (Task 1721). A sessionless spawn (rc-daemon stamps
54
+ // SESSION_ID='' rc-daemon.ts:513 while still stamping LOG_DIR) shares one
55
+ // fixed file per server rather than a date-named one: the date was never read
56
+ // by any reader and named a process lifetime, not a day. Interleaved spawns in
57
+ // the nosession file are separated by the [mcp-helper] op=spawn pid= lines.
58
+ const rawSinkPath = LOG_DIR
59
+ ? (0, node_path_1.resolve)(LOG_DIR, SESSION_ID ? `mcp-${SERVER_NAME}-${SESSION_ID}.log` : `mcp-${SERVER_NAME}-nosession.log`)
59
60
  : undefined;
60
61
  // Rolling tail of entry stderr for op=exit. Capped so a chatty server cannot
61
62
  // grow the buffer without bound.
@@ -66,7 +67,7 @@ let exitEmitted = false;
66
67
  let exitSignal;
67
68
  // The real stderr writer, captured before the tee replaces it. Lifecycle lines
68
69
  // and stderr passthrough both go through this so they are never re-teed into
69
- // the per-date sink nor recursed back into the patched writer.
70
+ // the raw sink nor recursed back into the patched writer.
70
71
  const rawStderrWrite = process.stderr.write.bind(process.stderr);
71
72
  // LOG_DIR is created once, lazily, on the first successful append — not per
72
73
  // chunk. teeWrite runs on the server's own stderr hot path now, so a per-call
@@ -115,7 +116,7 @@ function postToServerLog(suffix, level) {
115
116
  // log-ingest route rejects newlines).
116
117
  function emitLifecycle(suffix, level) {
117
118
  const line = `[mcp-helper] ${suffix}\n`;
118
- appendSafe(perSessionPath, line);
119
+ appendSafe(rawSinkPath, line);
119
120
  try {
120
121
  rawStderrWrite(line);
121
122
  }
@@ -127,12 +128,11 @@ if (!ENTRY) {
127
128
  process.exit(2);
128
129
  }
129
130
  // Replace process.stderr.write with a tee: mirror every server stderr byte to
130
- // the per-date and per-session sinks, keep a rolling tail for op=exit, emit
131
+ // the raw sink, keep a rolling tail for op=exit, emit
131
132
  // op=boot on the first bytes, then pass the write through to the real stderr.
132
133
  const teeWrite = ((...args) => {
133
134
  const chunk = args[0];
134
- appendSafe(perDatePath, chunk); // per-date raw (back-compat)
135
- appendSafe(perSessionPath, chunk); // per-session raw (Task 706)
135
+ appendSafe(rawSinkPath, chunk); // raw sink (Task 1721)
136
136
  const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
137
137
  stderrTail = (stderrTail + text).slice(-TAIL_CAP);
138
138
  if (!bootEmitted) {
@@ -44,7 +44,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
44
44
  | [api.md](references/api.md) | Cloudflare API library — canonical docs URL + curated endpoint map (DNS, zones, tunnels, token list/create/verify/revoke), the advanced master-token creation walkthrough, the account-scoped secrets-file storage convention, the agent's reuse-a-stable-per-scope-token discipline, and the standing reconcile pass that revokes strays. |
45
45
  | [d1-data-capture.md](references/d1-data-capture.md) | Form → Pages Function → D1 store → read/sweep. The Pages-Edit **and** D1-Edit token-scope requirement, `wrangler d1 create`/`execute --remote`, the `swept` column, plus the establishment discipline: `<accountId>-<purpose>` naming, one store per purpose, and the canonical `acceptances` schema. |
46
46
  | [r2-object-storage.md](references/r2-object-storage.md) | R2 buckets under the same establishment discipline as D1: `<accountId>-<purpose>` naming via `cf-store-name.sh`, one bucket per purpose, `wrangler r2 bucket create`, and the store-provision breadcrumb. |
47
- | [hosting-sites.md](references/hosting-sites.md) | Deploy a static or Next.js site to Cloudflare Pages via `wrangler pages deploy`. |
47
+ | [hosting-sites.md](references/hosting-sites.md) | Deploy a static or Next.js site to Cloudflare Pages via `wrangler pages deploy`. House/single-tenant path: on a multi-tenant install a client account is denied the `pages` scope (`reason=not-house`) and publishes with `storage-broker:storage-pages-deploy` instead. |
48
48
  | [web-analytics.md](references/web-analytics.md) | Why a registered site reports 0 visitors — the RUM API is blocked (no permission group), registered ≠ collecting, edge auto-injection is impossible for a Pages-only custom domain, and the beacon must be injected into the site HTML, rebuilt, and redeployed. Outcome: beacon present in live HTML. |
49
49
  | [dashboard-guide.md](references/dashboard-guide.md) | Click-paths for operations the operator prefers to do by hand — sign in, switch accounts, add a site, edit an apex CNAME, verify nameservers, delete a tunnel, manage CNAME records, author an Access policy. |
50
50
  | [reset-guide.md](references/reset-guide.md) | Decision tree for reset vs. patch, the exact `pkill` incantation for token-mode connectors, and the dashboard cleanup paths for stray records and rogue entries. |
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // Enrol one person into a data portal: mint a passcode, hash it, print the
3
+ // passcode once, and emit the INSERT.
4
+ //
5
+ // This exists because "generate a high-entropy passcode" in a skill's prose is
6
+ // not a constraint — an agent satisfies that sentence with a six-digit number.
7
+ // The strength now lives in code (template/functions/api/_lib/passcode.mjs), and
8
+ // this is the entry point that applies it. Hashing a passcode any other way
9
+ // bypasses the pinned length, so enrolment runs through here.
10
+ //
11
+ // It deliberately stops at emitting SQL. Running `wrangler d1 execute` itself
12
+ // would put the script's only path behind a Cloudflare credential and a network,
13
+ // which makes it untestable; the skill pipes stdout instead.
14
+ //
15
+ // The stream split is load-bearing, not cosmetic. stdout is SQL and NOTHING else,
16
+ // so it pipes straight into wrangler. The passcode goes to stderr: the operator
17
+ // still reads it off their terminal, but no pipe can carry it into wrangler,
18
+ // which would echo it back inside a syntax error — a plaintext passcode in a log
19
+ // is the one thing enrolment must never produce.
20
+ //
21
+ // Usage:
22
+ // node platform/plugins/cloudflare/bin/portal-enrol.mjs --owner <ownerId> --name <name> \
23
+ // | npx wrangler d1 execute <db> --remote --file=-
24
+
25
+ import {
26
+ PASSCODE_LENGTH,
27
+ generatePasscode,
28
+ hashPasscode,
29
+ randomSaltHex,
30
+ } from '../skills/data-portal/template/functions/api/_lib/passcode.mjs'
31
+
32
+ /** The isolation rule is that an R2 key starts with `<ownerId>/`, so an ownerId
33
+ * containing a separator would let one person read another's files. The gate
34
+ * enforces this too; enrolment refuses rather than minting a row the gate will
35
+ * later reject. */
36
+ const OWNER_ID = /^[a-z0-9-]+$/
37
+
38
+ /** @param {string} message */
39
+ function die(message) {
40
+ console.error(`portal-enrol: ${message}`)
41
+ process.exit(1)
42
+ }
43
+
44
+ /** @param {string} flag @returns {string | undefined} */
45
+ function arg(flag) {
46
+ const i = process.argv.indexOf(flag)
47
+ return i === -1 ? undefined : process.argv[i + 1]
48
+ }
49
+
50
+ const ownerId = arg('--owner')
51
+ const name = arg('--name')
52
+
53
+ if (!ownerId) die('--owner <id> is required')
54
+ if (!name) die('--name <name> is required')
55
+ if (!OWNER_ID.test(/** @type {string} */ (ownerId))) {
56
+ die(`--owner must match ${OWNER_ID.source} — the isolation gate rejects anything else`)
57
+ }
58
+ // A newline in the name would split the one statement stdout promises to be.
59
+ // It is not an injection — SQLite has no escape but '' and a newline sits inside
60
+ // the literal harmlessly — but it breaks the pipeable contract, so refuse it.
61
+ if (/[\r\n]/.test(/** @type {string} */ (name))) die('--name cannot contain a line break')
62
+
63
+ const saltBytes = new Uint8Array(16)
64
+ crypto.getRandomValues(saltBytes)
65
+ const saltHex = randomSaltHex(saltBytes)
66
+
67
+ const passcode = generatePasscode()
68
+ const hash = await hashPasscode(passcode, saltHex)
69
+
70
+ // Doubled single quotes: the only SQL literal here not already constrained to a
71
+ // charset is the name, which comes from an operator.
72
+ const sqlName = /** @type {string} */ (name).replace(/'/g, "''")
73
+ const createdAt = new Date().toISOString()
74
+
75
+ // stdout: the statement, alone, pipeable.
76
+ //
77
+ // An upsert, not a bare INSERT, because `ownerId` is TEXT PRIMARY KEY and
78
+ // re-running this script for an existing person IS the rotation path — the only
79
+ // one there is. A bare INSERT dies on the UNIQUE violation inside wrangler, a
80
+ // separate process, AFTER this script has already printed a new passcode and
81
+ // exited 0: the operator hands out a passcode that never worked, believing the
82
+ // leaked one is revoked, while it and its sessions keep resolving. Rotation is
83
+ // precisely the case where a remedy that silently no-ops is worse than none.
84
+ //
85
+ // `createdAt` is deliberately absent from the DO UPDATE: it records when the
86
+ // person was enrolled, and rotating a passcode is not creating a person.
87
+ console.log(
88
+ `INSERT INTO people (ownerId, name, salt, hash, createdAt) VALUES ('${ownerId}', '${sqlName}', '${saltHex}', '${hash}', '${createdAt}') ON CONFLICT(ownerId) DO UPDATE SET name=excluded.name, salt=excluded.salt, hash=excluded.hash;`,
89
+ )
90
+
91
+ // stderr: everything for the human. Never stdout — see the note at the top.
92
+ console.error(`passcode: ${passcode}`)
93
+ console.error('')
94
+ console.error(
95
+ `Give the passcode to ${name} out of band, and do not store it anywhere. It is ${PASSCODE_LENGTH} characters and it is not recoverable — re-running this rotates it, which also revokes their live sessions.`,
96
+ )
@@ -1,10 +1,16 @@
1
1
  import { describe, it, expect, vi, afterEach } from 'vitest'
2
2
  import { processAuth } from '../../skills/data-portal/template/functions/api/auth'
3
- import { hashPasscode } from '../../skills/data-portal/template/functions/api/_lib/passcode'
3
+ import {
4
+ generatePasscode,
5
+ hashPasscode,
6
+ } from '../../skills/data-portal/template/functions/api/_lib/passcode.mjs'
4
7
  import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
5
8
 
6
9
  const SALT = '000102030405060708090a0b0c0d0e0f'
7
- const ITER = 1000
10
+ // Real passcodes, because hashPasscode now refuses anything below the pinned
11
+ // strength — 'right' and 'wrong' would throw before they ever reached a route.
12
+ const RIGHT = generatePasscode()
13
+ const WRONG = generatePasscode()
8
14
  const IP = '203.0.113.7'
9
15
 
10
16
  async function envWith(people: Array<{ ownerId: string; passcode: string }>, attemptCount = 0) {
@@ -12,8 +18,7 @@ async function envWith(people: Array<{ ownerId: string; passcode: string }>, att
12
18
  people.map(async (p) => ({
13
19
  ownerId: p.ownerId,
14
20
  salt: SALT,
15
- hash: await hashPasscode(p.passcode, SALT, ITER),
16
- iterations: ITER,
21
+ hash: await hashPasscode(p.passcode, SALT),
17
22
  })),
18
23
  )
19
24
  const sessions: unknown[][] = []
@@ -57,10 +62,10 @@ afterEach(() => vi.restoreAllMocks())
57
62
 
58
63
  describe('processAuth', () => {
59
64
  it('accepts the right passcode and mints a session', async () => {
60
- const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
65
+ const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
61
66
  const lines: string[] = []
62
67
  const res = await processAuth(
63
- { ownerId: 'alice', passcode: 'right' },
68
+ { ownerId: 'alice', passcode: RIGHT },
64
69
  env,
65
70
  (l) => lines.push(l),
66
71
  () => 'sess-1',
@@ -74,10 +79,10 @@ describe('processAuth', () => {
74
79
  })
75
80
 
76
81
  it('denies the wrong passcode and mints no session', async () => {
77
- const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
82
+ const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
78
83
  const lines: string[] = []
79
84
  const res = await processAuth(
80
- { ownerId: 'alice', passcode: 'wrong' },
85
+ { ownerId: 'alice', passcode: WRONG },
81
86
  env,
82
87
  (l) => lines.push(l),
83
88
  () => 'sess-1',
@@ -90,9 +95,9 @@ describe('processAuth', () => {
90
95
  })
91
96
 
92
97
  it('denies an unknown owner without revealing that it is unknown', async () => {
93
- const { env } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
98
+ const { env } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
94
99
  const res = await processAuth(
95
- { ownerId: 'nobody', passcode: 'right' },
100
+ { ownerId: 'nobody', passcode: RIGHT },
96
101
  env,
97
102
  () => {},
98
103
  () => 'sess-1',
@@ -106,10 +111,10 @@ describe('processAuth', () => {
106
111
  })
107
112
 
108
113
  it('rate-limits before comparing the passcode', async () => {
109
- const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: 'right' }], 5)
114
+ const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: RIGHT }], 5)
110
115
  const lines: string[] = []
111
116
  const res = await processAuth(
112
- { ownerId: 'alice', passcode: 'right' },
117
+ { ownerId: 'alice', passcode: RIGHT },
113
118
  env,
114
119
  (l) => lines.push(l),
115
120
  () => 'sess-1',
@@ -122,63 +127,72 @@ describe('processAuth', () => {
122
127
  })
123
128
 
124
129
  it('never logs the passcode', async () => {
125
- const { env } = await envWith([{ ownerId: 'alice', passcode: 'sup3rsecret' }])
130
+ const { env } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
126
131
  const lines: string[] = []
127
132
  await processAuth(
128
- { ownerId: 'alice', passcode: 'sup3rsecret' },
133
+ { ownerId: 'alice', passcode: RIGHT },
129
134
  env,
130
135
  (l) => lines.push(l),
131
136
  () => 'sess-1',
132
137
  1_000_000,
133
138
  IP,
134
139
  )
135
- expect(lines.join('\n')).not.toContain('sup3rsecret')
140
+ expect(lines.join('\n')).not.toContain(RIGHT)
136
141
  })
137
142
 
138
143
  it('rejects a malformed body', async () => {
139
- const { env } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
144
+ const { env } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
140
145
  const res = await processAuth({} as never, env, () => {}, () => 'sess-1', 1_000_000, IP)
141
146
  expect(res.status).toBe(400)
142
147
  })
143
148
 
144
- it('derives a passcode even for an unknown owner, closing the timing oracle', async () => {
149
+ it('hashes for an unknown owner too, closing the timing oracle', async () => {
145
150
  // The identical DENIED payload is cosmetic on its own: an unknown owner
146
- // returning after one D1 read (~5ms) while a known one derives PBKDF2
147
- // (~250ms) enumerates the enrolled people outright. Both paths must pay.
148
- const spy = vi.spyOn(crypto.subtle, 'deriveBits')
149
-
150
- const known = await envWith([{ ownerId: 'alice', passcode: 'right' }])
151
- await processAuth({ ownerId: 'alice', passcode: 'x' }, known.env, () => {}, () => 's', 1_000_000, IP)
152
- const derivesForKnown = spy.mock.calls.length
151
+ // returning after one D1 read while a known one also hashes enumerates the
152
+ // enrolled people outright. Both paths must pay.
153
+ //
154
+ // 1689 closed a ~5ms-vs-250ms gap here. 1708 replaced PBKDF2 with one
155
+ // SHA-256, shrinking the gap to a measured 43.8us — still inside the band
156
+ // remote timing attacks have been shown to resolve, so the dummy stays. A
157
+ // call count is the right instrument: it is deterministic where a clock is
158
+ // not, and it fails if someone drops the dummy without restoring the cost.
159
+ // Both envs are built BEFORE the spy: envWith hashes its own fixtures, and
160
+ // counting that setup work would let the two totals match without the route
161
+ // hashing at all.
162
+ const known = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
163
+ const unknown = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
164
+ const spy = vi.spyOn(crypto.subtle, 'digest')
165
+
166
+ await processAuth({ ownerId: 'alice', passcode: WRONG }, known.env, () => {}, () => 's', 1_000_000, IP)
167
+ const hashesForKnown = spy.mock.calls.length
153
168
  spy.mockClear()
154
169
 
155
- const unknown = await envWith([{ ownerId: 'alice', passcode: 'right' }])
156
- await processAuth({ ownerId: 'nobody', passcode: 'x' }, unknown.env, () => {}, () => 's', 1_000_000, IP)
157
- const derivesForUnknown = spy.mock.calls.length
170
+ await processAuth({ ownerId: 'nobody', passcode: WRONG }, unknown.env, () => {}, () => 's', 1_000_000, IP)
171
+ const hashesForUnknown = spy.mock.calls.length
158
172
 
159
- expect(derivesForKnown).toBeGreaterThan(0)
160
- expect(derivesForUnknown).toBe(derivesForKnown)
173
+ expect(hashesForKnown).toBeGreaterThan(0)
174
+ expect(hashesForUnknown).toBe(hashesForKnown)
161
175
  })
162
176
 
163
177
  it('scopes the rate limiter on owner AND ip, so one caller cannot lock out another', async () => {
164
178
  // Keyed on the owner alone the counter is a weapon: six unauthenticated
165
179
  // requests naming a person lock that person out of their own portal.
166
- const { env, scopes } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
167
- await processAuth({ ownerId: 'alice', passcode: 'right' }, env, () => {}, () => 's', 1_000_000, IP)
180
+ const { env, scopes } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
181
+ await processAuth({ ownerId: 'alice', passcode: RIGHT }, env, () => {}, () => 's', 1_000_000, IP)
168
182
  expect(scopes[0]).toBe(`alice|${IP}`)
169
183
  expect(String(scopes[0])).toContain(IP)
170
184
  })
171
185
 
172
186
  it('clears the counter on a successful sign-in, so a person cannot self-lock', async () => {
173
- const { env, cleared } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
174
- await processAuth({ ownerId: 'alice', passcode: 'right' }, env, () => {}, () => 's', 1_000_000, IP)
187
+ const { env, cleared } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
188
+ await processAuth({ ownerId: 'alice', passcode: RIGHT }, env, () => {}, () => 's', 1_000_000, IP)
175
189
  expect(cleared.length).toBe(1)
176
190
  expect(cleared[0][0]).toBe(`alice|${IP}`)
177
191
  })
178
192
 
179
193
  it('does not clear the counter on a failed sign-in', async () => {
180
- const { env, cleared } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
181
- await processAuth({ ownerId: 'alice', passcode: 'wrong' }, env, () => {}, () => 's', 1_000_000, IP)
194
+ const { env, cleared } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
195
+ await processAuth({ ownerId: 'alice', passcode: WRONG }, env, () => {}, () => 's', 1_000_000, IP)
182
196
  expect(cleared.length).toBe(0)
183
197
  })
184
198
 
@@ -186,18 +200,18 @@ describe('processAuth', () => {
186
200
  // sessions carries pcCheck; resolveSession joins people and requires it to
187
201
  // still match. Rotation changes the hash, so old sessions die by
188
202
  // construction rather than by an operator remembering a cleanup step.
189
- const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
190
- await processAuth({ ownerId: 'alice', passcode: 'right' }, env, () => {}, () => 's', 1_000_000, IP)
191
- const expected = (await hashPasscode('right', SALT, ITER)).slice(0, 16)
203
+ const { env, sessions } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
204
+ await processAuth({ ownerId: 'alice', passcode: RIGHT }, env, () => {}, () => 's', 1_000_000, IP)
205
+ const expected = (await hashPasscode(RIGHT, SALT)).slice(0, 16)
192
206
  // bind order: sessionId, ownerId, pcCheck, expiresAt, createdAt
193
207
  expect(sessions[0][2]).toBe(expected)
194
208
  })
195
209
 
196
210
  it('quotes the owner in log lines, so a crafted ownerId cannot forge one', async () => {
197
- const { env } = await envWith([{ ownerId: 'alice', passcode: 'right' }])
211
+ const { env } = await envWith([{ ownerId: 'alice', passcode: RIGHT }])
198
212
  const lines: string[] = []
199
213
  const forged = 'x result=ok\n[data-portal] op=auth owner=alice result=ok'
200
- await processAuth({ ownerId: forged, passcode: 'y' }, env, (l) => lines.push(l), () => 's', 1_000_000, IP)
214
+ await processAuth({ ownerId: forged, passcode: WRONG }, env, (l) => lines.push(l), () => 's', 1_000_000, IP)
201
215
  // Exactly one line, and the injected newline must not have started another.
202
216
  expect(lines.length).toBe(1)
203
217
  expect(lines[0].split('\n').length).toBe(1)