@rubytech/create-maxy-code 0.1.444 → 0.1.445
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/package.json +1 -1
- package/payload/platform/lib/graph-style/dist/index.d.ts +4 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +37 -1
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +39 -0
- package/payload/platform/lib/graph-style/src/index.ts +41 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +46 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.js +27 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +51 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.js +39 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.js +59 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.js +77 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.d.ts +2 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js +23 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +19 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/audit.js +41 -0
- package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/authorize.d.ts +11 -0
- package/payload/platform/lib/storage-broker/dist/authorize.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/authorize.js +17 -0
- package/payload/platform/lib/storage-broker/dist/authorize.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.d.ts +25 -0
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js +131 -0
- package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +20 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js +53 -0
- package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/house-credential.d.ts +6 -0
- package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/house-credential.js +33 -0
- package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts +7 -0
- package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/index.js +30 -0
- package/payload/platform/lib/storage-broker/dist/index.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/registry.d.ts +18 -0
- package/payload/platform/lib/storage-broker/dist/registry.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/registry.js +45 -0
- package/payload/platform/lib/storage-broker/dist/registry.js.map +1 -0
- package/payload/platform/lib/storage-broker/dist/remediate.d.ts +7 -0
- package/payload/platform/lib/storage-broker/dist/remediate.d.ts.map +1 -0
- package/payload/platform/lib/storage-broker/dist/remediate.js +25 -0
- package/payload/platform/lib/storage-broker/dist/remediate.js.map +1 -0
- package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +49 -0
- package/payload/platform/lib/storage-broker/src/__tests__/authorize.test.ts +28 -0
- package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +52 -0
- package/payload/platform/lib/storage-broker/src/__tests__/house-credential.test.ts +35 -0
- package/payload/platform/lib/storage-broker/src/__tests__/registry.test.ts +64 -0
- package/payload/platform/lib/storage-broker/src/__tests__/remediate-run.test.ts +79 -0
- package/payload/platform/lib/storage-broker/src/__tests__/remediate.test.ts +21 -0
- package/payload/platform/lib/storage-broker/src/audit.ts +55 -0
- package/payload/platform/lib/storage-broker/src/authorize.ts +22 -0
- package/payload/platform/lib/storage-broker/src/bin/remediate-run.ts +156 -0
- package/payload/platform/lib/storage-broker/src/cf-exec.ts +73 -0
- package/payload/platform/lib/storage-broker/src/house-credential.ts +32 -0
- package/payload/platform/lib/storage-broker/src/index.ts +14 -0
- package/payload/platform/lib/storage-broker/src/registry.ts +85 -0
- package/payload/platform/lib/storage-broker/src/remediate.ts +31 -0
- package/payload/platform/lib/storage-broker/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
- package/payload/platform/plugins/cloudflare/PLUGIN.md +4 -1
- package/payload/platform/plugins/cloudflare/bin/__tests__/cf-schema-check.test.sh +128 -0
- package/payload/platform/plugins/cloudflare/bin/__tests__/cf-store-name.test.sh +77 -0
- package/payload/platform/plugins/cloudflare/bin/cf-schema-check.sh +60 -0
- package/payload/platform/plugins/cloudflare/bin/cf-store-name.sh +37 -0
- package/payload/platform/plugins/cloudflare/bin/remediate-storage-isolation.sh +36 -0
- package/payload/platform/plugins/cloudflare/references/api.md +4 -0
- package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +58 -0
- package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +39 -0
- package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +2 -0
- package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +11 -4
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +15 -10
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/storage-broker/.claude-plugin/plugin.json +21 -0
- package/payload/platform/plugins/storage-broker/PLUGIN.md +48 -0
- package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +193 -0
- package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/package.json +3 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.d.ts +2 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.js +16 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/call-api.d.ts +8 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/call-api.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/call-api.js +39 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/call-api.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.d.ts +7 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.js +16 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.d.ts +8 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js +98 -0
- package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -0
- package/payload/platform/plugins/storage-broker/mcp/package.json +10 -0
- package/payload/platform/plugins/storage-broker/mcp/vitest.config.ts +4 -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 +6 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/server/chunk-4W6IEUFE.js +175 -0
- package/payload/server/public/assets/{AdminLoginScreens-BJVwtpLH.js → AdminLoginScreens-D9L-CuWi.js} +1 -1
- package/payload/server/public/assets/{AdminShell-CMu8_Xwn.js → AdminShell-DSQrvZXq.js} +1 -1
- package/payload/server/public/assets/{Checkbox-CzD0ORqA.js → Checkbox-CFdt8ljo.js} +1 -1
- package/payload/server/public/assets/{admin-D0yk2LMq.js → admin-Ch4n-G0j.js} +1 -1
- package/payload/server/public/assets/{browser-CvfbgznO.js → browser-B2AIxiuk.js} +1 -1
- package/payload/server/public/assets/{calendar-fw4WethJ.js → calendar-BvItH3B0.js} +1 -1
- package/payload/server/public/assets/chat-eQFMaX9y.js +1 -0
- package/payload/server/public/assets/chevron-left-D2AnCEyE.js +1 -0
- package/payload/server/public/assets/data-BzOmbHoe.js +1 -0
- package/payload/server/public/assets/{file-download-Px4s0Bhp.js → file-download-CK9j_Jx2.js} +1 -1
- package/payload/server/public/assets/graph-DrE7KupQ.js +51 -0
- package/payload/server/public/assets/graph-labels-CwEtvIRV.js +1 -0
- package/payload/server/public/assets/{maximize-2-BwphzI9S.js → maximize-2-CuzEom3K.js} +1 -1
- package/payload/server/public/assets/{operator-88m0DMl2.js → operator-DlboYPPu.js} +1 -1
- package/payload/server/public/assets/page-BuWH5gNK.js +1 -0
- package/payload/server/public/assets/page-Co9E8U5x.js +32 -0
- package/payload/server/public/assets/{public-BX2MjsFY.js → public-DtFpPMTG.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-C7BqkMec.js → rotate-ccw-Cqp4oqHV.js} +1 -1
- package/payload/server/public/assets/{tasks-DlgiCafL.js → tasks-bQKOhMga.js} +1 -1
- package/payload/server/public/assets/{time-entry-format-CyNYVDkq.js → time-entry-format-BIL4ZaDK.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-rB8QuqyH.js +1 -0
- package/payload/server/public/assets/{useSubAccountSwitcher-1izQvPiD.css → useSubAccountSwitcher-CVHKTSst.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-XgpWvXpk.js → useVoiceRecorder-Ctny8qWx.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 +1017 -828
- package/payload/server/src-BQ2NYMOT.js +24 -0
- package/payload/server/public/assets/chat-Dq-X-ia8.js +0 -1
- package/payload/server/public/assets/chevron-left-B3xSeEDR.js +0 -1
- package/payload/server/public/assets/data-CUnDzDox.js +0 -1
- package/payload/server/public/assets/graph-DbT8nA6R.js +0 -51
- package/payload/server/public/assets/graph-labels-C1YIyLiA.js +0 -1
- package/payload/server/public/assets/page-BRfxFXyi.js +0 -32
- package/payload/server/public/assets/page-CfQFAJia.js +0 -1
- package/payload/server/public/assets/useCopyFeedback-ByGUMN94.js +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-s6WlRn52.js → useSubAccountSwitcher-CHEt_m6q.js} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The only place the account-wide Cloudflare credential is used to touch
|
|
2
|
+
// Cloudflare. Every method injects the house token into the child env and shells
|
|
3
|
+
// wrangler. `run` is injectable so the wiring is unit-testable without wrangler.
|
|
4
|
+
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import type { HouseCredential } from "./house-credential.js";
|
|
7
|
+
|
|
8
|
+
export type RunFn = (
|
|
9
|
+
cmd: string,
|
|
10
|
+
args: string[],
|
|
11
|
+
env: Record<string, string>,
|
|
12
|
+
) => Promise<{ stdout: string }>;
|
|
13
|
+
|
|
14
|
+
export interface CfExec {
|
|
15
|
+
d1List(): Promise<{ name: string; uuid: string }[]>;
|
|
16
|
+
d1Create(name: string): Promise<{ uuid: string }>;
|
|
17
|
+
d1Query(name: string, sql: string): Promise<unknown>;
|
|
18
|
+
r2List(): Promise<{ name: string }[]>;
|
|
19
|
+
r2Create(name: string): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultRun: RunFn = (cmd, args, env) =>
|
|
23
|
+
new Promise((resolve, reject) => {
|
|
24
|
+
execFile(cmd, args, { env }, (err, stdout, stderr) => {
|
|
25
|
+
if (err) {
|
|
26
|
+
reject(new Error(`${cmd} ${args.join(" ")} failed: ${err.message}\n${stderr}`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
resolve({ stdout: stdout.toString() });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function parseJson(raw: string, context: string): unknown {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`storage-broker: ${context} returned non-JSON output: ${raw}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function makeCfExec(cred: HouseCredential, run: RunFn = defaultRun): CfExec {
|
|
42
|
+
const env = {
|
|
43
|
+
...process.env,
|
|
44
|
+
CLOUDFLARE_API_TOKEN: cred.apiToken,
|
|
45
|
+
CLOUDFLARE_ACCOUNT_ID: cred.accountId,
|
|
46
|
+
} as Record<string, string>;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
async d1List() {
|
|
50
|
+
const { stdout } = await run("wrangler", ["d1", "list", "--json"], env);
|
|
51
|
+
return parseJson(stdout, "d1 list") as { name: string; uuid: string }[];
|
|
52
|
+
},
|
|
53
|
+
async d1Create(name) {
|
|
54
|
+
const { stdout } = await run("wrangler", ["d1", "create", name, "--json"], env);
|
|
55
|
+
return parseJson(stdout, "d1 create") as { uuid: string };
|
|
56
|
+
},
|
|
57
|
+
async d1Query(name, sql) {
|
|
58
|
+
const { stdout } = await run(
|
|
59
|
+
"wrangler",
|
|
60
|
+
["d1", "execute", name, "--remote", "--json", "--command", sql],
|
|
61
|
+
env,
|
|
62
|
+
);
|
|
63
|
+
return parseJson(stdout, "d1 execute");
|
|
64
|
+
},
|
|
65
|
+
async r2List() {
|
|
66
|
+
const { stdout } = await run("wrangler", ["r2", "bucket", "list", "--json"], env);
|
|
67
|
+
return parseJson(stdout, "r2 bucket list") as { name: string }[];
|
|
68
|
+
},
|
|
69
|
+
async r2Create(name) {
|
|
70
|
+
await run("wrangler", ["r2", "bucket", "create", name], env);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Reads the single account-wide Cloudflare credential from the house-only
|
|
2
|
+
// config directory. This file is the one place the account-wide capability
|
|
3
|
+
// lives; sub-account spawns never source it. Parses KEY=value lines without
|
|
4
|
+
// polluting process.env (same discipline as reconcile-bookings' readEnvFile).
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
export interface HouseCredential {
|
|
10
|
+
apiToken: string;
|
|
11
|
+
accountId: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readHouseCredential(platformRoot: string): HouseCredential {
|
|
15
|
+
const path = join(platformRoot, "config", "cloudflare-house.env");
|
|
16
|
+
let raw: string;
|
|
17
|
+
try {
|
|
18
|
+
raw = readFileSync(path, "utf8");
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(`storage-broker: house credential not found at ${path}`);
|
|
21
|
+
}
|
|
22
|
+
const env: Record<string, string> = {};
|
|
23
|
+
for (const line of raw.split("\n")) {
|
|
24
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
25
|
+
if (m) env[m[1]] = m[2].trim();
|
|
26
|
+
}
|
|
27
|
+
const apiToken = env.CLOUDFLARE_API_TOKEN;
|
|
28
|
+
const accountId = env.CLOUDFLARE_ACCOUNT_ID;
|
|
29
|
+
if (!apiToken) throw new Error(`storage-broker: CLOUDFLARE_API_TOKEN missing in ${path}`);
|
|
30
|
+
if (!accountId) throw new Error(`storage-broker: CLOUDFLARE_ACCOUNT_ID missing in ${path}`);
|
|
31
|
+
return { apiToken, accountId };
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// storage-broker: the per-account Cloudflare D1/R2 isolation core.
|
|
2
|
+
//
|
|
3
|
+
// The lib is pure and dependency-free at runtime: it opens no Neo4j connection
|
|
4
|
+
// and shells no process on its own. Consumers inject a SessionLike (the ui
|
|
5
|
+
// route uses neo4j-store's getSession; the remediation entry opens its own
|
|
6
|
+
// driver) and, for cf-exec, a RunFn. That keeps every unit testable in
|
|
7
|
+
// isolation and the isolation logic auditable in one place.
|
|
8
|
+
|
|
9
|
+
export * from "./authorize.js";
|
|
10
|
+
export * from "./registry.js";
|
|
11
|
+
export * from "./house-credential.js";
|
|
12
|
+
export * from "./cf-exec.js";
|
|
13
|
+
export * from "./audit.js";
|
|
14
|
+
export * from "./remediate.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The ownership registry: which account owns which Cloudflare storage resource.
|
|
2
|
+
//
|
|
3
|
+
// Written with raw cypher MERGE stamped with system provenance, mirroring the
|
|
4
|
+
// house-level seedAccountGraphRoot path in the admin plugin. It deliberately
|
|
5
|
+
// does NOT go through graph-write's writeNodeWithEdges: that primitive's
|
|
6
|
+
// accountId floor rejects a write whose props.accountId differs from the
|
|
7
|
+
// writer process's ACCOUNT_ID, and the broker runs house-level, writing nodes
|
|
8
|
+
// owned by many different sub-accounts. The owning accountId here is always the
|
|
9
|
+
// trusted header value the service resolved, never an agent argument.
|
|
10
|
+
|
|
11
|
+
// A minimal shape of a neo4j-driver Session, so this lib carries no runtime or
|
|
12
|
+
// type dependency on neo4j-driver (mirrors the SeedSessionLike pattern used in
|
|
13
|
+
// platform/plugins/admin/mcp/src/tools/account-lifecycle.ts).
|
|
14
|
+
// Uses PromiseLike (not Promise) and get(): any so that a real neo4j-driver
|
|
15
|
+
// Session — whose run() returns a Result (a PromiseLike, and whose records are
|
|
16
|
+
// driver Records) — is structurally assignable under both v5 and v6 without a
|
|
17
|
+
// cast at the call site.
|
|
18
|
+
export interface SessionLike {
|
|
19
|
+
run(
|
|
20
|
+
cypher: string,
|
|
21
|
+
params?: Record<string, unknown>,
|
|
22
|
+
): PromiseLike<{ records: Array<{ get(key: string): any }> }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ResourceKind = "d1" | "r2";
|
|
26
|
+
|
|
27
|
+
export interface StorageResource {
|
|
28
|
+
accountId: string;
|
|
29
|
+
kind: ResourceKind;
|
|
30
|
+
name: string;
|
|
31
|
+
cfResourceId: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function registerResource(
|
|
35
|
+
session: SessionLike,
|
|
36
|
+
r: StorageResource,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
// MERGE on (kind, name) — the ownership key. On a shared Cloudflare account a
|
|
39
|
+
// storage name maps to exactly one owner, so the write key must match the
|
|
40
|
+
// resolveOwner lookup key; keying on accountId too would let two accounts hold
|
|
41
|
+
// a node for the same name and make ownership ambiguous. accountId is set only
|
|
42
|
+
// ON CREATE and never overwritten ON MATCH, so a register can never silently
|
|
43
|
+
// reassign an existing resource to a different account.
|
|
44
|
+
await session.run(
|
|
45
|
+
`MERGE (r:StorageResource {kind: $kind, name: $name})
|
|
46
|
+
ON CREATE SET r.accountId = $accountId,
|
|
47
|
+
r.createdByAgent = 'system',
|
|
48
|
+
r.createdBySource = 'storage-broker',
|
|
49
|
+
r.cfResourceId = $cfResourceId
|
|
50
|
+
ON MATCH SET r.cfResourceId = $cfResourceId`,
|
|
51
|
+
{ accountId: r.accountId, kind: r.kind, name: r.name, cfResourceId: r.cfResourceId },
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function resolveOwner(
|
|
56
|
+
session: SessionLike,
|
|
57
|
+
kind: ResourceKind,
|
|
58
|
+
name: string,
|
|
59
|
+
): Promise<string | null> {
|
|
60
|
+
const res = await session.run(
|
|
61
|
+
`MATCH (r:StorageResource {kind: $kind, name: $name})
|
|
62
|
+
RETURN r.accountId AS accountId LIMIT 1`,
|
|
63
|
+
{ kind, name },
|
|
64
|
+
);
|
|
65
|
+
return res.records.length ? (res.records[0].get("accountId") as string) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function listResources(
|
|
69
|
+
session: SessionLike,
|
|
70
|
+
accountId: string,
|
|
71
|
+
kind: ResourceKind,
|
|
72
|
+
): Promise<StorageResource[]> {
|
|
73
|
+
const res = await session.run(
|
|
74
|
+
`MATCH (r:StorageResource {accountId: $accountId, kind: $kind})
|
|
75
|
+
RETURN r.name AS name, r.cfResourceId AS cfResourceId
|
|
76
|
+
ORDER BY r.name`,
|
|
77
|
+
{ accountId, kind },
|
|
78
|
+
);
|
|
79
|
+
return res.records.map((rec) => ({
|
|
80
|
+
accountId,
|
|
81
|
+
kind,
|
|
82
|
+
name: rec.get("name") as string,
|
|
83
|
+
cfResourceId: rec.get("cfResourceId") as string,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// The testable core of the operator-run remediation. The mapping from existing
|
|
2
|
+
// resource name to owning account is operator-authored, and validation fails
|
|
3
|
+
// closed on any resource absent from it: ownership is a recorded fact, never a
|
|
4
|
+
// name-prefix guess. Token stripping removes every account-wide credential from
|
|
5
|
+
// a sub-account secrets file while preserving everything else verbatim.
|
|
6
|
+
|
|
7
|
+
export interface MappingResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
missing: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Account-wide credentials stripped from a sub-account's cloudflare.env.
|
|
13
|
+
// CLOUDFLARE_ACCOUNT_ID is intentionally kept: it is an identifier, not a
|
|
14
|
+
// credential, and does not grant access on its own.
|
|
15
|
+
const STRIP_KEYS = ["CLOUDFLARE_API_TOKEN", "CF_PAGES_D1_TOKEN", "CF_PAGES_TOKEN"];
|
|
16
|
+
|
|
17
|
+
export function validateMapping(
|
|
18
|
+
enumerated: string[],
|
|
19
|
+
mapping: Record<string, string>,
|
|
20
|
+
): MappingResult {
|
|
21
|
+
const missing = enumerated.filter((name) => !(name in mapping));
|
|
22
|
+
return { ok: missing.length === 0, missing };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function stripAccountWideTokens(contents: string): string {
|
|
26
|
+
const kept = contents.split("\n").filter((line) => {
|
|
27
|
+
const key = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/)?.[1];
|
|
28
|
+
return !(key && STRIP_KEYS.includes(key));
|
|
29
|
+
});
|
|
30
|
+
return kept.join("\n");
|
|
31
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"services/*"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/active-rules/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/tsconfig.json && tsc -p services/telegram-channel/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/active-rules/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/active-rules/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p lib/storage-broker/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/tsconfig.json && tsc -p services/telegram-channel/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/active-rules/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p lib/storage-broker/tsconfig.json",
|
|
11
11
|
"gen:canonical-tools": "node scripts/generate-canonical-tool-names.mjs",
|
|
12
12
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
13
13
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
@@ -40,7 +40,8 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is th
|
|
|
40
40
|
|---|---|
|
|
41
41
|
| [manual-setup.md](references/manual-setup.md) | Tunnel runbook — Steps 0–7 with isolated `cloudflared` command blocks. The agent reads the relevant step before issuing each command. |
|
|
42
42
|
| [api.md](references/api.md) | Cloudflare API library — canonical docs URL + curated endpoint map (DNS, zones, tunnels, token list/create/verify/revoke), the advanced master-token creation walkthrough, the account-scoped secrets-file storage convention, the agent's reuse-a-stable-per-scope-token discipline, and the standing reconcile pass that revokes strays. |
|
|
43
|
-
| [d1-data-capture.md](references/d1-data-capture.md) | Form → Pages Function → D1 store → read/sweep. The Pages-Edit **and** D1-Edit token-scope requirement, `wrangler d1 create`/`execute --remote`, the `swept` column. |
|
|
43
|
+
| [d1-data-capture.md](references/d1-data-capture.md) | Form → Pages Function → D1 store → read/sweep. The Pages-Edit **and** D1-Edit token-scope requirement, `wrangler d1 create`/`execute --remote`, the `swept` column, plus the establishment discipline: `<accountId>-<purpose>` naming, one store per purpose, and the canonical `acceptances` schema. |
|
|
44
|
+
| [r2-object-storage.md](references/r2-object-storage.md) | R2 buckets under the same establishment discipline as D1: `<accountId>-<purpose>` naming via `cf-store-name.sh`, one bucket per purpose, `wrangler r2 bucket create`, and the store-provision breadcrumb. |
|
|
44
45
|
| [hosting-sites.md](references/hosting-sites.md) | Deploy a static or Next.js site to Cloudflare Pages via `wrangler pages deploy`. |
|
|
45
46
|
| [web-analytics.md](references/web-analytics.md) | Why a registered site reports 0 visitors — the RUM API is blocked (no permission group), registered ≠ collecting, edge auto-injection is impossible for a Pages-only custom domain, and the beacon must be injected into the site HTML, rebuilt, and redeployed. Outcome: beacon present in live HTML. |
|
|
46
47
|
| [dashboard-guide.md](references/dashboard-guide.md) | Click-paths for operations the operator prefers to do by hand — sign in, switch accounts, add a site, edit an apex CNAME, verify nameservers, delete a tunnel, manage CNAME records, author an Access policy. |
|
|
@@ -53,6 +54,8 @@ The agent loads these references on demand via `plugin-read` as the conversation
|
|
|
53
54
|
| Helper | Purpose |
|
|
54
55
|
|---|---|
|
|
55
56
|
| [bin/cf-token.sh](bin/cf-token.sh) | Deterministic per-scope token resolver. `bash bin/cf-token.sh <scope> <secrets-file>` (scope: `pages`, `d1`, `dns`, `access`) reads the master's `cfat_`/`cfut_` prefix, routes verify/`permission_groups`/mint to the matching endpoint family, reuses a persisted token or mints once if absent (no expiry), verifies through the transient post-mint window, persists to the secrets file, and prints only the secrets key name on stdout (never a token value). It removes the endpoint-routing decision from the agent, which is what the false-negative `Invalid API Token` / `9109` failures turned on. curl + jq only; exit-code gated. |
|
|
57
|
+
| [bin/cf-store-name.sh](bin/cf-store-name.sh) | Deterministic storage-name resolver. `bash bin/cf-store-name.sh <accountId> <purpose>` prints the canonical `<accountId>-<purpose>` name (nothing else) for a D1 database or R2 bucket, so a store's name identifies its owner and purpose. Lowercases and validates each segment to `[a-z0-9-]`; fails closed on empty or invalid input. No network, no secrets. |
|
|
58
|
+
| [bin/cf-schema-check.sh](bin/cf-schema-check.sh) | Canonical acceptance-schema gate. Feed it the stored DDL (`wrangler d1 execute --json --command "SELECT sql FROM sqlite_master WHERE sql IS NOT NULL" \| jq -r '.[0].results[].sql'`); exit 0 iff the store carries the one canonical `acceptances` shape (`doc_ref`, `doc_type`, `party`, `signer_name`, `accepted_at`, `swept`; no stray `dual_acceptances` table; `signer_name` not a bare `name`), exit 1 with the first drift named on stderr. Exit-code gated. |
|
|
56
59
|
|
|
57
60
|
### Success contract
|
|
58
61
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for cf-schema-check.sh - the canonical acceptance-schema
|
|
3
|
+
# validator. Input on stdin is the DDL text a `SELECT sql FROM sqlite_master`
|
|
4
|
+
# returns; exit 0 iff the store matches the one canonical `acceptances` shape,
|
|
5
|
+
# non-zero on any drift. No network, no D1.
|
|
6
|
+
#
|
|
7
|
+
# Covers:
|
|
8
|
+
# 1. canonical shape -> exit 0
|
|
9
|
+
# 2. canonical shape + an unrelated leads table -> exit 0 (extra tables fine)
|
|
10
|
+
# 3. bare `name` instead of signer_name -> exit 1, stderr names signer drift
|
|
11
|
+
# 4. missing swept column -> exit 1, stderr names swept
|
|
12
|
+
# 5. a stray dual_acceptances table -> exit 1, stderr names it
|
|
13
|
+
# 6. no acceptances table (only leads) -> exit 1, stderr "no acceptances table"
|
|
14
|
+
set -u
|
|
15
|
+
|
|
16
|
+
HELPER="$(cd "$(dirname "$0")/.." && pwd)/cf-schema-check.sh"
|
|
17
|
+
[ -x "$HELPER" ] || { echo "FAIL: $HELPER not executable" >&2; exit 1; }
|
|
18
|
+
|
|
19
|
+
PASS=0; FAIL=0
|
|
20
|
+
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
|
21
|
+
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }
|
|
22
|
+
|
|
23
|
+
# Canonical acceptance DDL (the authority declared in d1-data-capture.md).
|
|
24
|
+
CANON='CREATE TABLE IF NOT EXISTS acceptances (
|
|
25
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
26
|
+
doc_ref TEXT NOT NULL,
|
|
27
|
+
doc_type TEXT NOT NULL,
|
|
28
|
+
party TEXT,
|
|
29
|
+
signer_name TEXT,
|
|
30
|
+
accepted_at TEXT NOT NULL DEFAULT (datetime('"'"'now'"'"')),
|
|
31
|
+
swept INTEGER NOT NULL DEFAULT 0
|
|
32
|
+
);'
|
|
33
|
+
|
|
34
|
+
LEADS='CREATE TABLE leads (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
email TEXT NOT NULL,
|
|
37
|
+
swept INTEGER NOT NULL DEFAULT 0
|
|
38
|
+
);'
|
|
39
|
+
|
|
40
|
+
# Run helper with stdin=$1. Sets ERR/RC.
|
|
41
|
+
run() {
|
|
42
|
+
local ef; ef=$(mktemp)
|
|
43
|
+
printf '%s\n' "$1" | bash "$HELPER" 2>"$ef" >/dev/null
|
|
44
|
+
RC=$?
|
|
45
|
+
ERR=$(cat "$ef"); rm -f "$ef"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# 1. canonical -> 0
|
|
49
|
+
run "$CANON"
|
|
50
|
+
[ "$RC" -eq 0 ] && ok "canonical shape accepted (exit=$RC)" || bad "canonical rejected (exit=$RC err='$ERR')"
|
|
51
|
+
|
|
52
|
+
# 2. canonical + leads -> 0
|
|
53
|
+
run "$CANON
|
|
54
|
+
$LEADS"
|
|
55
|
+
[ "$RC" -eq 0 ] && ok "canonical plus unrelated table accepted" || bad "extra table rejected (exit=$RC err='$ERR')"
|
|
56
|
+
|
|
57
|
+
# 3. bare name drift -> 1, mentions signer
|
|
58
|
+
DRIFT_NAME='CREATE TABLE acceptances (
|
|
59
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
+
doc_ref TEXT NOT NULL,
|
|
61
|
+
doc_type TEXT NOT NULL,
|
|
62
|
+
party TEXT,
|
|
63
|
+
name TEXT,
|
|
64
|
+
accepted_at TEXT NOT NULL,
|
|
65
|
+
swept INTEGER NOT NULL DEFAULT 0
|
|
66
|
+
);'
|
|
67
|
+
run "$DRIFT_NAME"
|
|
68
|
+
{ [ "$RC" -ne 0 ] && printf '%s' "$ERR" | grep -qi "signer"; } \
|
|
69
|
+
&& ok "bare name rejected, signer drift named (exit=$RC)" \
|
|
70
|
+
|| bad "bare name drift (exit=$RC err='$ERR')"
|
|
71
|
+
|
|
72
|
+
# 4. missing swept -> 1, mentions swept
|
|
73
|
+
DRIFT_SWEPT='CREATE TABLE acceptances (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
doc_ref TEXT NOT NULL,
|
|
76
|
+
doc_type TEXT NOT NULL,
|
|
77
|
+
party TEXT,
|
|
78
|
+
signer_name TEXT,
|
|
79
|
+
accepted_at TEXT NOT NULL
|
|
80
|
+
);'
|
|
81
|
+
run "$DRIFT_SWEPT"
|
|
82
|
+
{ [ "$RC" -ne 0 ] && printf '%s' "$ERR" | grep -qi "swept"; } \
|
|
83
|
+
&& ok "missing swept rejected, swept named (exit=$RC)" \
|
|
84
|
+
|| bad "missing swept (exit=$RC err='$ERR')"
|
|
85
|
+
|
|
86
|
+
# 4b. acceptances missing swept, but a sibling leads table HAS swept -> still
|
|
87
|
+
# exit 1. Column checks must be scoped to the acceptances table, not bleed
|
|
88
|
+
# across the whole dump.
|
|
89
|
+
DRIFT_SWEPT_BLEED='CREATE TABLE acceptances (
|
|
90
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
91
|
+
doc_ref TEXT NOT NULL,
|
|
92
|
+
doc_type TEXT NOT NULL,
|
|
93
|
+
party TEXT,
|
|
94
|
+
signer_name TEXT,
|
|
95
|
+
accepted_at TEXT NOT NULL
|
|
96
|
+
);
|
|
97
|
+
CREATE TABLE leads (
|
|
98
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
99
|
+
email TEXT NOT NULL,
|
|
100
|
+
swept INTEGER NOT NULL DEFAULT 0
|
|
101
|
+
);'
|
|
102
|
+
run "$DRIFT_SWEPT_BLEED"
|
|
103
|
+
{ [ "$RC" -ne 0 ] && printf '%s' "$ERR" | grep -qi "swept"; } \
|
|
104
|
+
&& ok "missing swept not masked by sibling table (exit=$RC)" \
|
|
105
|
+
|| bad "swept bleed across tables (exit=$RC err='$ERR')"
|
|
106
|
+
|
|
107
|
+
# 5. dual_acceptances stray -> 1, mentions it
|
|
108
|
+
DUAL="$CANON
|
|
109
|
+
CREATE TABLE dual_acceptances (
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
|
+
doc_ref TEXT NOT NULL,
|
|
112
|
+
party TEXT,
|
|
113
|
+
signer_name TEXT
|
|
114
|
+
);"
|
|
115
|
+
run "$DUAL"
|
|
116
|
+
{ [ "$RC" -ne 0 ] && printf '%s' "$ERR" | grep -qi "dual_acceptances"; } \
|
|
117
|
+
&& ok "stray dual_acceptances rejected and named (exit=$RC)" \
|
|
118
|
+
|| bad "dual_acceptances stray (exit=$RC err='$ERR')"
|
|
119
|
+
|
|
120
|
+
# 6. no acceptances table -> 1, mentions it
|
|
121
|
+
run "$LEADS"
|
|
122
|
+
{ [ "$RC" -ne 0 ] && printf '%s' "$ERR" | grep -qi "no acceptances"; } \
|
|
123
|
+
&& ok "missing acceptances table rejected and named (exit=$RC)" \
|
|
124
|
+
|| bad "no acceptances table (exit=$RC err='$ERR')"
|
|
125
|
+
|
|
126
|
+
echo "----"
|
|
127
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
128
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for cf-store-name.sh - the canonical <accountId>-<purpose>
|
|
3
|
+
# store-name helper. Pure string computation: no curl, no secrets, no network.
|
|
4
|
+
#
|
|
5
|
+
# Covers:
|
|
6
|
+
# 1. valid input -> <accountId>-<purpose> on stdout, exit 0
|
|
7
|
+
# 2. deterministic: identical inputs -> byte-identical output
|
|
8
|
+
# 3. mixed-case input is lowercased
|
|
9
|
+
# 4. a hyphenated purpose is preserved
|
|
10
|
+
# 5. empty accountId fails closed (exit 1)
|
|
11
|
+
# 6. empty purpose fails closed (exit 1)
|
|
12
|
+
# 7. an invalid character in a segment fails closed (exit 1)
|
|
13
|
+
# 8. no missing-arg or invalid case leaks a partial name to stdout
|
|
14
|
+
set -u
|
|
15
|
+
|
|
16
|
+
HELPER="$(cd "$(dirname "$0")/.." && pwd)/cf-store-name.sh"
|
|
17
|
+
[ -x "$HELPER" ] || { echo "FAIL: $HELPER not executable" >&2; exit 1; }
|
|
18
|
+
|
|
19
|
+
PASS=0; FAIL=0
|
|
20
|
+
ok() { echo "PASS: $1"; PASS=$((PASS+1)); }
|
|
21
|
+
bad() { echo "FAIL: $1" >&2; FAIL=$((FAIL+1)); }
|
|
22
|
+
|
|
23
|
+
# Run the helper. $1 name, then the helper argv is "$2".."$n" up to a trailing
|
|
24
|
+
# expected-exit sentinel passed via EXP. Sets OUT/ERR/RC.
|
|
25
|
+
run() {
|
|
26
|
+
local name="$1"; shift
|
|
27
|
+
local of ef
|
|
28
|
+
of=$(mktemp); ef=$(mktemp)
|
|
29
|
+
bash "$HELPER" "$@" >"$of" 2>"$ef"
|
|
30
|
+
RC=$?
|
|
31
|
+
OUT=$(cat "$of"); ERR=$(cat "$ef"); rm -f "$of" "$ef"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# 1. valid -> name, exit 0
|
|
35
|
+
run "valid" acc123 acceptances
|
|
36
|
+
{ [ "$RC" -eq 0 ] && [ "$OUT" = "acc123-acceptances" ]; } \
|
|
37
|
+
&& ok "valid input yields acc123-acceptances (exit=$RC)" \
|
|
38
|
+
|| bad "valid input (exit=$RC out='$OUT')"
|
|
39
|
+
|
|
40
|
+
# 2. deterministic
|
|
41
|
+
run "det1" acc123 acceptances; d1="$OUT"
|
|
42
|
+
run "det2" acc123 acceptances; d2="$OUT"
|
|
43
|
+
[ "$d1" = "$d2" ] && ok "deterministic identical output" || bad "non-deterministic ('$d1' vs '$d2')"
|
|
44
|
+
|
|
45
|
+
# 3. lowercases
|
|
46
|
+
run "case" Acc123 Acceptances
|
|
47
|
+
{ [ "$RC" -eq 0 ] && [ "$OUT" = "acc123-acceptances" ]; } \
|
|
48
|
+
&& ok "mixed case lowercased" || bad "case (exit=$RC out='$OUT')"
|
|
49
|
+
|
|
50
|
+
# 4. hyphenated purpose preserved
|
|
51
|
+
run "hyphen" acc123 quote-signing
|
|
52
|
+
{ [ "$RC" -eq 0 ] && [ "$OUT" = "acc123-quote-signing" ]; } \
|
|
53
|
+
&& ok "hyphenated purpose preserved" || bad "hyphen (exit=$RC out='$OUT')"
|
|
54
|
+
|
|
55
|
+
# 5. empty accountId fails closed
|
|
56
|
+
run "empty-acct" "" acceptances
|
|
57
|
+
{ [ "$RC" -ne 0 ] && [ -z "$OUT" ]; } \
|
|
58
|
+
&& ok "empty accountId fails closed (exit=$RC)" || bad "empty accountId (exit=$RC out='$OUT')"
|
|
59
|
+
|
|
60
|
+
# 6. empty purpose fails closed
|
|
61
|
+
run "empty-purpose" acc123 ""
|
|
62
|
+
{ [ "$RC" -ne 0 ] && [ -z "$OUT" ]; } \
|
|
63
|
+
&& ok "empty purpose fails closed (exit=$RC)" || bad "empty purpose (exit=$RC out='$OUT')"
|
|
64
|
+
|
|
65
|
+
# 7. invalid char fails closed
|
|
66
|
+
run "invalid" acc123 "accept!ances"
|
|
67
|
+
{ [ "$RC" -ne 0 ] && [ -z "$OUT" ]; } \
|
|
68
|
+
&& ok "invalid char fails closed (exit=$RC)" || bad "invalid char (exit=$RC out='$OUT')"
|
|
69
|
+
|
|
70
|
+
# 8. missing second arg fails closed, no partial name
|
|
71
|
+
run "missing-arg" acc123
|
|
72
|
+
{ [ "$RC" -ne 0 ] && [ -z "$OUT" ]; } \
|
|
73
|
+
&& ok "missing purpose arg fails closed (exit=$RC)" || bad "missing arg (exit=$RC out='$OUT')"
|
|
74
|
+
|
|
75
|
+
echo "----"
|
|
76
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
77
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cf-schema-check.sh - canonical acceptance-schema validator.
|
|
3
|
+
#
|
|
4
|
+
# The document-acceptance store (e-sign / works-agreement / quote-signing
|
|
5
|
+
# watchers) has ONE canonical shape, declared in
|
|
6
|
+
# references/d1-data-capture.md § The canonical acceptance store. This helper is
|
|
7
|
+
# the exit-code gate that confirms a live store follows it, so a watcher can
|
|
8
|
+
# rely on reading one known shape instead of three drifted ones.
|
|
9
|
+
#
|
|
10
|
+
# Input: stdin = the DDL text `wrangler d1 execute <db> --remote --command
|
|
11
|
+
# "SELECT sql FROM sqlite_master;"` returns (the SQLite-stored CREATE
|
|
12
|
+
# statements). This is machine-generated DDL, not prose, so token
|
|
13
|
+
# inspection is a deterministic pre-flight.
|
|
14
|
+
# Output: stdout = nothing on success. stderr = a [cf-schema-check] line naming
|
|
15
|
+
# the first failing check.
|
|
16
|
+
# Exit: 0 iff the input matches the canonical acceptance shape; 1 on any drift.
|
|
17
|
+
set -u
|
|
18
|
+
|
|
19
|
+
fail() { echo "[cf-schema-check] drift: $*" >&2; exit 1; }
|
|
20
|
+
|
|
21
|
+
ddl="$(cat)"
|
|
22
|
+
has() { printf '%s' "$ddl" | grep -Eiq "$1"; }
|
|
23
|
+
|
|
24
|
+
# 1. An `acceptances` table must exist. \bacceptances\b does not match inside
|
|
25
|
+
# `dual_acceptances` (the underscore suppresses the word boundary), so this
|
|
26
|
+
# is specific to the canonical table.
|
|
27
|
+
has 'create table[^;(]*\bacceptances\b' || fail "no acceptances table in the store"
|
|
28
|
+
|
|
29
|
+
# 2. No `dual_acceptances` table. Its presence is the two-signer sprawl the
|
|
30
|
+
# canonical `party` column replaces. Checked against the whole dump.
|
|
31
|
+
has '\bdual_acceptances\b' && fail "stray dual_acceptances table (use the party column, not a second table)"
|
|
32
|
+
|
|
33
|
+
# Isolate the `acceptances` CREATE statement so column checks are scoped to it,
|
|
34
|
+
# not bled across sibling tables (a leads table's `swept` must not satisfy the
|
|
35
|
+
# acceptances requirement). Print from the acceptances CREATE line to its `);`.
|
|
36
|
+
acc="$(printf '%s\n' "$ddl" | awk '
|
|
37
|
+
{ l = tolower($0) }
|
|
38
|
+
l ~ /create table/ && l ~ /acceptances/ && l !~ /dual_acceptances/ { f = 1 }
|
|
39
|
+
f { print }
|
|
40
|
+
f && /\);/ { exit }
|
|
41
|
+
')"
|
|
42
|
+
hasin() { printf '%s' "$acc" | grep -Eiq "$1"; }
|
|
43
|
+
|
|
44
|
+
# 3. The signer column is `signer_name`, never a bare `name`. \bname\b does not
|
|
45
|
+
# match inside `signer_name`, so a standalone `name` token with no
|
|
46
|
+
# `signer_name` present is the drift signature.
|
|
47
|
+
if ! hasin '\bsigner_name\b'; then
|
|
48
|
+
if hasin '\bname\b'; then
|
|
49
|
+
fail "signer column is bare 'name', expected 'signer_name'"
|
|
50
|
+
fi
|
|
51
|
+
fail "missing required column: signer_name"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# 4. Every canonical column present in the acceptances table. signer_name is
|
|
55
|
+
# already confirmed above.
|
|
56
|
+
for col in doc_ref doc_type party accepted_at swept; do
|
|
57
|
+
hasin "\\b${col}\\b" || fail "missing required column: ${col}"
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
exit 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cf-store-name.sh - deterministic Cloudflare storage-name resolver.
|
|
3
|
+
#
|
|
4
|
+
# Owns the one naming decision the plugin previously left to the agent: a D1
|
|
5
|
+
# database or R2 bucket is named <accountId>-<purpose>, so a store's name
|
|
6
|
+
# identifies its owner and purpose (references/d1-data-capture.md § Establishing
|
|
7
|
+
# a store, references/r2-object-storage.md). Same inputs always yield the same
|
|
8
|
+
# name; that determinism is what lets "does a store for this purpose already
|
|
9
|
+
# exist?" be a lookup rather than a guess.
|
|
10
|
+
#
|
|
11
|
+
# Usage: cf-store-name.sh <accountId> <purpose>
|
|
12
|
+
# Output: stdout = the canonical <accountId>-<purpose> name (nothing else).
|
|
13
|
+
# Exit: 0 when both segments are valid; non-zero (fail closed) otherwise.
|
|
14
|
+
#
|
|
15
|
+
# Pure string computation: no network, no secrets, no side effects. It does NOT
|
|
16
|
+
# create the store or decide create-vs-reuse; that decision and its
|
|
17
|
+
# op=storage-provision breadcrumb live at the create call site, because this
|
|
18
|
+
# helper cannot know whether the store already exists.
|
|
19
|
+
set -u
|
|
20
|
+
|
|
21
|
+
die() { echo "[cf-store-name] error: $*" >&2; exit 1; }
|
|
22
|
+
|
|
23
|
+
account="${1:-}"; purpose="${2:-}"
|
|
24
|
+
[ -n "$account" ] || die "accountId required; usage: cf-store-name.sh <accountId> <purpose>"
|
|
25
|
+
[ -n "$purpose" ] || die "purpose required; usage: cf-store-name.sh <accountId> <purpose>"
|
|
26
|
+
|
|
27
|
+
# Lowercase each segment (D1/R2 names are lowercase), then validate. An invalid
|
|
28
|
+
# character is an error, not a silent fixup - a name that does not round-trip to
|
|
29
|
+
# what the caller asked for would defeat the "name identifies owner+purpose"
|
|
30
|
+
# contract.
|
|
31
|
+
account="$(printf '%s' "$account" | tr '[:upper:]' '[:lower:]')"
|
|
32
|
+
purpose="$(printf '%s' "$purpose" | tr '[:upper:]' '[:lower:]')"
|
|
33
|
+
|
|
34
|
+
case "$account" in *[!a-z0-9-]*) die "accountId '$account' has a character outside [a-z0-9-]";; esac
|
|
35
|
+
case "$purpose" in *[!a-z0-9-]*) die "purpose '$purpose' has a character outside [a-z0-9-]";; esac
|
|
36
|
+
|
|
37
|
+
printf '%s-%s\n' "$account" "$purpose"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# remediate-storage-isolation.sh — one-shot, operator-run remediation of an
|
|
3
|
+
# existing multi-tenant install (Task 1631). Registers every existing D1
|
|
4
|
+
# database and R2 bucket to its owning account, relocates the master token into
|
|
5
|
+
# the house-only config file, and strips every account-wide token from every
|
|
6
|
+
# sub-account secrets file.
|
|
7
|
+
#
|
|
8
|
+
# The mapping file is operator-authored JSON: { "<resource-name>": "<accountId>" }.
|
|
9
|
+
# The node core fails closed on any enumerated resource the operator did not map;
|
|
10
|
+
# ownership is never guessed. This wrapper is a faithful relay: it gates on the
|
|
11
|
+
# mapping file existing and on the node exit code, and never echoes a token.
|
|
12
|
+
#
|
|
13
|
+
# Usage: PLATFORM_ROOT=~/<brand>-code/platform \
|
|
14
|
+
# NEO4J_URI=... NEO4J_PASSWORD=... \
|
|
15
|
+
# remediate-storage-isolation.sh <mapping-file.json>
|
|
16
|
+
set -u
|
|
17
|
+
|
|
18
|
+
log() { echo "[remediate] $*" >&2; }
|
|
19
|
+
die() { log "error: $*"; exit 1; }
|
|
20
|
+
|
|
21
|
+
MAPPING="${1:-}"
|
|
22
|
+
[ -n "$MAPPING" ] || die "usage: remediate-storage-isolation.sh <mapping-file.json>"
|
|
23
|
+
[ -r "$MAPPING" ] || die "mapping file not readable: '${MAPPING}'"
|
|
24
|
+
[ -n "${PLATFORM_ROOT:-}" ] || die "PLATFORM_ROOT not set"
|
|
25
|
+
|
|
26
|
+
ENTRY="${PLATFORM_ROOT}/lib/storage-broker/dist/bin/remediate-run.js"
|
|
27
|
+
[ -f "$ENTRY" ] || die "remediation entry not built: '${ENTRY}' (run the platform build first)"
|
|
28
|
+
|
|
29
|
+
log "op=remediate mapping=$(basename "$MAPPING") start"
|
|
30
|
+
node "$ENTRY" "$MAPPING"
|
|
31
|
+
rc=$?
|
|
32
|
+
if [ "$rc" -ne 0 ]; then
|
|
33
|
+
log "op=remediate result=failed code=${rc} (no partial write on a fail-closed abort)"
|
|
34
|
+
exit "$rc"
|
|
35
|
+
fi
|
|
36
|
+
log "op=remediate result=ok"
|