@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
|
@@ -1,74 +1,204 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
PASSCODE_ALPHABET,
|
|
4
|
+
PASSCODE_LENGTH,
|
|
5
|
+
PASSCODE_ENTROPY_BITS,
|
|
6
|
+
PasscodeTooWeakError,
|
|
7
|
+
assertPasscodeStrength,
|
|
4
8
|
constantTimeEqual,
|
|
9
|
+
generatePasscode,
|
|
5
10
|
hashPasscode,
|
|
6
11
|
verifyPasscode,
|
|
7
12
|
randomSaltHex,
|
|
8
|
-
} from '../../skills/data-portal/template/functions/api/_lib/passcode'
|
|
13
|
+
} from '../../skills/data-portal/template/functions/api/_lib/passcode.mjs'
|
|
9
14
|
|
|
10
15
|
const SALT = '000102030405060708090a0b0c0d0e0f'
|
|
11
16
|
|
|
12
|
-
describe('
|
|
13
|
-
it('is
|
|
14
|
-
expect(
|
|
17
|
+
describe('the pinned strength', () => {
|
|
18
|
+
it('is at least 128 bits', () => {
|
|
19
|
+
expect(PASSCODE_ENTROPY_BITS).toBeGreaterThanOrEqual(128)
|
|
15
20
|
})
|
|
16
|
-
it('
|
|
17
|
-
|
|
21
|
+
it('states an entropy figure the alphabet and length actually produce', () => {
|
|
22
|
+
// 32 symbols is 5 bits each, exactly, because 256 divides by 32.
|
|
23
|
+
expect(PASSCODE_ALPHABET.length).toBe(32)
|
|
24
|
+
expect(PASSCODE_ENTROPY_BITS).toBe(PASSCODE_LENGTH * 5)
|
|
18
25
|
})
|
|
19
|
-
it('
|
|
20
|
-
expect(
|
|
26
|
+
it('draws from an alphabet with no repeats and no ambiguous characters', () => {
|
|
27
|
+
expect(new Set(PASSCODE_ALPHABET).size).toBe(PASSCODE_ALPHABET.length)
|
|
28
|
+
for (const c of 'ILOU') expect(PASSCODE_ALPHABET).not.toContain(c)
|
|
21
29
|
})
|
|
22
|
-
it('
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
it('survives the case normalisation verifyPasscode applies, without folding', () => {
|
|
31
|
+
// Task 1713 made verification case-insensitive. This pins WHY that costs no
|
|
32
|
+
// entropy, rather than leaving a comment to assert it.
|
|
33
|
+
//
|
|
34
|
+
// The argument: generation emits exactly one case, so uppercasing is the
|
|
35
|
+
// identity over the generated set, and the map from a generated passcode to
|
|
36
|
+
// its normalised form is injective. Two distinct generated passcodes can
|
|
37
|
+
// therefore never fold onto one string, and PASSCODE_ENTROPY_BITS still
|
|
38
|
+
// counts what it says it counts. Normalisation widens accepted INPUT, not
|
|
39
|
+
// the search space.
|
|
40
|
+
//
|
|
41
|
+
// If this ever goes red, the alphabet has grown a character whose case
|
|
42
|
+
// folds, and the entropy figure above is overstated.
|
|
43
|
+
for (const c of PASSCODE_ALPHABET) expect(c.toUpperCase()).toBe(c)
|
|
44
|
+
expect(new Set([...PASSCODE_ALPHABET].map((c) => c.toUpperCase())).size).toBe(
|
|
45
|
+
PASSCODE_ALPHABET.length,
|
|
46
|
+
)
|
|
47
|
+
const generated = Array.from({ length: 200 }, () => generatePasscode())
|
|
48
|
+
for (const pc of generated) expect(pc.toUpperCase()).toBe(pc)
|
|
49
|
+
// Compared against the count of DISTINCT originals, not against 200: this
|
|
50
|
+
// asserts injectivity alone, and does not quietly also assert that 200 draws
|
|
51
|
+
// never repeat (which is generatePasscode's claim, tested separately).
|
|
52
|
+
expect(new Set(generated.map((pc) => pc.toUpperCase())).size).toBe(new Set(generated).size)
|
|
53
|
+
expect(PASSCODE_ENTROPY_BITS).toBe(PASSCODE_LENGTH * 5)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('generatePasscode', () => {
|
|
58
|
+
it('mints a passcode at the pinned length, from the pinned alphabet', () => {
|
|
59
|
+
const pc = generatePasscode()
|
|
60
|
+
expect(pc).toHaveLength(PASSCODE_LENGTH)
|
|
61
|
+
for (const c of pc) expect(PASSCODE_ALPHABET).toContain(c)
|
|
62
|
+
})
|
|
63
|
+
it('does not repeat itself', () => {
|
|
64
|
+
const seen = new Set(Array.from({ length: 50 }, () => generatePasscode()))
|
|
65
|
+
expect(seen.size).toBe(50)
|
|
66
|
+
})
|
|
67
|
+
it('mints what the strength check accepts', () => {
|
|
68
|
+
expect(() => assertPasscodeStrength(generatePasscode())).not.toThrow()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('assertPasscodeStrength', () => {
|
|
73
|
+
it('refuses a six-digit number — the case the skill prose allowed', () => {
|
|
74
|
+
expect(() => assertPasscodeStrength('123456')).toThrow(PasscodeTooWeakError)
|
|
75
|
+
})
|
|
76
|
+
it('refuses with a named reason rather than a bare Error', () => {
|
|
77
|
+
try {
|
|
78
|
+
assertPasscodeStrength('123456')
|
|
79
|
+
throw new Error('should have refused')
|
|
80
|
+
} catch (e) {
|
|
81
|
+
expect(e).toBeInstanceOf(PasscodeTooWeakError)
|
|
82
|
+
expect((e as PasscodeTooWeakError).reason).toBe('passcode-too-weak')
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
it('refuses one character short', () => {
|
|
86
|
+
expect(() => assertPasscodeStrength('A'.repeat(PASSCODE_LENGTH - 1))).toThrow(
|
|
87
|
+
PasscodeTooWeakError,
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
it('refuses a character outside the alphabet', () => {
|
|
91
|
+
expect(() => assertPasscodeStrength('!'.repeat(PASSCODE_LENGTH))).toThrow(PasscodeTooWeakError)
|
|
92
|
+
})
|
|
93
|
+
it('refuses the ambiguous characters the alphabet excludes', () => {
|
|
94
|
+
expect(() => assertPasscodeStrength('I'.repeat(PASSCODE_LENGTH))).toThrow(PasscodeTooWeakError)
|
|
95
|
+
})
|
|
96
|
+
it('refuses an empty passcode', () => {
|
|
97
|
+
expect(() => assertPasscodeStrength('')).toThrow(PasscodeTooWeakError)
|
|
98
|
+
})
|
|
99
|
+
it('checks structure only, and does NOT claim to measure entropy', () => {
|
|
100
|
+
// This documents a limitation deliberately. A repeated character has ~no
|
|
101
|
+
// entropy yet passes, because length and alphabet are all a check on an
|
|
102
|
+
// arbitrary string can verify. Entropy comes from generatePasscode alone.
|
|
103
|
+
// If this ever throws, the check has grown a claim it cannot support.
|
|
104
|
+
expect(() => assertPasscodeStrength('A'.repeat(PASSCODE_LENGTH))).not.toThrow()
|
|
28
105
|
})
|
|
29
106
|
})
|
|
30
107
|
|
|
31
108
|
describe('hashPasscode', () => {
|
|
32
|
-
it('is deterministic for the same passcode
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
109
|
+
it('is deterministic for the same passcode and salt', async () => {
|
|
110
|
+
const pc = generatePasscode()
|
|
111
|
+
expect(await hashPasscode(pc, SALT)).toBe(await hashPasscode(pc, SALT))
|
|
112
|
+
})
|
|
113
|
+
it('returns 64 hex characters', async () => {
|
|
114
|
+
expect(await hashPasscode(generatePasscode(), SALT)).toMatch(/^[0-9a-f]{64}$/)
|
|
37
115
|
})
|
|
38
116
|
it('differs for a different passcode', async () => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
117
|
+
expect(await hashPasscode(generatePasscode(), SALT)).not.toBe(
|
|
118
|
+
await hashPasscode(generatePasscode(), SALT),
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
it('differs for the same passcode under a different salt', async () => {
|
|
122
|
+
const pc = generatePasscode()
|
|
123
|
+
const other = '0f0e0d0c0b0a09080706050403020100'
|
|
124
|
+
expect(await hashPasscode(pc, SALT)).not.toBe(await hashPasscode(pc, other))
|
|
42
125
|
})
|
|
43
|
-
it('
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
expect(
|
|
126
|
+
it('refuses to hash a weak passcode rather than hashing it anyway', async () => {
|
|
127
|
+
// The mutation check: revert assertPasscodeStrength's call here and this
|
|
128
|
+
// fails. Without it the check is decoration and the fast hash is unsafe.
|
|
129
|
+
await expect(hashPasscode('123456', SALT)).rejects.toBeInstanceOf(PasscodeTooWeakError)
|
|
130
|
+
})
|
|
131
|
+
it('rejects a salt that is not hex', async () => {
|
|
132
|
+
await expect(hashPasscode(generatePasscode(), 'nothex')).rejects.toThrow()
|
|
47
133
|
})
|
|
48
134
|
})
|
|
49
135
|
|
|
50
136
|
describe('verifyPasscode', () => {
|
|
51
137
|
it('accepts the right passcode', async () => {
|
|
52
|
-
const
|
|
53
|
-
expect(await verifyPasscode(
|
|
138
|
+
const pc = generatePasscode()
|
|
139
|
+
expect(await verifyPasscode(pc, SALT, await hashPasscode(pc, SALT))).toBe(true)
|
|
54
140
|
})
|
|
55
141
|
it('rejects the wrong passcode', async () => {
|
|
56
|
-
const
|
|
57
|
-
expect(await verifyPasscode(
|
|
142
|
+
const stored = await hashPasscode(generatePasscode(), SALT)
|
|
143
|
+
expect(await verifyPasscode(generatePasscode(), SALT, stored)).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
it('rejects rather than throws when the stored hash is malformed', async () => {
|
|
146
|
+
expect(await verifyPasscode(generatePasscode(), SALT, 'nothex')).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
it('rejects a weak passcode at sign-in without throwing', async () => {
|
|
149
|
+
// A row hashed before the check existed must not crash the auth route.
|
|
150
|
+
expect(await verifyPasscode('123456', SALT, '0'.repeat(64))).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
it('accepts a lowercase transcription of the passcode', async () => {
|
|
153
|
+
// The whole reason the alphabet excludes I, L, O and U is that a person
|
|
154
|
+
// transcribes this by hand. Crockford is case-insensitive on decode for
|
|
155
|
+
// that same reason. The mutation check: drop .toUpperCase() from
|
|
156
|
+
// verifyPasscode and this goes red.
|
|
157
|
+
const pc = generatePasscode()
|
|
158
|
+
const stored = await hashPasscode(pc, SALT)
|
|
159
|
+
expect(await verifyPasscode(pc.toLowerCase(), SALT, stored)).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
it('accepts a mixed-case transcription of the passcode', async () => {
|
|
162
|
+
const pc = generatePasscode()
|
|
163
|
+
const stored = await hashPasscode(pc, SALT)
|
|
164
|
+
const mixed = [...pc].map((c, i) => (i % 2 ? c.toLowerCase() : c)).join('')
|
|
165
|
+
expect(await verifyPasscode(mixed, SALT, stored)).toBe(true)
|
|
58
166
|
})
|
|
59
|
-
it('rejects
|
|
60
|
-
|
|
167
|
+
it('still rejects the wrong passcode, lowercase included', async () => {
|
|
168
|
+
// Widening the accepted case must not widen the accepted passcode.
|
|
169
|
+
const stored = await hashPasscode(generatePasscode(), SALT)
|
|
170
|
+
expect(await verifyPasscode(generatePasscode().toLowerCase(), SALT, stored)).toBe(false)
|
|
171
|
+
})
|
|
172
|
+
it('returns false rather than throwing when the passcode is not a string', async () => {
|
|
173
|
+
// .toUpperCase() throws on a non-string, so it sits INSIDE this function's
|
|
174
|
+
// try. Move it out and this goes red. That placement is what keeps the
|
|
175
|
+
// documented never-throw contract true for the input class it covers.
|
|
176
|
+
// @ts-expect-error deliberately passing a non-string
|
|
177
|
+
expect(await verifyPasscode(null, SALT, '0'.repeat(64))).toBe(false)
|
|
61
178
|
})
|
|
62
179
|
})
|
|
63
180
|
|
|
64
|
-
describe('
|
|
65
|
-
it('
|
|
66
|
-
expect(
|
|
181
|
+
describe('constantTimeEqual', () => {
|
|
182
|
+
it('is true for identical arrays', () => {
|
|
183
|
+
expect(constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
it('is false when a byte differs', () => {
|
|
186
|
+
expect(constantTimeEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]))).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
it('is false for different lengths, without throwing', () => {
|
|
189
|
+
expect(constantTimeEqual(new Uint8Array([1, 2]), new Uint8Array([1, 2, 3]))).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
it('compares every byte even after a mismatch (no early return)', () => {
|
|
192
|
+
// A first-byte mismatch and a last-byte mismatch must both be false.
|
|
193
|
+
// This does not measure timing; it pins that the function does not
|
|
194
|
+
// short-circuit in a way a rewrite could reintroduce.
|
|
195
|
+
expect(constantTimeEqual(new Uint8Array([9, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(false)
|
|
196
|
+
expect(constantTimeEqual(new Uint8Array([1, 2, 9]), new Uint8Array([1, 2, 3]))).toBe(false)
|
|
67
197
|
})
|
|
68
198
|
})
|
|
69
199
|
|
|
70
|
-
describe('
|
|
71
|
-
it('
|
|
72
|
-
expect(
|
|
200
|
+
describe('randomSaltHex', () => {
|
|
201
|
+
it('renders bytes as lowercase hex, zero-padded', () => {
|
|
202
|
+
expect(randomSaltHex(new Uint8Array([0, 15, 16, 255]))).toBe('000f10ff')
|
|
73
203
|
})
|
|
74
204
|
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { execFile } from 'node:child_process'
|
|
3
|
+
import { promisify } from 'node:util'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { dirname, resolve } from 'node:path'
|
|
6
|
+
import {
|
|
7
|
+
PASSCODE_ALPHABET,
|
|
8
|
+
PASSCODE_LENGTH,
|
|
9
|
+
verifyPasscode,
|
|
10
|
+
} from '../../skills/data-portal/template/functions/api/_lib/passcode.mjs'
|
|
11
|
+
|
|
12
|
+
const run = promisify(execFile)
|
|
13
|
+
const SCRIPT = resolve(dirname(fileURLToPath(import.meta.url)), '../../bin/portal-enrol.mjs')
|
|
14
|
+
|
|
15
|
+
const enrol = (args: string[]) => run('node', [SCRIPT, ...args])
|
|
16
|
+
|
|
17
|
+
/** The script's contract: stdout is SQL and nothing else, so it pipes straight
|
|
18
|
+
* into `wrangler d1 execute --file=-`; the passcode goes to stderr, where the
|
|
19
|
+
* operator still sees it on their terminal but a pipe never carries it. The two
|
|
20
|
+
* streams are the whole point — a passcode on stdout would be fed to wrangler,
|
|
21
|
+
* which echoes the failing statement into its error output. */
|
|
22
|
+
function parse(r: { stdout: string; stderr: string }) {
|
|
23
|
+
const passcode = /^passcode:\s*(\S+)$/m.exec(r.stderr)?.[1]
|
|
24
|
+
const sql = /^(INSERT INTO people .*)$/m.exec(r.stdout)?.[1]
|
|
25
|
+
return { passcode, sql }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const valuesOf = (sql: string) =>
|
|
29
|
+
/VALUES \('([^']*)', '((?:[^']|'')*)', '([0-9a-f]+)', '([0-9a-f]+)', '([^']*)'\)/.exec(sql)!
|
|
30
|
+
|
|
31
|
+
/** Apply the emitted SQL to a real SQLite database built from the template's own
|
|
32
|
+
* schema.sql, the way `wrangler d1 execute` applies it to D1. A fake would model
|
|
33
|
+
* the constraint we are trying to prove we satisfy, which is how 1689 shipped a
|
|
34
|
+
* UNIQUE-violating upload: its fake accepted duplicates the schema rejected. */
|
|
35
|
+
async function d1(dir: string) {
|
|
36
|
+
const { mkdtemp } = await import('node:fs/promises')
|
|
37
|
+
const { tmpdir } = await import('node:os')
|
|
38
|
+
const root = await mkdtemp(resolve(tmpdir(), dir))
|
|
39
|
+
const db = resolve(root, 'portal.db')
|
|
40
|
+
const schema = resolve(
|
|
41
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
42
|
+
'../../skills/data-portal/template/schema.sql',
|
|
43
|
+
)
|
|
44
|
+
// SQL as an argv entry, not stdin: execFile has no `input` option. The schema
|
|
45
|
+
// goes via `.read` because it opens with a `--` comment, which sqlite3 would
|
|
46
|
+
// otherwise parse as a CLI option.
|
|
47
|
+
await run('sqlite3', [db, `.read ${schema}`])
|
|
48
|
+
return {
|
|
49
|
+
exec: (sql: string) => run('sqlite3', [db, sql]),
|
|
50
|
+
query: async (sql: string) => (await run('sqlite3', [db, sql])).stdout.trim(),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('portal-enrol', () => {
|
|
55
|
+
it('prints a passcode at the pinned length from the pinned alphabet', async () => {
|
|
56
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
57
|
+
const { passcode } = parse(r)
|
|
58
|
+
expect(passcode).toHaveLength(PASSCODE_LENGTH)
|
|
59
|
+
for (const c of passcode!) expect(PASSCODE_ALPHABET).toContain(c)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('emits a hash the portal will actually accept for that passcode', async () => {
|
|
63
|
+
// This is the test that matters. It proves enrolment and the Worker agree,
|
|
64
|
+
// which is the whole reason the hash has one implementation rather than a
|
|
65
|
+
// twin in this script.
|
|
66
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
67
|
+
const { passcode, sql } = parse(r)
|
|
68
|
+
const [, , , salt, hash] = valuesOf(sql!)
|
|
69
|
+
expect(await verifyPasscode(passcode!, salt, hash)).toBe(true)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('emits a hash that rejects a different passcode', async () => {
|
|
73
|
+
const a = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
74
|
+
const [, , , salt, hash] = valuesOf(parse(a).sql!)
|
|
75
|
+
const b = await enrol(['--owner', 'bob', '--name', 'Bob'])
|
|
76
|
+
expect(await verifyPasscode(parse(b).passcode!, salt, hash)).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('never puts the passcode in the SQL', async () => {
|
|
80
|
+
// A passcode reaching D1 or a log in plaintext is the one thing enrolment
|
|
81
|
+
// must never do.
|
|
82
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
83
|
+
const { passcode, sql } = parse(r)
|
|
84
|
+
expect(sql).not.toContain(passcode!)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('does not repeat a passcode across enrolments', async () => {
|
|
88
|
+
const a = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
89
|
+
const b = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
90
|
+
expect(parse(a).passcode).not.toBe(parse(b).passcode)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('does not repeat a salt across enrolments', async () => {
|
|
94
|
+
const saltOf = async (owner: string) => {
|
|
95
|
+
const r = await enrol(['--owner', owner, '--name', owner])
|
|
96
|
+
return valuesOf(parse(r).sql!)[3]
|
|
97
|
+
}
|
|
98
|
+
expect(await saltOf('alice')).not.toBe(await saltOf('bob'))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('refuses an ownerId the isolation gate would reject', async () => {
|
|
102
|
+
// `alice` and `alice/2024` co-enrolled would breach the prefix isolation
|
|
103
|
+
// rule, so enrolment fails closed on the same charset the gate enforces.
|
|
104
|
+
await expect(enrol(['--owner', 'alice/2024', '--name', 'Alice'])).rejects.toThrow()
|
|
105
|
+
await expect(enrol(['--owner', 'Alice', '--name', 'Alice'])).rejects.toThrow()
|
|
106
|
+
await expect(enrol(['--owner', '../etc', '--name', 'Alice'])).rejects.toThrow()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('refuses a missing owner or name', async () => {
|
|
110
|
+
await expect(enrol(['--name', 'Alice'])).rejects.toThrow()
|
|
111
|
+
await expect(enrol(['--owner', 'alice'])).rejects.toThrow()
|
|
112
|
+
await expect(enrol([])).rejects.toThrow()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('escapes a quote in the name rather than breaking the statement', async () => {
|
|
116
|
+
const r = await enrol(['--owner', 'alice', '--name', "O'Brien"])
|
|
117
|
+
expect(parse(r).sql).toContain("'O''Brien'")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('writes the row the schema declares, in order', async () => {
|
|
121
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
122
|
+
const { sql } = parse(r)
|
|
123
|
+
expect(sql).toContain('INSERT INTO people (ownerId, name, salt, hash, createdAt)')
|
|
124
|
+
const [, owner, name, salt, hash, createdAt] = valuesOf(sql!)
|
|
125
|
+
expect(owner).toBe('alice')
|
|
126
|
+
expect(name).toBe('Alice')
|
|
127
|
+
expect(salt).toMatch(/^[0-9a-f]{32}$/)
|
|
128
|
+
expect(hash).toMatch(/^[0-9a-f]{64}$/)
|
|
129
|
+
expect(Number.isNaN(Date.parse(createdAt))).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('puts SQL and only SQL on stdout, so the documented pipe works', async () => {
|
|
133
|
+
// SKILL.md tells the agent to pipe stdout into `wrangler d1 execute`. Anything
|
|
134
|
+
// else on stdout goes into wrangler as SQL — and a passcode there would be
|
|
135
|
+
// echoed back in wrangler's syntax error, which is the one place a plaintext
|
|
136
|
+
// passcode must never reach.
|
|
137
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
138
|
+
expect(r.stdout.trim()).toMatch(/^INSERT INTO people [^\n]*;$/)
|
|
139
|
+
expect(r.stdout).not.toContain('passcode')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('keeps the passcode off stdout entirely, on stderr where a pipe cannot take it', async () => {
|
|
143
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
144
|
+
const { passcode } = parse(r)
|
|
145
|
+
expect(passcode).toHaveLength(PASSCODE_LENGTH)
|
|
146
|
+
expect(r.stdout).not.toContain(passcode!)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('rejects a newline in the name, which would break the pipeable contract', async () => {
|
|
150
|
+
await expect(enrol(['--owner', 'alice', '--name', 'Bob\nDROP'])).rejects.toThrow()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('against a real SQLite database built from schema.sql', () => {
|
|
154
|
+
it('applies cleanly', async () => {
|
|
155
|
+
const db = await d1('enrol-apply-')
|
|
156
|
+
const r = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
157
|
+
await db.exec(parse(r).sql!)
|
|
158
|
+
expect(await db.query('SELECT ownerId, name FROM people;')).toBe('alice|Alice')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('rotates a passcode by re-running, which is the ONLY sanctioned rotation path', async () => {
|
|
162
|
+
// SKILL.md: "Rotating a passcode is a re-run of the script for that person."
|
|
163
|
+
// A bare INSERT cannot do that — ownerId is TEXT PRIMARY KEY, so the second
|
|
164
|
+
// run dies on a UNIQUE violation in a SEPARATE process, AFTER the script has
|
|
165
|
+
// already printed a new passcode and exited 0. The operator hands out a
|
|
166
|
+
// passcode that never worked while the leaked one still does, and pcCheck
|
|
167
|
+
// revocation never fires. Rotation is exactly the case where a remedy that
|
|
168
|
+
// silently no-ops is worse than no remedy.
|
|
169
|
+
const db = await d1('enrol-rotate-')
|
|
170
|
+
|
|
171
|
+
const first = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
172
|
+
await db.exec(parse(first).sql!)
|
|
173
|
+
|
|
174
|
+
const second = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
175
|
+
await db.exec(parse(second).sql!)
|
|
176
|
+
|
|
177
|
+
const [salt, hash] = (await db.query("SELECT salt, hash FROM people WHERE ownerId='alice';"))
|
|
178
|
+
.split('|')
|
|
179
|
+
expect(await verifyPasscode(parse(second).passcode!, salt, hash)).toBe(true)
|
|
180
|
+
expect(await verifyPasscode(parse(first).passcode!, salt, hash)).toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('rotating leaves exactly one row, and does not reset createdAt', async () => {
|
|
184
|
+
// createdAt is when the person was enrolled. Rotation is not creation, so
|
|
185
|
+
// it does not move; nothing reads it, but a field that lies about what it
|
|
186
|
+
// holds is the defect this component keeps being reviewed for.
|
|
187
|
+
const db = await d1('enrol-one-row-')
|
|
188
|
+
const first = await enrol(['--owner', 'alice', '--name', 'Alice'])
|
|
189
|
+
await db.exec(parse(first).sql!)
|
|
190
|
+
const createdAt = await db.query("SELECT createdAt FROM people WHERE ownerId='alice';")
|
|
191
|
+
|
|
192
|
+
await db.exec(parse(await enrol(['--owner', 'alice', '--name', 'Alice'])).sql!)
|
|
193
|
+
|
|
194
|
+
expect(await db.query('SELECT COUNT(*) FROM people;')).toBe('1')
|
|
195
|
+
expect(await db.query("SELECT createdAt FROM people WHERE ownerId='alice';")).toBe(createdAt)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('stores a crafted name as data rather than executing it', async () => {
|
|
199
|
+
const db = await d1('enrol-inject-')
|
|
200
|
+
const r = await enrol(['--owner', 'alice', '--name', "x'); DROP TABLE people; --"])
|
|
201
|
+
await db.exec(parse(r).sql!)
|
|
202
|
+
expect(await db.query("SELECT name FROM people WHERE ownerId='alice';")).toBe(
|
|
203
|
+
"x'); DROP TABLE people; --",
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { initStderrTee } from "../../../../lib/mcp-stderr-tee/dist/index.js";
|
|
2
|
-
initStderrTee("cloudflare");
|
|
3
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
3
|
// The Cloudflare plugin exposes zero agent-facing tools. Every
|
|
6
4
|
// Cloudflare operation is the agent invoking `cloudflared` directly via
|
|
7
5
|
// the Bash tool, following the runbook in
|
|
8
6
|
// platform/plugins/cloudflare/references/manual-setup.md, with
|
|
9
|
-
// cloudflared stdout/stderr streamed verbatim into the PTY. The
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// cloudflared stdout/stderr streamed verbatim into the PTY. The plugin
|
|
8
|
+
// declares `mcp-manifest: skip` and no `mcp:` block, so this entry is never
|
|
9
|
+
// spawned as an MCP server: only the generated .claude-plugin/plugin.json
|
|
10
|
+
// mcpServers block makes Claude Code spawn one (tool-registration-drift.ts).
|
|
11
|
+
// Task 1721 removed its unreachable stderr-tee call.
|
|
12
12
|
const server = new McpServer({
|
|
13
13
|
name: "cloudflare",
|
|
14
14
|
version: "0.4.0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,+DAA+D;AAC/D,wEAAwE;AACxE,0CAA0C;AAC1C,+DAA+D;AAC/D,uEAAuE;AACvE,4EAA4E;AAC5E,0EAA0E;AAC1E,6EAA6E;AAC7E,qDAAqD;AAErD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,YAAY;IAClB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,2BAA2B,GAAG,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "tsc",
|
|
9
9
|
"start": "node dist/index.js",
|
|
10
10
|
"test": "vitest run",
|
|
11
|
-
"typecheck:templates": "tsc --noEmit --strict --target es2022 --module esnext --moduleResolution bundler --lib es2022,dom ../skills/*/template/functions/api/*.ts ../skills/*/template/functions/api/_lib/*.ts"
|
|
11
|
+
"typecheck:templates": "tsc --noEmit --strict --allowJs --checkJs --target es2022 --module esnext --moduleResolution bundler --lib es2022,dom ../skills/*/template/functions/api/*.ts ../skills/*/template/functions/api/_lib/*.ts ../skills/*/template/functions/api/_lib/*.mjs"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
@@ -33,6 +33,8 @@ If the site captures form data, add the `[[d1_databases]]` binding from `d1-data
|
|
|
33
33
|
|
|
34
34
|
## 3. Load the reused deploy token
|
|
35
35
|
|
|
36
|
+
> **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool.
|
|
37
|
+
|
|
36
38
|
A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). The `pages` scope resolves the stable `<brand>-pages-d1` token, which carries both. Resolve it with the deterministic helper, which mints it once only if absent and reuses it thereafter. This is the one command; do not source the secrets file and hand the resulting `CLOUDFLARE_API_TOKEN` to `wrangler` — that variable holds the **master**, not the per-scope token, and `wrangler pages` then returns `Authentication error [code: 10000]`:
|
|
37
39
|
|
|
38
40
|
```bash
|
|
@@ -56,7 +56,7 @@ The booking-specific schema ships with the template: `schema.sql` creates both t
|
|
|
56
56
|
|
|
57
57
|
## Deploy through site-deploy
|
|
58
58
|
|
|
59
|
-
Hand the assembled tree to `cloudflare:site-deploy`: it builds nothing for a static tree, runs `wrangler pages deploy public --project-name <project> --branch=main`, attaches the booking domain (creating the DNS record with the `<brand>-dns` token when the attach leaves validation pending), and reuses the same per-scope token. Everything in `site-deploy`'s auth, custom-domain, and DNS guidance applies unchanged.
|
|
59
|
+
Hand the assembled tree to `cloudflare:site-deploy`: it builds nothing for a static tree, runs `wrangler pages deploy public --project-name <project> --branch=main`, attaches the booking domain (creating the DNS record with the `<brand>-dns` token when the attach leaves validation pending), and reuses the same per-scope token. Everything in `site-deploy`'s auth, custom-domain, and DNS guidance applies unchanged — including its multi-tenant branch: "admin-run" means a `role:"admin"` session, which on a multi-tenant install is **not** the same thing as the house. A client account's admin session is denied the `pages` scope (`reason=not-house`) and publishes with `storage-pages-deploy` instead.
|
|
60
60
|
|
|
61
61
|
## Done-gate
|
|
62
62
|
|
|
@@ -43,11 +43,25 @@ Collect from the operator the portal's custom domain and the list of people, the
|
|
|
43
43
|
`<accountDir>/data-portal.json` carrying `portalDomain`, `portalDbName`, `bucketName`, and a
|
|
44
44
|
`people` array of `{ name, ownerId, prefix }`.
|
|
45
45
|
|
|
46
|
-
For each person
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
For each person:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node platform/plugins/cloudflare/bin/portal-enrol.mjs --owner <ownerId> --name <name> \
|
|
50
|
+
| npx wrangler d1 execute <portalDbName> --remote --file=-
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The script mints the passcode and prints it **once to stderr**, so it reaches the operator's terminal
|
|
54
|
+
and never the pipe; stdout carries the `INSERT` alone. Do not hand-roll this: the passcode's length
|
|
55
|
+
and alphabet are pinned in `template/functions/api/_lib/passcode.mjs`, the script is what applies
|
|
56
|
+
them, and a passcode minted any other way is unpinned by definition. The script refuses an `ownerId`
|
|
57
|
+
the isolation gate would reject, so a bad name fails here rather than after the row lands.
|
|
58
|
+
|
|
59
|
+
The statement is an upsert on `ownerId`, which is what makes rotation a re-run rather than a
|
|
60
|
+
constraint error.
|
|
61
|
+
|
|
62
|
+
**A passcode is never stored in plaintext** — not in `data-portal.json`, not in a log line, nowhere
|
|
63
|
+
on the device. The operator conveys it to the person out of band. Rotating a passcode is a re-run of
|
|
64
|
+
the script for that person; there is no separate editor.
|
|
51
65
|
|
|
52
66
|
Rotation also revokes: a session records a digest of the person's passcode hash at sign-in, and the
|
|
53
67
|
session lookup requires it to still match, so re-running enrolment kills every live session for that
|
|
@@ -74,23 +88,12 @@ Hand the assembled tree to `cloudflare:site-deploy`. Both bindings — D1 and R2
|
|
|
74
88
|
config in `wrangler.toml`, carried in the deploy payload under the Pages scope; the deploy needs no
|
|
75
89
|
R2 token. Only the bucket *creation* in step 1 needs R2 write, and the broker holds that.
|
|
76
90
|
|
|
77
|
-
## The portal needs a paid Cloudflare plan
|
|
78
|
-
|
|
79
|
-
Check this before promising a date. Signing in costs one PBKDF2 derivation at the OWASP floor of
|
|
80
|
-
600,000 iterations, measured at roughly 250ms of CPU. Workers Free allows **10ms of CPU per
|
|
81
|
-
request**, so on a free plan authentication cannot succeed — and no OWASP-compliant setting fits
|
|
82
|
-
either, since even 100,000 iterations costs about 40ms. Paid plans allow far more and are fine.
|
|
83
|
-
|
|
84
|
-
The cost is tunable without a migration if it ever needs to be: `iterations` is stored per person
|
|
85
|
-
row, so it is the enrolment-time value that binds, not a constant baked into the deployed code.
|
|
86
|
-
|
|
87
91
|
## Done-gate
|
|
88
92
|
|
|
89
93
|
The portal is not done until all five hold on the live domain:
|
|
90
94
|
|
|
91
95
|
1. A cache-busted request to the portal domain returns `HTTP/2 200`.
|
|
92
|
-
2. Signing in with the right passcode returns `200
|
|
93
|
-
out — this is the check that catches a free-plan deployment, and it fails nowhere else.
|
|
96
|
+
2. Signing in with the right passcode returns `200`.
|
|
94
97
|
3. A wrong passcode returns `401`; the **sixth** rapid wrong passcode returns `429` (five are
|
|
95
98
|
allowed per fifteen minutes, per person, per IP).
|
|
96
99
|
4. An upload as one person returns `200`, and the object is present in R2 under that person's
|
|
@@ -100,8 +103,7 @@ The portal is not done until all five hold on the live domain:
|
|
|
100
103
|
|
|
101
104
|
A `502` on `/api/upload` with `op=r2-put result=failed` on every attempt is the expected signature of
|
|
102
105
|
an R2 binding that did not deploy — check `wrangler.toml` reached the tree with its placeholder
|
|
103
|
-
filled.
|
|
104
|
-
above, not a code fault.
|
|
106
|
+
filled.
|
|
105
107
|
|
|
106
108
|
Uploads are capped at 25 MiB and a larger one returns `413` with `result=too-large`.
|
|
107
109
|
|