@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.
- package/dist/__tests__/cron-heartbeat-registration.test.js +57 -28
- package/dist/__tests__/launchd-plist.test.js +26 -0
- package/dist/cron-registration.js +30 -8
- package/dist/index.js +50 -9
- package/dist/launchd-plist.js +1 -1
- package/dist/uninstall.js +9 -3
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.d.ts +2 -1
- package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.js +14 -22
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/index.ts +15 -22
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts +8 -7
- package/payload/platform/lib/mcp-spawn-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js +19 -19
- package/payload/platform/lib/mcp-spawn-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-spawn-tee/src/__tests__/spawn-tee.test.ts +34 -5
- package/payload/platform/lib/mcp-spawn-tee/src/index.ts +22 -19
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts +35 -17
- package/payload/platform/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js +41 -48
- package/payload/platform/lib/mcp-stderr-tee/dist/index.js.map +1 -1
- package/payload/platform/lib/mcp-stderr-tee/src/index.ts +42 -51
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +27 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +49 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +11 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.js +16 -0
- package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +18 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +77 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.js +3 -0
- package/payload/platform/lib/storage-broker/dist/house-scoped-token.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/registry.d.ts +1 -1
- package/payload/platform/lib/storage-broker/dist/registry.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/registry.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +32 -1
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +61 -0
- package/payload/platform/lib/storage-broker/src/audit.ts +45 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +91 -0
- package/payload/platform/lib/storage-broker/src/house-scoped-token.ts +3 -0
- package/payload/platform/lib/storage-broker/src/registry.ts +5 -1
- package/payload/platform/plugins/admin/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +22 -11
- package/payload/platform/plugins/aeo/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/browser/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +96 -0
- package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +54 -40
- package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +169 -39
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +207 -0
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js +5 -5
- package/payload/platform/plugins/cloudflare/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/package.json +1 -1
- package/payload/platform/plugins/cloudflare/references/hosting-sites.md +2 -0
- package/payload/platform/plugins/cloudflare/skills/calendar-site/SKILL.md +1 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +21 -19
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.mjs +193 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +18 -12
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +14 -5
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +2 -0
- package/payload/platform/plugins/connector/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/contacts/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/docs/references/admin-session.md +3 -3
- package/payload/platform/plugins/docs/references/cloudflare.md +2 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +11 -4
- package/payload/platform/plugins/docs/references/plugins-guide.md +8 -6
- package/payload/platform/plugins/email/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/filesystem/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/memory/PLUGIN.md +1 -1
- package/payload/platform/plugins/memory/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -1
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +2 -2
- package/payload/platform/plugins/outlook/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js +238 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-threaded.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +9 -5
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +58 -9
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +113 -19
- package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
- package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +5 -2
- package/payload/platform/plugins/quickbooks/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/replicate/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/scheduling/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/storage-broker/PLUGIN.md +9 -1
- package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -1
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +16 -5
- package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -1
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js +49 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/telegram/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/url-get/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/voice-mirror/PLUGIN.md +1 -1
- package/payload/platform/plugins/voice-mirror/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +98 -9
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js +54 -21
- package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +110 -0
- package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +6 -3
- package/payload/platform/plugins/web-designer/PLUGIN.md +1 -1
- package/payload/platform/plugins/web-designer/commands/website.md +6 -0
- package/payload/platform/plugins/web-designer/skills/web-design/references/deploy.md +12 -0
- package/payload/platform/plugins/whatsapp/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/work/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/plugins/workflows/lib/mcp-spawn-tee/index.js +19 -19
- package/payload/platform/scripts/__tests__/logs-rotate.test.sh +260 -0
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +79 -0
- package/payload/platform/scripts/logs-read.sh +31 -0
- package/payload/platform/scripts/logs-rotate.sh +207 -0
- package/payload/platform/scripts/resume-tunnel.sh +27 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +41 -5
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +22 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts +2 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/install-start-counter.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +122 -9
- package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-census.js +214 -29
- package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +39 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +5 -3
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +13 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js +4 -2
- package/payload/platform/services/claude-session-manager/dist/session-memory-cap.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +150 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +333 -0
- package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js +55 -0
- package/payload/platform/services/claude-session-manager/dist/start-counter.js.map +1 -0
- package/payload/server/{chunk-JXWFVE5X.js → chunk-BBSPFN45.js} +65 -0
- package/payload/server/public/assets/{AdminLoginScreens-BFbpkEUS.js → AdminLoginScreens-Bed8SDxY.js} +1 -1
- package/payload/server/public/assets/{AdminShell-CdOGObaZ.js → AdminShell-BLh9O6dn.js} +1 -1
- package/payload/server/public/assets/{Checkbox-RFD_F0c0.js → Checkbox-BC_KSbpS.js} +1 -1
- package/payload/server/public/assets/{admin-CYGdL-1O.js → admin-DfWgJVI3.js} +1 -1
- package/payload/server/public/assets/{browser-DVfa_plj.js → browser-C0vkv-hH.js} +1 -1
- package/payload/server/public/assets/{calendar-JC_Dxa4n.js → calendar-_pfHw0Hp.js} +1 -1
- package/payload/server/public/assets/chat-CufidiBQ.js +1 -0
- package/payload/server/public/assets/chevron-left-BnpVBu51.js +1 -0
- package/payload/server/public/assets/data-CR-fbPWr.js +1 -0
- package/payload/server/public/assets/{graph-hBUrRSkr.js → graph-BZiudUGr.js} +1 -1
- package/payload/server/public/assets/{graph-labels-CHh6zs8t.js → graph-labels-BzmmJwCR.js} +1 -1
- package/payload/server/public/assets/{maximize-2-DjxE4rm9.js → maximize-2-Bhdsv0U-.js} +1 -1
- package/payload/server/public/assets/{operator-BqU9SQuu.js → operator-Cj0B_usT.js} +1 -1
- package/payload/server/public/assets/page-84Fdwiu_.js +32 -0
- package/payload/server/public/assets/{page-DiQhU6lD.js → page-8AB3jUa_.js} +1 -1
- package/payload/server/public/assets/{public-Cu_eALgY.js → public-BkNW9e9T.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-F9DsSTSJ.js → rotate-ccw-D_5cETGt.js} +1 -1
- package/payload/server/public/assets/{tasks-CE-D0f50.js → tasks-EyKLXINi.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-D2rsWhov.js → time-entry-format-DAwmmR3i.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-QxOmoobV.js → useCopyFeedback-rOWd_lzl.js} +1 -1
- package/payload/server/public/assets/{useSelectionMode-RIzNqFjV.js → useSelectionMode-DDmLmPph.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-BW_fK1Nt.css → useSubAccountSwitcher-pu0jjLCz.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-WrWurSYI.js → useVoiceRecorder-azG8zSUI.js} +1 -1
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +12 -12
- package/payload/server/public/data.html +10 -10
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +13 -13
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +12 -12
- package/payload/server/public/tasks.html +5 -5
- package/payload/server/server.js +581 -209
- package/payload/server/{src-4F37OHLK.js → src-YLGAZY36.js} +5 -1
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +0 -73
- package/payload/server/public/assets/chat-Yf8Nkop8.js +0 -1
- package/payload/server/public/assets/chevron-left-CAiAUBxO.js +0 -1
- package/payload/server/public/assets/data-hqdYXaEn.js +0 -1
- package/payload/server/public/assets/page-B4c4_4HP.js +0 -32
- /package/payload/server/public/assets/{useSubAccountSwitcher-wo_qGW-I.js → useSubAccountSwitcher-CU-lOxTd.js} +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Passcode generation, strength and hashing for the data portal.
|
|
2
|
+
//
|
|
3
|
+
// This module is JavaScript, not TypeScript, and that is deliberate: it has two
|
|
4
|
+
// consumers on two runtimes. The Worker imports it (wrangler bundles JS), and
|
|
5
|
+
// enrolment imports it from bare node on-device, which cannot load .ts. A second
|
|
6
|
+
// copy of the hash for the enrolment side would drift, and the failure mode of
|
|
7
|
+
// drift is that enrolment writes hashes the Worker rejects. JSDoc plus
|
|
8
|
+
// `typecheck:templates --checkJs` keeps the type cover 1689 added.
|
|
9
|
+
//
|
|
10
|
+
// constantTimeEqual is hand-rolled deliberately: crypto.subtle.timingSafeEqual
|
|
11
|
+
// is a Cloudflare extension with no Node equivalent, so depending on it would
|
|
12
|
+
// make the comparison untestable here.
|
|
13
|
+
|
|
14
|
+
/** Crockford base32: digits and letters minus I, L, O and U, which are the ones
|
|
15
|
+
* misread when the operator conveys a passcode to a person out of band. 32
|
|
16
|
+
* symbols is exactly 5 bits, and 256 divides by 32, so a masked random byte is
|
|
17
|
+
* an unbiased draw with no rejection sampling.
|
|
18
|
+
*
|
|
19
|
+
* How much of Crockford this portal takes, stated so nobody has to infer it
|
|
20
|
+
* from behaviour again (Task 1713):
|
|
21
|
+
*
|
|
22
|
+
* ADOPTED — the excluded letters, above, and case-insensitivity on decode.
|
|
23
|
+
* The alphabet is uppercase and so is everything stored;
|
|
24
|
+
* verifyPasscode uppercases what it is given, so a person may type
|
|
25
|
+
* either case. That is the one normalisation point.
|
|
26
|
+
* DECLINED — the letter-to-digit decode (I and L to 1, O to 0), check
|
|
27
|
+
* symbols, and hyphen tolerance. None are implemented.
|
|
28
|
+
*
|
|
29
|
+
* So "Crockford" here means this alphabet plus the case rule, and nothing
|
|
30
|
+
* further. 1708 shipped the first half without the second, which is the gap
|
|
31
|
+
* 1713 closed; a future reader adding the rest should treat it as a new
|
|
32
|
+
* decision, not as finishing this one.
|
|
33
|
+
*/
|
|
34
|
+
export const PASSCODE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'
|
|
35
|
+
|
|
36
|
+
/** 26 x 5 bits = 130, which clears the 128-bit floor this portal's security
|
|
37
|
+
* rests on. Changing this constant changes that guarantee: see the note at
|
|
38
|
+
* hashPasscode before touching it. */
|
|
39
|
+
export const PASSCODE_LENGTH = 26
|
|
40
|
+
|
|
41
|
+
/** What PASSCODE_LENGTH and PASSCODE_ALPHABET buy, stated once so a test can
|
|
42
|
+
* pin it rather than a comment asserting it. */
|
|
43
|
+
export const PASSCODE_ENTROPY_BITS = PASSCODE_LENGTH * 5
|
|
44
|
+
|
|
45
|
+
/** Enrolment refused a passcode below the pinned strength. Named rather than a
|
|
46
|
+
* bare Error so a caller can tell a weak passcode from a broken salt. */
|
|
47
|
+
export class PasscodeTooWeakError extends Error {
|
|
48
|
+
/** @param {string} detail */
|
|
49
|
+
constructor(detail) {
|
|
50
|
+
super(`passcode too weak: ${detail}`)
|
|
51
|
+
this.name = 'PasscodeTooWeakError'
|
|
52
|
+
/** @type {'passcode-too-weak'} */
|
|
53
|
+
this.reason = 'passcode-too-weak'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Mint a passcode. This is the ONLY source of passcode entropy in the portal —
|
|
58
|
+
* assertPasscodeStrength below cannot supply it, only check the shape.
|
|
59
|
+
* @returns {string} */
|
|
60
|
+
export function generatePasscode() {
|
|
61
|
+
const bytes = new Uint8Array(PASSCODE_LENGTH)
|
|
62
|
+
crypto.getRandomValues(bytes)
|
|
63
|
+
let out = ''
|
|
64
|
+
// & 31 is unbiased here precisely because the alphabet is 32 long.
|
|
65
|
+
for (const b of bytes) out += PASSCODE_ALPHABET[b & 31]
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Refuse a passcode that is not the pinned length drawn from the pinned
|
|
70
|
+
* alphabet.
|
|
71
|
+
*
|
|
72
|
+
* This is a STRUCTURAL check and nothing more. It cannot measure entropy: a
|
|
73
|
+
* 26-character run of 'A' passes it. Do not describe it as enforcing
|
|
74
|
+
* PASSCODE_ENTROPY_BITS — an arbitrary string's entropy is not a computable
|
|
75
|
+
* property of the string. What it does buy is the case that motivated it: an
|
|
76
|
+
* enrolling agent satisfying "high-entropy passcode" with a six-digit number
|
|
77
|
+
* can no longer store one. Entropy itself comes from generatePasscode.
|
|
78
|
+
* @param {string} passcode
|
|
79
|
+
* @returns {void} */
|
|
80
|
+
export function assertPasscodeStrength(passcode) {
|
|
81
|
+
if (typeof passcode !== 'string' || passcode.length !== PASSCODE_LENGTH) {
|
|
82
|
+
throw new PasscodeTooWeakError(`expected ${PASSCODE_LENGTH} characters`)
|
|
83
|
+
}
|
|
84
|
+
for (const c of passcode) {
|
|
85
|
+
if (!PASSCODE_ALPHABET.includes(c)) {
|
|
86
|
+
throw new PasscodeTooWeakError('character outside the pinned alphabet')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @param {Uint8Array} bytes @returns {string} */
|
|
92
|
+
export function randomSaltHex(bytes) {
|
|
93
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Returns Uint8Array<ArrayBuffer>, not the bare Uint8Array: the bare form
|
|
97
|
+
* widens to ArrayBufferLike, which Web Crypto will not accept as a BufferSource
|
|
98
|
+
* because a SharedArrayBuffer could back it.
|
|
99
|
+
* @param {string} hex
|
|
100
|
+
* @returns {Uint8Array<ArrayBuffer> | null} */
|
|
101
|
+
function hexToBytes(hex) {
|
|
102
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null
|
|
103
|
+
const out = new Uint8Array(hex.length / 2)
|
|
104
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
|
105
|
+
return out
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Hash a passcode for storage: one SHA-256 over salt || passcode.
|
|
109
|
+
*
|
|
110
|
+
* ONE fast hash, no key stretching, and that is safe ONLY because passcodes are
|
|
111
|
+
* machine-generated by generatePasscode at PASSCODE_LENGTH. Stretching exists to
|
|
112
|
+
* make guessing a HUMAN-CHOSEN password expensive; 130 random bits are
|
|
113
|
+
* unguessable however fast the hash is, which is why nobody stretches a
|
|
114
|
+
* magic-link token either.
|
|
115
|
+
*
|
|
116
|
+
* So: if anyone ever lets an operator choose a memorable passcode, key
|
|
117
|
+
* stretching must come back in the SAME change. The danger here is not the fast
|
|
118
|
+
* hash. It is a later relaxation of generated-only while the fast hash stays.
|
|
119
|
+
*
|
|
120
|
+
* The salt earns its keep even without stretching: two people holding the same
|
|
121
|
+
* passcode still get different stored hashes, and no precomputed table over the
|
|
122
|
+
* alphabet is reusable across rows.
|
|
123
|
+
* @param {string} passcode
|
|
124
|
+
* @param {string} saltHex
|
|
125
|
+
* @returns {Promise<string>} */
|
|
126
|
+
export async function hashPasscode(passcode, saltHex) {
|
|
127
|
+
assertPasscodeStrength(passcode)
|
|
128
|
+
const salt = hexToBytes(saltHex)
|
|
129
|
+
if (!salt) throw new Error('salt must be hex')
|
|
130
|
+
const encoded = new TextEncoder().encode(passcode)
|
|
131
|
+
const message = new Uint8Array(salt.length + encoded.length)
|
|
132
|
+
message.set(salt, 0)
|
|
133
|
+
message.set(encoded, salt.length)
|
|
134
|
+
const digest = await crypto.subtle.digest('SHA-256', message)
|
|
135
|
+
return randomSaltHex(new Uint8Array(digest))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Verify a passcode against a stored salt and hash. Returns false rather than
|
|
139
|
+
* throwing on any malformed input, including a passcode that would not pass
|
|
140
|
+
* enrolment: sign-in is not the place to crash over a bad row.
|
|
141
|
+
* @param {string} passcode
|
|
142
|
+
* @param {string} saltHex
|
|
143
|
+
* @param {string} expectedHashHex
|
|
144
|
+
* @returns {Promise<boolean>} */
|
|
145
|
+
export async function verifyPasscode(passcode, saltHex, expectedHashHex) {
|
|
146
|
+
const expected = hexToBytes(expectedHashHex)
|
|
147
|
+
if (!expected) return false
|
|
148
|
+
/** @type {string} */
|
|
149
|
+
let actualHex
|
|
150
|
+
try {
|
|
151
|
+
// Crockford is case-insensitive on decode, and 1708 chose this alphabet for
|
|
152
|
+
// the very reason that rule exists: a person transcribes the passcode by
|
|
153
|
+
// hand. Uppercasing here is the second half of that choice (Task 1713), so
|
|
154
|
+
// the case someone types stops being something they can get wrong.
|
|
155
|
+
//
|
|
156
|
+
// This is the ONE normalisation point. hashPasscode and
|
|
157
|
+
// assertPasscodeStrength stay strict on the canonical uppercase form, so
|
|
158
|
+
// what enrolment stores is unchanged and only accepted INPUT widens.
|
|
159
|
+
//
|
|
160
|
+
// Inside the try deliberately: a malformed PASSCODE must return false rather
|
|
161
|
+
// than throw, and .toUpperCase() throws on a non-string. Outside the try,
|
|
162
|
+
// that would break for exactly the input class it exists to cover.
|
|
163
|
+
// (Scoped to the passcode on purpose. The docblock above says "any
|
|
164
|
+
// malformed input", which is not quite true of expectedHashHex — a null one
|
|
165
|
+
// throws out of hexToBytes before this try is entered. Unreachable today,
|
|
166
|
+
// since schema.sql pins hash TEXT NOT NULL; tracked in Task 1718 rather
|
|
167
|
+
// than restated here as though it held.)
|
|
168
|
+
//
|
|
169
|
+
// The strength gate therefore applies to the normalised form: verification
|
|
170
|
+
// accepts any string whose uppercase form is a valid passcode. One
|
|
171
|
+
// consequence, left unguarded on purpose — 'ß' uppercases to 'SS', so a
|
|
172
|
+
// 25-character string containing it can normalise onto a 26-character
|
|
173
|
+
// passcode. Building one requires already knowing the passcode, so it costs
|
|
174
|
+
// nothing an attacker could spend, and a fourth check to close it would buy
|
|
175
|
+
// no security.
|
|
176
|
+
actualHex = await hashPasscode(passcode.toUpperCase(), saltHex)
|
|
177
|
+
} catch {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
const actual = hexToBytes(actualHex)
|
|
181
|
+
if (!actual) return false
|
|
182
|
+
return constantTimeEqual(actual, expected)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** @param {Uint8Array} a @param {Uint8Array} b @returns {boolean} */
|
|
186
|
+
export function constantTimeEqual(a, b) {
|
|
187
|
+
// Length is not secret; an early return on it leaks nothing a caller cannot
|
|
188
|
+
// already see. The byte loop below never short-circuits.
|
|
189
|
+
if (a.length !== b.length) return false
|
|
190
|
+
let diff = 0
|
|
191
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]
|
|
192
|
+
return diff === 0
|
|
193
|
+
}
|
package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Handler, Logger, PortalEnv } from './_lib/types'
|
|
2
|
-
import {
|
|
2
|
+
import { verifyPasscode } from './_lib/passcode.mjs'
|
|
3
3
|
import { checkAndRecordAttempt, clearAttempts, rateScope } from './_lib/ratelimit'
|
|
4
4
|
import { SESSION_COOKIE, SESSION_TTL_MS, mintSession, passcodeCheck } from './_lib/session'
|
|
5
5
|
import { q } from './_lib/log'
|
|
@@ -17,11 +17,19 @@ interface AuthResult extends Handler {
|
|
|
17
17
|
// them would enumerate who is enrolled.
|
|
18
18
|
const DENIED: Handler = { status: 401, payload: { ok: false, error: 'denied' } }
|
|
19
19
|
|
|
20
|
-
// A real-shaped salt and hash used only to burn the same
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
20
|
+
// A real-shaped salt and hash used only to burn the same hash when the owner
|
|
21
|
+
// does not exist, so the two paths cost the same and the identical DENIED
|
|
22
|
+
// payload above is not merely cosmetic.
|
|
23
|
+
//
|
|
24
|
+
// Task 1708 re-measured this after replacing PBKDF2 with one SHA-256, because
|
|
25
|
+
// the gap it closes shrank by four orders of magnitude and the dummy might have
|
|
26
|
+
// stopped earning its place. It did not. Without it the delta is one hash —
|
|
27
|
+
// measured at 43.8us (dev Mac, off-platform) — and published remote-timing work
|
|
28
|
+
// resolves roughly 15-20us on a LAN and ~100us over the internet, so 43.8us sits
|
|
29
|
+
// inside the demonstrated-resolvable band, and an attacker gets to pick a
|
|
30
|
+
// Cloudflare PoP near the target. Keeping it costs 43.8us against a 10ms CPU
|
|
31
|
+
// budget, which is 0.4% of it. Cheap enough not to think about; measurable
|
|
32
|
+
// enough not to remove.
|
|
25
33
|
const DUMMY_SALT = '00000000000000000000000000000000'
|
|
26
34
|
const DUMMY_HASH = '0'.repeat(64)
|
|
27
35
|
|
|
@@ -52,20 +60,18 @@ export async function processAuth(
|
|
|
52
60
|
return { status: 429, payload: { ok: false, error: 'too many attempts' } }
|
|
53
61
|
}
|
|
54
62
|
|
|
55
|
-
const person = await env.DB.prepare(
|
|
56
|
-
'SELECT ownerId, salt, hash, iterations FROM people WHERE ownerId = ?',
|
|
57
|
-
)
|
|
63
|
+
const person = await env.DB.prepare('SELECT ownerId, salt, hash FROM people WHERE ownerId = ?')
|
|
58
64
|
.bind(ownerId)
|
|
59
|
-
.first<{ ownerId: string; salt: string; hash: string
|
|
65
|
+
.first<{ ownerId: string; salt: string; hash: string }>()
|
|
60
66
|
|
|
61
67
|
if (!person) {
|
|
62
68
|
// Burn the same work an enrolled owner would cost, then deny identically.
|
|
63
|
-
await verifyPasscode(passcode, DUMMY_SALT, DUMMY_HASH
|
|
69
|
+
await verifyPasscode(passcode, DUMMY_SALT, DUMMY_HASH)
|
|
64
70
|
log(`[data-portal] op=auth owner=${q(ownerId)} result=denied reason=no-such-owner`)
|
|
65
71
|
return DENIED
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
const ok = await verifyPasscode(passcode, person.salt, person.hash
|
|
74
|
+
const ok = await verifyPasscode(passcode, person.salt, person.hash)
|
|
69
75
|
if (!ok) {
|
|
70
76
|
log(`[data-portal] op=auth owner=${q(ownerId)} result=denied reason=bad-passcode`)
|
|
71
77
|
return DENIED
|
|
@@ -4,16 +4,25 @@
|
|
|
4
4
|
-- what they have dropped (manifest), and how hard they have been trying
|
|
5
5
|
-- (auth_attempts). Everything is IF NOT EXISTS so re-applying is safe.
|
|
6
6
|
|
|
7
|
-
-- One row per enrolled person. No plaintext passcode is ever stored: the
|
|
8
|
-
-- generates the passcode,
|
|
9
|
-
--
|
|
10
|
-
--
|
|
7
|
+
-- One row per enrolled person. No plaintext passcode is ever stored: the
|
|
8
|
+
-- enrolment script generates the passcode, prints it to the operator once, and
|
|
9
|
+
-- persists only the salt and a SHA-256 of salt || passcode.
|
|
10
|
+
--
|
|
11
|
+
-- There is no `iterations` column. Verification is a single fast hash, which is
|
|
12
|
+
-- safe only because passcodes are machine-generated at a pinned length — see the
|
|
13
|
+
-- note at hashPasscode in functions/api/_lib/passcode.mjs. A cost column here
|
|
14
|
+
-- would imply a tunable that does not exist.
|
|
15
|
+
--
|
|
16
|
+
-- `iterations` was dropped outright rather than migrated because no portal had
|
|
17
|
+
-- been deployed when it went (Task 1708). If you meet a `people` table that still
|
|
18
|
+
-- has the column, this file will NOT remove it — IF NOT EXISTS is a no-op on an
|
|
19
|
+
-- existing table — and enrolment's INSERT will fail on `iterations NOT NULL`.
|
|
20
|
+
-- Drop the column by hand in that case; do not add it back here.
|
|
11
21
|
CREATE TABLE IF NOT EXISTS people (
|
|
12
22
|
ownerId TEXT PRIMARY KEY,
|
|
13
23
|
name TEXT NOT NULL,
|
|
14
24
|
salt TEXT NOT NULL,
|
|
15
25
|
hash TEXT NOT NULL,
|
|
16
|
-
iterations INTEGER NOT NULL,
|
|
17
26
|
createdAt TEXT NOT NULL
|
|
18
27
|
);
|
|
19
28
|
|
|
@@ -33,6 +33,8 @@ There is never a `sites/<brand>/` versus `output/<brand>-site/` pair, a `.bak` c
|
|
|
33
33
|
|
|
34
34
|
## Auth: reuse the per-scope token, never mint ad hoc
|
|
35
35
|
|
|
36
|
+
> **Client accounts on a multi-tenant install do not use this section.** Post-1631 their secrets carry no master and `cf-token.sh` denies them the `pages` scope (`reason=not-house`). They publish with `storage-pages-deploy`, which deploys house-side against a project the house has recorded to them (`storage-pages-adopt`). A `not-house` denial here is that boundary working, not a token fault — do not try to mint around it. Everything below is the **house/single-tenant** path.
|
|
37
|
+
|
|
36
38
|
Resolve the Pages-and-D1 token with the deterministic helper **before** any token `curl`, never a hand-built one:
|
|
37
39
|
|
|
38
40
|
```bash
|
|
@@ -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 →
|
|
16
|
+
* stderr : process.stderr.write teed → raw sink + passthrough
|
|
17
17
|
*
|
|
18
|
-
* Destinations
|
|
19
|
-
* - `${LOG_DIR}/mcp-<name
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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 →
|
|
16
|
+
* stderr : process.stderr.write teed → raw sink + passthrough
|
|
17
17
|
*
|
|
18
|
-
* Destinations
|
|
19
|
-
* - `${LOG_DIR}/mcp-<name
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
? (0, node_path_1.resolve)(LOG_DIR, `mcp-${SERVER_NAME}
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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) {
|
|
@@ -112,7 +112,7 @@ Each session row carries a usage icon (Lucide `Gauge`) that opens a modal with a
|
|
|
112
112
|
|
|
113
113
|
## Memory MCP write-path outcome lines
|
|
114
114
|
|
|
115
|
-
Every write-path tool in the memory plugin MCP emits one structured line per invocation to `server.log` via the loopback `/api/admin/log-ingest` route. The operator can answer "did this write succeed?" from a single greppable surface — no need to cross-reference the per-
|
|
115
|
+
Every write-path tool in the memory plugin MCP emits one structured line per invocation to `server.log` via the loopback `/api/admin/log-ingest` route. The operator can answer "did this write succeed?" from a single greppable surface — no need to cross-reference the per-session `mcp-memory-<sessionId>.log`.
|
|
116
116
|
|
|
117
117
|
Line shapes:
|
|
118
118
|
|
|
@@ -137,10 +137,10 @@ grep '\[mcp:memory\] .* result=error' ~/.${brand}/logs/server.log | awk -F'reaso
|
|
|
137
137
|
# Did the loopback POST itself fail? (Helper falls back to stderr and re-writes
|
|
138
138
|
# the structured line, so the event is never lost — this just flags that the
|
|
139
139
|
# session-manager's /api/admin/log-ingest was unreachable for some window.)
|
|
140
|
-
grep '\[mcp:memory\] log-ingest-failed' ~/.${brand}/logs/mcp-memory
|
|
140
|
+
grep '\[mcp:memory\] log-ingest-failed' ~/.${brand}/logs/mcp-memory-*.log
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
The existing
|
|
143
|
+
The existing raw capture is unchanged on error paths — the new structured line is additive, not a relocation. Crash-mode debugging continues to read the raw sink `mcp-memory-<sessionId>.log`; routine "did it land?" questions read `server.log`.
|
|
144
144
|
|
|
145
145
|
**Test contract.** Each of the 20 wired tools carries a `*-emit.test.ts` fixture under `platform/plugins/memory/mcp/src/tools/__tests__/` that mocks `fetch` and asserts the wire body. A refactor that drops `withWriteEvent` from any tool fails the matching tool's tests with a clear `expected "vi.fn()" to be called 1 times, but got 0 times` error, naming the tool. A shared helper at `__tests__/_helpers/emit-capture.ts` standardises the fetch capture + Neo4j Session stub so each tool test stays a few lines of mocks + one happy + one error assertion. Drift between the helper's body shape and the route's validator is pinned by an end-to-end test at `platform/ui/server/routes/admin/__tests__/log-ingest-mcp-end-to-end.test.ts`, which routes the helper's POST through the real Hono `log-ingest` route with a loopback `remoteAddress` shim.
|
|
146
146
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Each installation has its own Cloudflare account. The **tunnel** sign-in is OAuth: the agent invokes `cloudflared tunnel login` via Bash; the Cloudflare Authorize URL streams into the admin chat PTY and the native terminal renders it as a clickable link. Click it, authorise in your own browser, and `cloudflared` writes `cert.pem` to the brand's config directory. For **everything else** (DNS, Pages, D1, Access) the agent uses the Cloudflare API, authenticated by a short-lived narrow token it mints from a master token you provision once in the dashboard (an advanced step the agent never automates). Some account-side jobs — adding a domain, switching accounts — are still easiest in your browser, and the agent relays those click-paths; the rest it can do directly via the API.
|
|
4
4
|
|
|
5
|
+
Where one installation serves several client accounts from one Cloudflare account, that master is held by the house account alone, and a client account cannot mint from it — it is account-wide, so handing it to one client would hand them every other client's data. Publishing a client's website therefore runs house-side on the client's behalf: the client's session asks, the house does the deploy, and no Cloudflare token ever enters the client's account. Which website belongs to which client is recorded once by the house and is never inferred from folder names, so a site nobody has recorded cannot be published by anyone until it is.
|
|
6
|
+
|
|
5
7
|
## Identity model
|
|
6
8
|
|
|
7
9
|
| Concept | Source |
|
|
@@ -34,7 +34,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
|
|
|
34
34
|
| `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
|
|
35
35
|
| `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
|
|
36
36
|
| `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
|
|
37
|
-
| `outlook-draft` | Create a draft in Drafts
|
|
37
|
+
| `outlook-draft` | Create a draft in Drafts, either a new message or a threaded reply. Returns the draft id; never sends. Attachments are attached before the id is returned, so the draft is ready to send. Needs `Mail.ReadWrite`. **New message** (no `replyToMessageId`): `POST /me/messages`, requires `to` + `subject`, same body model as `outlook-mail-send`, threads under nothing. **Threaded reply** (`replyToMessageId` = the id of the message being answered): `POST /me/messages/{id}/createReply` — the draft carries the parent's conversationId, so it lands under the original. This is the only way to draft a reply without sending it: `outlook-mail-reply` threads but always sends. Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote, matching what `outlook-mail-reply` sends, so the threaded path is **plain text only** — `to`, `subject`, and `isHtml: true` are **refused** (not ignored) when replying. `replyAll` includes everyone on the original; `cc`/`bcc` add recipients. |
|
|
38
38
|
| `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
|
|
39
39
|
| `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
|
|
40
40
|
|
|
@@ -76,9 +76,9 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
|
|
|
76
76
|
| Send args | `send-args account=<id> recipients=<N> hasBody=<bool>` |
|
|
77
77
|
| Sent | `sent account=<id> status=202 messageId=<id-or-null>` |
|
|
78
78
|
| Send failed | `send-failed account=<id> error=<msg>` |
|
|
79
|
-
| Draft request | `draft-request account=<id>` |
|
|
80
|
-
| Draft created | `draft-created account=<id> id=<draftId>` |
|
|
81
|
-
| Draft failed | `draft-failed account=<id> error=<msg>` |
|
|
79
|
+
| Draft request | `draft-request account=<id> threaded=<bool>` |
|
|
80
|
+
| Draft created | `draft-created account=<id> id=<draftId> threaded=<bool> convId=<id\|null>` |
|
|
81
|
+
| Draft failed | `draft-failed account=<id> threaded=<bool> error=<msg>` |
|
|
82
82
|
| Draft-send request | `draft-send-request account=<id> id=<draftId>` |
|
|
83
83
|
| Draft sent | `draft-sent account=<id> id=<draftId> status=202` |
|
|
84
84
|
| Draft-send failed | `draft-send-failed account=<id> id=<draftId> error=<msg>` |
|
|
@@ -91,8 +91,15 @@ ssh laptop 'grep -E "^\[outlook-mcp\]" ~/.maxy/logs/server.log | grep "account=<
|
|
|
91
91
|
|
|
92
92
|
# Token-leak audit — must always return zero
|
|
93
93
|
grep -rn -iE "Bearer |access_token=" ~/.maxy/logs/server.log | head
|
|
94
|
+
|
|
95
|
+
# Threading audit — a threaded write that reports no conversation LOST its thread.
|
|
96
|
+
# Must always return zero. Covers both outbound threaded paths (mail-reply and draft-created).
|
|
97
|
+
grep -E "^\[outlook-mcp\] (mail-reply|draft-created)" ~/.maxy/logs/server.log \
|
|
98
|
+
| grep -E "threaded=true|op=reply" | grep -E "convId=(null)?( |$)" | head
|
|
94
99
|
```
|
|
95
100
|
|
|
101
|
+
Threading triage: **absence of a `convId` on a threaded write means threading was lost.** `draft-created threaded=false convId=null` is correct and expected — a new message has no parent conversation. `draft-created threaded=true convId=null` is a fault: Graph returned a reply draft that names no conversation, and the reply will land detached from the original.
|
|
102
|
+
|
|
96
103
|
Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions issue; `elapsedMs > 5000` → Graph slowness or DNS.
|
|
97
104
|
|
|
98
105
|
## Failure modes
|
|
@@ -161,7 +161,9 @@ import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
|
|
|
161
161
|
initStderrTee("your-plugin-name");
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
After this, every `console.error("[your-tool]...")` from any tool in the plugin
|
|
164
|
+
After this, every `console.error("[your-tool]...")` from any tool in the plugin is captured in the raw per-server file `mcp-your-plugin-name-{sessionId}.log`, written by the `mcp-spawn-tee` wrapper. That is where you grep.
|
|
165
|
+
|
|
166
|
+
`initStderrTee` is *intended* to also mirror each line into the per-conversation stream log `claude-agent-stream-{sessionId}.log` as `[<iso-ts>] [mcp:your-plugin-name] [your-tool]...`, alongside the usual agent events. It does not currently do so: that destination is gated on `STREAM_LOG_PATH`, which no spawner sets, so the call is a no-op in production. Do not expect `[mcp:<name>]` lines in a conversation's stream log, and do not read their absence as the server having emitted nothing. Tracked as [`.tasks/1736-*.md`](../../../.tasks/1736-nothing-sets-stream-log-path-so-the-per-conversation-mcp-stream-log-does-not-exist.md).
|
|
165
167
|
|
|
166
168
|
**Premium plugins.** Source lives at `premium-plugins/<bundle>/plugins/<name>/mcp/src/` — deeper than platform plugins, so the source-relative import to `platform/lib/mcp-stderr-tee/dist/index.js` uses more `../` segments. The bundler rewrites the compiled output to the canonical `../../../../lib/mcp-stderr-tee/dist/index.js` at staging time and ships `platform/lib/mcp-stderr-tee/{dist,package.json}` into `premium-plugins/<bundle>/lib/mcp-stderr-tee/` so the import resolves at deployed depth. The bundler fails loudly if `platform/lib/mcp-stderr-tee/package.json` is missing (it must pin `type` so install-location parent walks cannot mis-classify the dist file) or if any lib referenced by a rewritten import has no source dist.
|
|
167
169
|
|
|
@@ -172,7 +174,7 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
|
|
|
172
174
|
**Retrieve MCP diagnostic lines for a conversation:**
|
|
173
175
|
|
|
174
176
|
- All servers: `logs-read { type: "system", sessionId: "..." }` → grep `[mcp:<name>]` on the returned stream log.
|
|
175
|
-
- One server raw feed: `logs-read { type: "mcp" }` → tails the most recent `mcp-<name
|
|
177
|
+
- One server raw feed: `logs-read { type: "mcp" }` → tails the most recent `mcp-<name>-*.log` (per-plugin, not per-conversation).
|
|
176
178
|
|
|
177
179
|
**Tee-state markers** land in the stream log: `[platform] [mcp-tee-attach] server=<name> streamLogPath=...` when the tee wires up, `[platform] [mcp-tee-skip] server=<name> destination=... reason=...` when a destination fails (missing `LOG_DIR`, unwritable path, `STREAM_LOG_PATH` not set, etc.), `[platform] [mcp-tee-detach] server=<name>` on graceful shutdown. If a server invoked tools but no `[mcp:<name>]` lines appear in the conversation's log, look for the skip marker first.
|
|
178
180
|
|
|
@@ -180,16 +182,16 @@ After this, every `console.error("[your-tool]...")` from any tool in the plugin
|
|
|
180
182
|
|
|
181
183
|
## Failure-path observability contract (earlier platform fixes + earlier platform fixes)
|
|
182
184
|
|
|
183
|
-
The `initStderrTee` wrapper writes to the per-conversation stream log
|
|
185
|
+
The `initStderrTee` wrapper writes to the per-conversation stream log via `createWriteStream` — async, buffered. Any diagnostic `console.error(…)` followed by an immediate `process.exit(…)` is lost from *that* destination: the event loop never drains the WriteStream before the process terminates. Same race for any synchronous module-load throw: Node's uncaught-exception handler writes the stack to raw fd 2 and exits before the patched async stream flushes. The raw sink does not have this race — removed `initStderrTee`'s own raw file, and `mcp-spawn-tee` writes the sink with `appendFileSync`, which returns once the kernel has queued the write and so survives an immediate exit.
|
|
184
186
|
|
|
185
187
|
**Two layers now close the gap, each load-bearing on its own:**
|
|
186
188
|
|
|
187
189
|
1. **Plugin-side sync-write discipline.** Plugins that call `process.exit` during module load (rare — `graph-mcp` is the in-tree example; it spawns a child at boot to proxy upstream stdio) use `fs.appendFileSync` at every named exit path to guarantee the cause lands in both log destinations before exit. Lines follow the `[mcp:<name>] [<plugin-prefix>] <cause>` format so existing `grep '[mcp:<name>]'` investigator paths work. Each destination is wrapped in its own try/catch — an unwritable log must not mask the primary failure. This is the discipline propagated to any plugin author who knows their failure paths.
|
|
188
190
|
|
|
189
|
-
2. **Parent-side `mcp-spawn-tee` wrapper.** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` wrapper rather than `node <entry>` directly. The wrapper
|
|
191
|
+
2. **Parent-side `mcp-spawn-tee` wrapper.** Every node-based core MCP server is spawned via the `lib/mcp-spawn-tee` wrapper rather than `node <entry>` directly. The wrapper runs the real entry in its own process and writes stderr chunks to `${LOG_DIR}/mcp-${name}-${SESSION_ID}.log` (or `mcp-${name}-nosession.log` when `SESSION_ID` is unset) via `appendFileSync` while passing the same chunks through to its own stderr (Claude Code's consumer is unchanged). It is the only writer of that raw sink: retired the `mcp-${name}-stderr-<date>.log` twin, which `mcp-stderr-tee` also wrote, so every line used to land there twice. Synchronous `appendFileSync` survives `process.exit`, so the sink captures even (a) module-load throws before `initStderrTee` runs, (b) `MODULE_NOT_FOUND` on the entry script itself, and (c) anything else a plugin author missed. The wrapper writes `[mcp-spawn-tee-attached] server=<name> pid=<n>` on attach and forwards SIGTERM/SIGINT to the child. This is the layer that makes capture independent of plugin discipline. Playwright stays unwrapped because it spawns via `npx`, not `node`.
|
|
190
192
|
|
|
191
|
-
A
|
|
193
|
+
A `[mcp-init-error]` probe on the platform side used to read the retired per-date file and tail it into the stream log. found no such probe in live code — `claude-agent.ts` has not carried one for some time — so the wrapper's own `[mcp-helper] op=boot` and `op=exit` lines are the record of a failed MCP boot.
|
|
192
194
|
|
|
193
|
-
**Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-
|
|
195
|
+
**Signal inventory after a failed session:** `[init] FAILED MCP servers: <names>` (names), `[mcp-spawn-tee-attached] server=<name> pid=<n>` (proof the wrapper attached), `[mcp-spawn-tee-exit] server=<name> code=<n>|signal=<s>` (proof the wrapper saw the exit), and optionally `[mcp:<name>] [<plugin>] …` from plugin-side sync-writes. Their union gives the investigator independent sources for the same failure. (The two `[mcp-spawn-tee-*]` names are stale — the shim emits `[mcp-helper] op=spawn|boot|exit`; see [`.tasks/1737-*.md`](../../../.tasks/1737-mcp-spawn-tee-docs-promise-lifecycle-markers-and-a-child-process-that-task-989-removed.md).)
|
|
194
196
|
|
|
195
197
|
**Boot-smoke as publish-time gate.** The memory MCP carries `scripts/boot-smoke.sh` that spawns `dist/index.js` with stub env, sleeps 2s, asserts `kill -0 <pid>`, and reports `[boot-smoke] memory ok|FAILED tail=<n-lines>`. Wired to `prepublish` in `plugins/memory/mcp/package.json`. The pattern is propagatable to other plugin MCPs — it's deliberately not generalised yet because each plugin's stub-env requirements differ (memory needs ACCOUNT_ID + PLATFORM_ROOT + NEO4J_URI + SESSION_ID; others differ).
|