@rubytech/create-maxy-code 0.1.444 → 0.1.446

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-style/dist/index.d.ts +4 -0
  3. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-style/dist/index.js +37 -1
  5. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +39 -0
  7. package/payload/platform/lib/graph-style/src/index.ts +41 -0
  8. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.d.ts +2 -0
  9. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.d.ts.map +1 -0
  10. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +46 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -0
  12. package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.d.ts +2 -0
  13. package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.d.ts.map +1 -0
  14. package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.js +27 -0
  15. package/payload/platform/lib/storage-broker/dist/__tests__/authorize.test.js.map +1 -0
  16. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.d.ts +2 -0
  17. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.d.ts.map +1 -0
  18. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +51 -0
  19. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -0
  20. package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.d.ts +2 -0
  21. package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.d.ts.map +1 -0
  22. package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.js +39 -0
  23. package/payload/platform/lib/storage-broker/dist/__tests__/house-credential.test.js.map +1 -0
  24. package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.d.ts +2 -0
  25. package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.d.ts.map +1 -0
  26. package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.js +59 -0
  27. package/payload/platform/lib/storage-broker/dist/__tests__/registry.test.js.map +1 -0
  28. package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.d.ts +2 -0
  29. package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.d.ts.map +1 -0
  30. package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.js +77 -0
  31. package/payload/platform/lib/storage-broker/dist/__tests__/remediate-run.test.js.map +1 -0
  32. package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.d.ts +2 -0
  33. package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.d.ts.map +1 -0
  34. package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js +23 -0
  35. package/payload/platform/lib/storage-broker/dist/__tests__/remediate.test.js.map +1 -0
  36. package/payload/platform/lib/storage-broker/dist/audit.d.ts +19 -0
  37. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -0
  38. package/payload/platform/lib/storage-broker/dist/audit.js +41 -0
  39. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -0
  40. package/payload/platform/lib/storage-broker/dist/authorize.d.ts +11 -0
  41. package/payload/platform/lib/storage-broker/dist/authorize.d.ts.map +1 -0
  42. package/payload/platform/lib/storage-broker/dist/authorize.js +17 -0
  43. package/payload/platform/lib/storage-broker/dist/authorize.js.map +1 -0
  44. package/payload/platform/lib/storage-broker/dist/bin/remediate-run.d.ts +25 -0
  45. package/payload/platform/lib/storage-broker/dist/bin/remediate-run.d.ts.map +1 -0
  46. package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js +131 -0
  47. package/payload/platform/lib/storage-broker/dist/bin/remediate-run.js.map +1 -0
  48. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +20 -0
  49. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -0
  50. package/payload/platform/lib/storage-broker/dist/cf-exec.js +53 -0
  51. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -0
  52. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts +6 -0
  53. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -0
  54. package/payload/platform/lib/storage-broker/dist/house-credential.js +33 -0
  55. package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -0
  56. package/payload/platform/lib/storage-broker/dist/index.d.ts +7 -0
  57. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -0
  58. package/payload/platform/lib/storage-broker/dist/index.js +30 -0
  59. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -0
  60. package/payload/platform/lib/storage-broker/dist/registry.d.ts +18 -0
  61. package/payload/platform/lib/storage-broker/dist/registry.d.ts.map +1 -0
  62. package/payload/platform/lib/storage-broker/dist/registry.js +45 -0
  63. package/payload/platform/lib/storage-broker/dist/registry.js.map +1 -0
  64. package/payload/platform/lib/storage-broker/dist/remediate.d.ts +7 -0
  65. package/payload/platform/lib/storage-broker/dist/remediate.d.ts.map +1 -0
  66. package/payload/platform/lib/storage-broker/dist/remediate.js +25 -0
  67. package/payload/platform/lib/storage-broker/dist/remediate.js.map +1 -0
  68. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +49 -0
  69. package/payload/platform/lib/storage-broker/src/__tests__/authorize.test.ts +28 -0
  70. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +52 -0
  71. package/payload/platform/lib/storage-broker/src/__tests__/house-credential.test.ts +35 -0
  72. package/payload/platform/lib/storage-broker/src/__tests__/registry.test.ts +64 -0
  73. package/payload/platform/lib/storage-broker/src/__tests__/remediate-run.test.ts +79 -0
  74. package/payload/platform/lib/storage-broker/src/__tests__/remediate.test.ts +21 -0
  75. package/payload/platform/lib/storage-broker/src/audit.ts +55 -0
  76. package/payload/platform/lib/storage-broker/src/authorize.ts +22 -0
  77. package/payload/platform/lib/storage-broker/src/bin/remediate-run.ts +156 -0
  78. package/payload/platform/lib/storage-broker/src/cf-exec.ts +73 -0
  79. package/payload/platform/lib/storage-broker/src/house-credential.ts +32 -0
  80. package/payload/platform/lib/storage-broker/src/index.ts +14 -0
  81. package/payload/platform/lib/storage-broker/src/registry.ts +85 -0
  82. package/payload/platform/lib/storage-broker/src/remediate.ts +31 -0
  83. package/payload/platform/lib/storage-broker/tsconfig.json +8 -0
  84. package/payload/platform/package.json +2 -2
  85. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  86. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  87. package/payload/platform/plugins/admin/mcp/dist/index.js +13 -3
  88. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  89. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +8 -0
  90. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -1
  91. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +20 -3
  92. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -1
  93. package/payload/platform/plugins/cloudflare/PLUGIN.md +4 -1
  94. package/payload/platform/plugins/cloudflare/bin/__tests__/cf-schema-check.test.sh +128 -0
  95. package/payload/platform/plugins/cloudflare/bin/__tests__/cf-store-name.test.sh +77 -0
  96. package/payload/platform/plugins/cloudflare/bin/cf-schema-check.sh +60 -0
  97. package/payload/platform/plugins/cloudflare/bin/cf-store-name.sh +37 -0
  98. package/payload/platform/plugins/cloudflare/bin/remediate-storage-isolation.sh +36 -0
  99. package/payload/platform/plugins/cloudflare/references/api.md +4 -0
  100. package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +58 -0
  101. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +39 -0
  102. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +2 -0
  103. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +2 -0
  104. package/payload/platform/plugins/outlook/PLUGIN.md +1 -1
  105. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js +23 -0
  106. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-list.test.js.map +1 -1
  107. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.js +34 -1
  108. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mailbox-registry.test.js.map +1 -1
  109. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts +26 -0
  110. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.d.ts.map +1 -1
  111. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js +20 -3
  112. package/payload/platform/plugins/outlook/mcp/dist/auth/mailbox-registry.js.map +1 -1
  113. package/payload/platform/plugins/outlook/mcp/dist/index.js +2 -2
  114. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  115. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts +7 -0
  116. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.d.ts.map +1 -1
  117. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js +4 -1
  118. package/payload/platform/plugins/outlook/mcp/dist/tools/mailbox-list.js.map +1 -1
  119. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +9 -3
  120. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js +11 -4
  121. package/payload/platform/plugins/scheduling/mcp/dist/scripts/publish-availability.js.map +1 -1
  122. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +15 -10
  123. package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
  124. package/payload/platform/plugins/storage-broker/.claude-plugin/plugin.json +21 -0
  125. package/payload/platform/plugins/storage-broker/PLUGIN.md +48 -0
  126. package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/index.js +193 -0
  127. package/payload/platform/plugins/storage-broker/lib/mcp-spawn-tee/package.json +3 -0
  128. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.d.ts +2 -0
  129. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.d.ts.map +1 -0
  130. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.js +16 -0
  131. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/caller-scope-headers.test.js.map +1 -0
  132. package/payload/platform/plugins/storage-broker/mcp/dist/call-api.d.ts +8 -0
  133. package/payload/platform/plugins/storage-broker/mcp/dist/call-api.d.ts.map +1 -0
  134. package/payload/platform/plugins/storage-broker/mcp/dist/call-api.js +39 -0
  135. package/payload/platform/plugins/storage-broker/mcp/dist/call-api.js.map +1 -0
  136. package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.d.ts +7 -0
  137. package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.d.ts.map +1 -0
  138. package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.js +16 -0
  139. package/payload/platform/plugins/storage-broker/mcp/dist/caller-scope-headers.js.map +1 -0
  140. package/payload/platform/plugins/storage-broker/mcp/dist/index.d.ts +8 -0
  141. package/payload/platform/plugins/storage-broker/mcp/dist/index.d.ts.map +1 -0
  142. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +98 -0
  143. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -0
  144. package/payload/platform/plugins/storage-broker/mcp/package.json +10 -0
  145. package/payload/platform/plugins/storage-broker/mcp/vitest.config.ts +4 -0
  146. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  147. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +6 -0
  148. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  149. package/payload/server/chunk-4W6IEUFE.js +175 -0
  150. package/payload/server/public/assets/{AdminLoginScreens-BJVwtpLH.js → AdminLoginScreens-D9L-CuWi.js} +1 -1
  151. package/payload/server/public/assets/{AdminShell-CMu8_Xwn.js → AdminShell-DSQrvZXq.js} +1 -1
  152. package/payload/server/public/assets/{Checkbox-CzD0ORqA.js → Checkbox-CFdt8ljo.js} +1 -1
  153. package/payload/server/public/assets/{admin-D0yk2LMq.js → admin-Ch4n-G0j.js} +1 -1
  154. package/payload/server/public/assets/{browser-CvfbgznO.js → browser-B2AIxiuk.js} +1 -1
  155. package/payload/server/public/assets/{calendar-fw4WethJ.js → calendar-BvItH3B0.js} +1 -1
  156. package/payload/server/public/assets/chat-eQFMaX9y.js +1 -0
  157. package/payload/server/public/assets/chevron-left-D2AnCEyE.js +1 -0
  158. package/payload/server/public/assets/data-BzOmbHoe.js +1 -0
  159. package/payload/server/public/assets/{file-download-Px4s0Bhp.js → file-download-CK9j_Jx2.js} +1 -1
  160. package/payload/server/public/assets/graph-DrE7KupQ.js +51 -0
  161. package/payload/server/public/assets/graph-labels-CwEtvIRV.js +1 -0
  162. package/payload/server/public/assets/{maximize-2-BwphzI9S.js → maximize-2-CuzEom3K.js} +1 -1
  163. package/payload/server/public/assets/{operator-88m0DMl2.js → operator-DlboYPPu.js} +1 -1
  164. package/payload/server/public/assets/page-BuWH5gNK.js +1 -0
  165. package/payload/server/public/assets/page-Co9E8U5x.js +32 -0
  166. package/payload/server/public/assets/{public-BX2MjsFY.js → public-DtFpPMTG.js} +1 -1
  167. package/payload/server/public/assets/{rotate-ccw-C7BqkMec.js → rotate-ccw-Cqp4oqHV.js} +1 -1
  168. package/payload/server/public/assets/{tasks-DlgiCafL.js → tasks-bQKOhMga.js} +1 -1
  169. package/payload/server/public/assets/{time-entry-format-CyNYVDkq.js → time-entry-format-BIL4ZaDK.js} +1 -1
  170. package/payload/server/public/assets/useCopyFeedback-rB8QuqyH.js +1 -0
  171. package/payload/server/public/assets/{useSubAccountSwitcher-1izQvPiD.css → useSubAccountSwitcher-CVHKTSst.css} +1 -1
  172. package/payload/server/public/assets/{useVoiceRecorder-XgpWvXpk.js → useVoiceRecorder-Ctny8qWx.js} +1 -1
  173. package/payload/server/public/browser.html +5 -5
  174. package/payload/server/public/calendar.html +6 -6
  175. package/payload/server/public/chat.html +12 -12
  176. package/payload/server/public/data.html +10 -10
  177. package/payload/server/public/graph.html +11 -11
  178. package/payload/server/public/index.html +13 -13
  179. package/payload/server/public/operator.html +14 -14
  180. package/payload/server/public/public.html +12 -12
  181. package/payload/server/public/tasks.html +5 -5
  182. package/payload/server/server.js +1033 -837
  183. package/payload/server/src-BQ2NYMOT.js +24 -0
  184. package/payload/server/public/assets/chat-Dq-X-ia8.js +0 -1
  185. package/payload/server/public/assets/chevron-left-B3xSeEDR.js +0 -1
  186. package/payload/server/public/assets/data-CUnDzDox.js +0 -1
  187. package/payload/server/public/assets/graph-DbT8nA6R.js +0 -51
  188. package/payload/server/public/assets/graph-labels-C1YIyLiA.js +0 -1
  189. package/payload/server/public/assets/page-BRfxFXyi.js +0 -32
  190. package/payload/server/public/assets/page-CfQFAJia.js +0 -1
  191. package/payload/server/public/assets/useCopyFeedback-ByGUMN94.js +0 -1
  192. /package/payload/server/public/assets/{useSubAccountSwitcher-s6WlRn52.js → useSubAccountSwitcher-CHEt_m6q.js} +0 -0
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // The ownership registry: which account owns which Cloudflare storage resource.
3
+ //
4
+ // Written with raw cypher MERGE stamped with system provenance, mirroring the
5
+ // house-level seedAccountGraphRoot path in the admin plugin. It deliberately
6
+ // does NOT go through graph-write's writeNodeWithEdges: that primitive's
7
+ // accountId floor rejects a write whose props.accountId differs from the
8
+ // writer process's ACCOUNT_ID, and the broker runs house-level, writing nodes
9
+ // owned by many different sub-accounts. The owning accountId here is always the
10
+ // trusted header value the service resolved, never an agent argument.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.registerResource = registerResource;
13
+ exports.resolveOwner = resolveOwner;
14
+ exports.listResources = listResources;
15
+ async function registerResource(session, r) {
16
+ // MERGE on (kind, name) — the ownership key. On a shared Cloudflare account a
17
+ // storage name maps to exactly one owner, so the write key must match the
18
+ // resolveOwner lookup key; keying on accountId too would let two accounts hold
19
+ // a node for the same name and make ownership ambiguous. accountId is set only
20
+ // ON CREATE and never overwritten ON MATCH, so a register can never silently
21
+ // reassign an existing resource to a different account.
22
+ await session.run(`MERGE (r:StorageResource {kind: $kind, name: $name})
23
+ ON CREATE SET r.accountId = $accountId,
24
+ r.createdByAgent = 'system',
25
+ r.createdBySource = 'storage-broker',
26
+ r.cfResourceId = $cfResourceId
27
+ ON MATCH SET r.cfResourceId = $cfResourceId`, { accountId: r.accountId, kind: r.kind, name: r.name, cfResourceId: r.cfResourceId });
28
+ }
29
+ async function resolveOwner(session, kind, name) {
30
+ const res = await session.run(`MATCH (r:StorageResource {kind: $kind, name: $name})
31
+ RETURN r.accountId AS accountId LIMIT 1`, { kind, name });
32
+ return res.records.length ? res.records[0].get("accountId") : null;
33
+ }
34
+ async function listResources(session, accountId, kind) {
35
+ const res = await session.run(`MATCH (r:StorageResource {accountId: $accountId, kind: $kind})
36
+ RETURN r.name AS name, r.cfResourceId AS cfResourceId
37
+ ORDER BY r.name`, { accountId, kind });
38
+ return res.records.map((rec) => ({
39
+ accountId,
40
+ kind,
41
+ name: rec.get("name"),
42
+ cfResourceId: rec.get("cfResourceId"),
43
+ }));
44
+ }
45
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":";AAAA,gFAAgF;AAChF,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,yEAAyE;AACzE,yEAAyE;AACzE,8EAA8E;AAC9E,gFAAgF;AAChF,sEAAsE;;AAyBtE,4CAmBC;AAED,oCAWC;AAED,sCAiBC;AAnDM,KAAK,UAAU,gBAAgB,CACpC,OAAoB,EACpB,CAAkB;IAElB,8EAA8E;IAC9E,0EAA0E;IAC1E,+EAA+E;IAC/E,+EAA+E;IAC/E,6EAA6E;IAC7E,wDAAwD;IACxD,MAAM,OAAO,CAAC,GAAG,CACf;;;;;oDAKgD,EAChD,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,YAAY,EAAE,CACrF,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,OAAoB,EACpB,IAAkB,EAClB,IAAY;IAEZ,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAC3B;+CAC2C,EAC3C,EAAE,IAAI,EAAE,IAAI,EAAE,CACf,CAAC;IACF,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAY,CAAC,CAAC,CAAC,IAAI,CAAC;AACjF,CAAC;AAEM,KAAK,UAAU,aAAa,CACjC,OAAoB,EACpB,SAAiB,EACjB,IAAkB;IAElB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAC3B;;uBAEmB,EACnB,EAAE,SAAS,EAAE,IAAI,EAAE,CACpB,CAAC;IACF,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC/B,SAAS;QACT,IAAI;QACJ,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,MAAM,CAAW;QAC/B,YAAY,EAAE,GAAG,CAAC,GAAG,CAAC,cAAc,CAAW;KAChD,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface MappingResult {
2
+ ok: boolean;
3
+ missing: string[];
4
+ }
5
+ export declare function validateMapping(enumerated: string[], mapping: Record<string, string>): MappingResult;
6
+ export declare function stripAccountWideTokens(contents: string): string;
7
+ //# sourceMappingURL=remediate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remediate.d.ts","sourceRoot":"","sources":["../src/remediate.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAOD,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAAE,EACpB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,aAAa,CAGf;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM/D"}
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ // The testable core of the operator-run remediation. The mapping from existing
3
+ // resource name to owning account is operator-authored, and validation fails
4
+ // closed on any resource absent from it: ownership is a recorded fact, never a
5
+ // name-prefix guess. Token stripping removes every account-wide credential from
6
+ // a sub-account secrets file while preserving everything else verbatim.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.validateMapping = validateMapping;
9
+ exports.stripAccountWideTokens = stripAccountWideTokens;
10
+ // Account-wide credentials stripped from a sub-account's cloudflare.env.
11
+ // CLOUDFLARE_ACCOUNT_ID is intentionally kept: it is an identifier, not a
12
+ // credential, and does not grant access on its own.
13
+ const STRIP_KEYS = ["CLOUDFLARE_API_TOKEN", "CF_PAGES_D1_TOKEN", "CF_PAGES_TOKEN"];
14
+ function validateMapping(enumerated, mapping) {
15
+ const missing = enumerated.filter((name) => !(name in mapping));
16
+ return { ok: missing.length === 0, missing };
17
+ }
18
+ function stripAccountWideTokens(contents) {
19
+ const kept = contents.split("\n").filter((line) => {
20
+ const key = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/)?.[1];
21
+ return !(key && STRIP_KEYS.includes(key));
22
+ });
23
+ return kept.join("\n");
24
+ }
25
+ //# sourceMappingURL=remediate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remediate.js","sourceRoot":"","sources":["../src/remediate.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,wEAAwE;;AAYxE,0CAMC;AAED,wDAMC;AAnBD,yEAAyE;AACzE,0EAA0E;AAC1E,oDAAoD;AACpD,MAAM,UAAU,GAAG,CAAC,sBAAsB,EAAE,mBAAmB,EAAE,gBAAgB,CAAC,CAAC;AAEnF,SAAgB,eAAe,CAC7B,UAAoB,EACpB,OAA+B;IAE/B,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC;IAChE,OAAO,EAAE,EAAE,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;AAC/C,CAAC;AAED,SAAgB,sBAAsB,CAAC,QAAgB;IACrD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,+BAA+B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7D,OAAO,CAAC,CAAC,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { reconcileStorage, type AuditDeps } from "../audit.js";
4
+
5
+ const clean: AuditDeps = {
6
+ cfD1: async () => ["gls-leads"],
7
+ cfR2: async () => [],
8
+ registeredNames: async (k) => (k === "d1" ? ["gls-leads"] : []),
9
+ accountSecretsFiles: async () => [
10
+ { accountId: "acc-a", path: "/x", contents: "SOME_OTHER=1\nCLOUDFLARE_ACCOUNT_ID=acc\n" },
11
+ ],
12
+ };
13
+
14
+ test("clean install reports zero strays", async () => {
15
+ const { strays } = await reconcileStorage(clean);
16
+ assert.deepEqual(strays, []);
17
+ });
18
+
19
+ test("flags an unregistered D1 database", async () => {
20
+ const { strays } = await reconcileStorage({
21
+ ...clean,
22
+ cfD1: async () => ["gls-leads", "beagle-investors"],
23
+ });
24
+ assert.ok(strays.some((s) => s.kind === "unattributed-d1" && s.detail === "beagle-investors"));
25
+ });
26
+
27
+ test("flags a lingering account-wide token", async () => {
28
+ const { strays } = await reconcileStorage({
29
+ ...clean,
30
+ accountSecretsFiles: async () => [
31
+ { accountId: "acc-a", path: "/x", contents: "CF_PAGES_D1_TOKEN=cfat_leak\n" },
32
+ ],
33
+ });
34
+ assert.ok(
35
+ strays.some((s) => s.kind === "account-wide-token" && s.detail.includes("acc-a")),
36
+ );
37
+ });
38
+
39
+ test("flags a lingering master token", async () => {
40
+ const { strays } = await reconcileStorage({
41
+ ...clean,
42
+ accountSecretsFiles: async () => [
43
+ { accountId: "acc-b", path: "/y", contents: "CLOUDFLARE_API_TOKEN=cfat_master\n" },
44
+ ],
45
+ });
46
+ assert.ok(
47
+ strays.some((s) => s.kind === "account-wide-token" && s.detail === "acc-b:CLOUDFLARE_API_TOKEN"),
48
+ );
49
+ });
@@ -0,0 +1,28 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { authorizeAccess } from "../authorize.js";
4
+
5
+ test("allows the owner", () => {
6
+ assert.deepEqual(
7
+ authorizeAccess({ caller: "acc-a", owner: "acc-a", action: "write" }),
8
+ { allow: true, reason: "owner" },
9
+ );
10
+ });
11
+
12
+ test("denies a different owner", () => {
13
+ const d = authorizeAccess({ caller: "acc-a", owner: "acc-b", action: "read" });
14
+ assert.equal(d.allow, false);
15
+ assert.equal(d.reason, "cross-account");
16
+ });
17
+
18
+ test("denies an unregistered resource (owner null)", () => {
19
+ const d = authorizeAccess({ caller: "acc-a", owner: null, action: "read" });
20
+ assert.equal(d.allow, false);
21
+ assert.equal(d.reason, "unregistered");
22
+ });
23
+
24
+ test("denies an empty caller", () => {
25
+ const d = authorizeAccess({ caller: "", owner: "acc-a", action: "read" });
26
+ assert.equal(d.allow, false);
27
+ assert.equal(d.reason, "no-caller");
28
+ });
@@ -0,0 +1,52 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { makeCfExec, type RunFn } from "../cf-exec.js";
4
+
5
+ const cred = { apiToken: "cfat_x", accountId: "acc-h" };
6
+
7
+ function recordingRun(stdout: string) {
8
+ const calls: Array<{ cmd: string; args: string[]; env: Record<string, string> }> = [];
9
+ const run: RunFn = async (cmd, args, env) => {
10
+ calls.push({ cmd, args, env });
11
+ return { stdout };
12
+ };
13
+ return { run, calls };
14
+ }
15
+
16
+ test("d1List parses --json output and injects the house token", async () => {
17
+ const { run, calls } = recordingRun(JSON.stringify([{ name: "gls-leads", uuid: "u1" }]));
18
+ const cf = makeCfExec(cred, run);
19
+ assert.deepEqual(await cf.d1List(), [{ name: "gls-leads", uuid: "u1" }]);
20
+ assert.equal(calls[0].cmd, "wrangler");
21
+ assert.deepEqual(calls[0].args, ["d1", "list", "--json"]);
22
+ assert.equal(calls[0].env.CLOUDFLARE_API_TOKEN, "cfat_x");
23
+ assert.equal(calls[0].env.CLOUDFLARE_ACCOUNT_ID, "acc-h");
24
+ });
25
+
26
+ test("d1Query runs execute --remote with the SQL", async () => {
27
+ const { run, calls } = recordingRun(JSON.stringify([{ results: [] }]));
28
+ const cf = makeCfExec(cred, run);
29
+ await cf.d1Query("gls-leads", "SELECT 1");
30
+ assert.deepEqual(calls[0].args, [
31
+ "d1", "execute", "gls-leads", "--remote", "--json", "--command", "SELECT 1",
32
+ ]);
33
+ });
34
+
35
+ test("d1Create parses the created uuid", async () => {
36
+ const { run } = recordingRun(JSON.stringify({ uuid: "new-db" }));
37
+ const cf = makeCfExec(cred, run);
38
+ assert.deepEqual(await cf.d1Create("gls-new"), { uuid: "new-db" });
39
+ });
40
+
41
+ test("r2Create shells wrangler r2 bucket create", async () => {
42
+ const { run, calls } = recordingRun("");
43
+ const cf = makeCfExec(cred, run);
44
+ await cf.r2Create("gls-assets");
45
+ assert.deepEqual(calls[0].args, ["r2", "bucket", "create", "gls-assets"]);
46
+ });
47
+
48
+ test("non-JSON output throws with the raw body", async () => {
49
+ const { run } = recordingRun("not json");
50
+ const cf = makeCfExec(cred, run);
51
+ await assert.rejects(() => cf.d1List(), /not json/);
52
+ });
@@ -0,0 +1,35 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { readHouseCredential } from "../house-credential.js";
7
+
8
+ function houseRoot(contents: string): string {
9
+ const dir = mkdtempSync(join(tmpdir(), "sb-house-"));
10
+ mkdirSync(join(dir, "config"));
11
+ writeFileSync(join(dir, "config", "cloudflare-house.env"), contents);
12
+ return dir;
13
+ }
14
+
15
+ test("parses master token + account id", () => {
16
+ const dir = houseRoot("CLOUDFLARE_API_TOKEN=cfat_abc\nCLOUDFLARE_ACCOUNT_ID=acc-house\n");
17
+ try {
18
+ assert.deepEqual(readHouseCredential(dir), { apiToken: "cfat_abc", accountId: "acc-house" });
19
+ } finally {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ }
22
+ });
23
+
24
+ test("throws when the token key is absent", () => {
25
+ const dir = houseRoot("CLOUDFLARE_ACCOUNT_ID=acc-house\n");
26
+ try {
27
+ assert.throws(() => readHouseCredential(dir), /CLOUDFLARE_API_TOKEN/);
28
+ } finally {
29
+ rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ test("throws when the file is absent", () => {
34
+ assert.throws(() => readHouseCredential("/no/such/root"), /cloudflare-house\.env/);
35
+ });
@@ -0,0 +1,64 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { registerResource, resolveOwner, listResources, type SessionLike } from "../registry.js";
4
+
5
+ function fakeSession(records: Array<{ get(key: string): unknown }>) {
6
+ const calls: Array<{ cypher: string; params?: Record<string, unknown> }> = [];
7
+ const session: SessionLike = {
8
+ async run(cypher, params) {
9
+ calls.push({ cypher, params });
10
+ return { records };
11
+ },
12
+ };
13
+ return { session, calls };
14
+ }
15
+
16
+ test("registerResource MERGEs on (kind, name) and sets accountId only on create", async () => {
17
+ const { session, calls } = fakeSession([]);
18
+ await registerResource(session, {
19
+ accountId: "acc-a",
20
+ kind: "d1",
21
+ name: "gls-leads",
22
+ cfResourceId: "db-123",
23
+ });
24
+ // Ownership key is (kind, name): a name maps to exactly one owner on the
25
+ // shared account, so resolveOwner's lookup key and the write key must agree.
26
+ assert.match(
27
+ calls[0].cypher,
28
+ /MERGE \(r:StorageResource \{kind: \$kind, name: \$name\}\)/,
29
+ );
30
+ // accountId is set ON CREATE and never overwritten ON MATCH (no silent takeover).
31
+ assert.match(calls[0].cypher, /ON CREATE SET r\.accountId = \$accountId/);
32
+ assert.doesNotMatch(calls[0].cypher, /ON MATCH SET[\s\S]*accountId/);
33
+ assert.match(calls[0].cypher, /r\.createdByAgent = 'system'/);
34
+ assert.match(calls[0].cypher, /r\.createdBySource = 'storage-broker'/);
35
+ assert.deepEqual(calls[0].params, {
36
+ accountId: "acc-a",
37
+ kind: "d1",
38
+ name: "gls-leads",
39
+ cfResourceId: "db-123",
40
+ });
41
+ });
42
+
43
+ test("resolveOwner returns the owning accountId, or null when absent", async () => {
44
+ const found = fakeSession([{ get: () => "acc-b" }]);
45
+ assert.equal(await resolveOwner(found.session, "d1", "beagle-investors"), "acc-b");
46
+
47
+ const missing = fakeSession([]);
48
+ assert.equal(await resolveOwner(missing.session, "d1", "no-such"), null);
49
+ });
50
+
51
+ test("listResources returns only the account's resources of a kind", async () => {
52
+ const { session, calls } = fakeSession([
53
+ { get: (k: string) => ({ name: "gls-leads", cfResourceId: "db-1" } as Record<string, unknown>)[k] },
54
+ ]);
55
+ const rows = await listResources(session, "acc-a", "d1");
56
+ assert.deepEqual(rows, [
57
+ { accountId: "acc-a", kind: "d1", name: "gls-leads", cfResourceId: "db-1" },
58
+ ]);
59
+ assert.match(
60
+ calls[0].cypher,
61
+ /MATCH \(r:StorageResource \{accountId: \$accountId, kind: \$kind\}\)/,
62
+ );
63
+ assert.deepEqual(calls[0].params, { accountId: "acc-a", kind: "d1" });
64
+ });
@@ -0,0 +1,79 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { runRemediation, type RemediationDeps } from "../bin/remediate-run.js";
7
+ import type { StorageResource } from "../registry.js";
8
+
9
+ function fixture(): { root: string; accountsDir: string; configPath: string } {
10
+ const root = mkdtempSync(join(tmpdir(), "sb-remediate-"));
11
+ const accountsDir = join(root, "data", "accounts");
12
+ for (const acc of ["acc-a", "acc-b"]) {
13
+ mkdirSync(join(accountsDir, acc, "secrets"), { recursive: true });
14
+ writeFileSync(
15
+ join(accountsDir, acc, "secrets", "cloudflare.env"),
16
+ `CLOUDFLARE_ACCOUNT_ID=shared\nCLOUDFLARE_API_TOKEN=cfat_master\nCF_PAGES_D1_TOKEN=cfat_pd\nCF_DNS_TOKEN=keep\n`,
17
+ );
18
+ }
19
+ return { root, accountsDir, configPath: join(root, "config", "cloudflare-house.env") };
20
+ }
21
+
22
+ function baseDeps(f: ReturnType<typeof fixture>, registered: StorageResource[]): RemediationDeps {
23
+ return {
24
+ cfD1: async () => [{ name: "gls-leads", cfResourceId: "u1" }],
25
+ cfR2: async () => [],
26
+ mapping: { "gls-leads": "acc-a" },
27
+ register: async (r) => { registered.push(r); },
28
+ accountsDir: f.accountsDir,
29
+ master: { apiToken: "cfat_master", accountId: "shared" },
30
+ configPath: f.configPath,
31
+ };
32
+ }
33
+
34
+ test("fails closed on an unmapped resource and writes nothing", async () => {
35
+ const f = fixture();
36
+ const registered: StorageResource[] = [];
37
+ const deps = { ...baseDeps(f, registered), cfD1: async () => [
38
+ { name: "gls-leads", cfResourceId: "u1" },
39
+ { name: "beagle-investors", cfResourceId: "u2" },
40
+ ] };
41
+ try {
42
+ await assert.rejects(() => runRemediation(deps), /unmapped resources: beagle-investors/);
43
+ assert.equal(registered.length, 0, "no resource registered on abort");
44
+ assert.equal(existsSync(f.configPath), false, "no house config written on abort");
45
+ const secrets = readFileSync(join(f.accountsDir, "acc-a", "secrets", "cloudflare.env"), "utf8");
46
+ assert.match(secrets, /CLOUDFLARE_API_TOKEN=cfat_master/, "secrets untouched on abort");
47
+ } finally {
48
+ rmSync(f.root, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ test("registers, relocates the master, and strips every account-wide token", async () => {
53
+ const f = fixture();
54
+ const registered: StorageResource[] = [];
55
+ try {
56
+ const result = await runRemediation(baseDeps(f, registered));
57
+ assert.equal(result.registered, 1);
58
+ assert.deepEqual(registered, [
59
+ { accountId: "acc-a", kind: "d1", name: "gls-leads", cfResourceId: "u1" },
60
+ ]);
61
+
62
+ // House config now holds the master.
63
+ const house = readFileSync(f.configPath, "utf8");
64
+ assert.match(house, /CLOUDFLARE_API_TOKEN=cfat_master/);
65
+ assert.match(house, /CLOUDFLARE_ACCOUNT_ID=shared/);
66
+
67
+ // Both sub-account secrets are stripped of account-wide tokens, scoped kept.
68
+ for (const acc of ["acc-a", "acc-b"]) {
69
+ const s = readFileSync(join(f.accountsDir, acc, "secrets", "cloudflare.env"), "utf8");
70
+ assert.doesNotMatch(s, /CLOUDFLARE_API_TOKEN=/);
71
+ assert.doesNotMatch(s, /CF_PAGES_D1_TOKEN=/);
72
+ assert.match(s, /CF_DNS_TOKEN=keep/);
73
+ assert.match(s, /CLOUDFLARE_ACCOUNT_ID=shared/);
74
+ }
75
+ assert.equal(result.strippedFiles.length, 2);
76
+ } finally {
77
+ rmSync(f.root, { recursive: true, force: true });
78
+ }
79
+ });
@@ -0,0 +1,21 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { validateMapping, stripAccountWideTokens } from "../remediate.js";
4
+
5
+ test("fails closed on an unmapped resource", () => {
6
+ const r = validateMapping(["gls-leads", "beagle-investors"], { "gls-leads": "acc-a" });
7
+ assert.deepEqual(r, { ok: false, missing: ["beagle-investors"] });
8
+ });
9
+
10
+ test("passes when every resource is mapped", () => {
11
+ assert.deepEqual(validateMapping(["gls-leads"], { "gls-leads": "acc-a" }), {
12
+ ok: true,
13
+ missing: [],
14
+ });
15
+ });
16
+
17
+ test("strips only account-wide token lines, keeps the account id and scoped tokens", () => {
18
+ const before =
19
+ "CLOUDFLARE_ACCOUNT_ID=acc\nCLOUDFLARE_API_TOKEN=cfat_x\nCF_PAGES_D1_TOKEN=y\nCF_DNS_TOKEN=keep\n";
20
+ assert.equal(stripAccountWideTokens(before), "CLOUDFLARE_ACCOUNT_ID=acc\nCF_DNS_TOKEN=keep\n");
21
+ });
@@ -0,0 +1,55 @@
1
+ // The standing audit: an independent periodic reconcile, not a per-action log,
2
+ // because the failure mode is a no-event blind spot. A mis-registered resource
3
+ // or a stray account-wide token leaks nothing until an agent happens to touch
4
+ // it. This reconciles what Cloudflare has against what the registry records and
5
+ // against what the per-account secrets files still carry.
6
+
7
+ export type StrayKind = "unattributed-d1" | "unattributed-r2" | "account-wide-token";
8
+
9
+ export interface Stray {
10
+ kind: StrayKind;
11
+ detail: string;
12
+ }
13
+
14
+ // Keys that must never remain in a sub-account's cloudflare.env after
15
+ // remediation. Each is an account-wide capability.
16
+ const ACCOUNT_WIDE_KEYS = ["CLOUDFLARE_API_TOKEN", "CF_PAGES_D1_TOKEN", "CF_PAGES_TOKEN"];
17
+
18
+ export interface AuditDeps {
19
+ cfD1(): Promise<string[]>;
20
+ cfR2(): Promise<string[]>;
21
+ registeredNames(kind: "d1" | "r2"): Promise<string[]>;
22
+ accountSecretsFiles(): Promise<Array<{ accountId: string; path: string; contents: string }>>;
23
+ }
24
+
25
+ export async function reconcileStorage(deps: AuditDeps): Promise<{ strays: Stray[] }> {
26
+ const strays: Stray[] = [];
27
+
28
+ const [cfD1, cfR2, regD1, regR2, secretsFiles] = await Promise.all([
29
+ deps.cfD1(),
30
+ deps.cfR2(),
31
+ deps.registeredNames("d1"),
32
+ deps.registeredNames("r2"),
33
+ deps.accountSecretsFiles(),
34
+ ]);
35
+
36
+ const regD1Set = new Set(regD1);
37
+ for (const name of cfD1) {
38
+ if (!regD1Set.has(name)) strays.push({ kind: "unattributed-d1", detail: name });
39
+ }
40
+ const regR2Set = new Set(regR2);
41
+ for (const name of cfR2) {
42
+ if (!regR2Set.has(name)) strays.push({ kind: "unattributed-r2", detail: name });
43
+ }
44
+
45
+ for (const file of secretsFiles) {
46
+ for (const line of file.contents.split("\n")) {
47
+ const key = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=/)?.[1];
48
+ if (key && ACCOUNT_WIDE_KEYS.includes(key)) {
49
+ strays.push({ kind: "account-wide-token", detail: `${file.accountId}:${key}` });
50
+ }
51
+ }
52
+ }
53
+
54
+ return { strays };
55
+ }
@@ -0,0 +1,22 @@
1
+ // The pure ownership decision at the centre of the storage broker. No I/O:
2
+ // given who is calling and who owns a resource, decide allow or deny. The
3
+ // caller identity is always the trusted header value the service resolved,
4
+ // never an agent-supplied argument.
5
+
6
+ export type StorageAction = "list" | "read" | "write";
7
+
8
+ export interface AccessDecision {
9
+ allow: boolean;
10
+ reason: string;
11
+ }
12
+
13
+ export function authorizeAccess(params: {
14
+ caller: string;
15
+ owner: string | null;
16
+ action: StorageAction;
17
+ }): AccessDecision {
18
+ if (!params.caller) return { allow: false, reason: "no-caller" };
19
+ if (params.owner === null) return { allow: false, reason: "unregistered" };
20
+ if (params.owner !== params.caller) return { allow: false, reason: "cross-account" };
21
+ return { allow: true, reason: "owner" };
22
+ }
@@ -0,0 +1,156 @@
1
+ // Operator-run remediation of an existing multi-tenant install. Registers every
2
+ // existing D1 database and R2 bucket to its owning account, relocates the master
3
+ // token into the house-only config file, and strips every account-wide token
4
+ // from every sub-account secrets file. Fails closed on any enumerated resource
5
+ // the operator did not map: ownership is never guessed.
6
+ //
7
+ // runRemediation is the testable core (all effects injected or against a given
8
+ // directory). main() wires the real deps and is the on-device entry the shell
9
+ // wrapper calls.
10
+
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { validateMapping, stripAccountWideTokens } from "../remediate.js";
14
+ import { registerResource, type StorageResource } from "../registry.js";
15
+ import { readHouseCredential } from "../house-credential.js";
16
+ import { makeCfExec } from "../cf-exec.js";
17
+
18
+ export interface RemediationDeps {
19
+ cfD1(): Promise<Array<{ name: string; cfResourceId: string }>>;
20
+ cfR2(): Promise<Array<{ name: string; cfResourceId: string }>>;
21
+ mapping: Record<string, string>;
22
+ register(r: StorageResource): Promise<void>;
23
+ accountsDir: string;
24
+ master: { apiToken: string; accountId: string };
25
+ configPath: string;
26
+ }
27
+
28
+ export interface RemediationResult {
29
+ registered: number;
30
+ strippedFiles: string[];
31
+ }
32
+
33
+ export async function runRemediation(deps: RemediationDeps): Promise<RemediationResult> {
34
+ const d1 = await deps.cfD1();
35
+ const r2 = await deps.cfR2();
36
+ const enumerated = [...d1.map((x) => x.name), ...r2.map((x) => x.name)];
37
+
38
+ // Fail closed BEFORE any write: an enumerated resource with no mapped owner
39
+ // aborts the whole run, leaving the install untouched.
40
+ const validation = validateMapping(enumerated, deps.mapping);
41
+ if (!validation.ok) {
42
+ throw new Error(`storage-broker remediation: unmapped resources: ${validation.missing.join(", ")}`);
43
+ }
44
+
45
+ for (const x of d1) {
46
+ await deps.register({ accountId: deps.mapping[x.name], kind: "d1", name: x.name, cfResourceId: x.cfResourceId });
47
+ }
48
+ for (const x of r2) {
49
+ await deps.register({ accountId: deps.mapping[x.name], kind: "r2", name: x.name, cfResourceId: x.cfResourceId });
50
+ }
51
+
52
+ // Relocate the master into the house-only config file (read before stripping).
53
+ mkdirSync(dirname(deps.configPath), { recursive: true });
54
+ writeFileSync(
55
+ deps.configPath,
56
+ `CLOUDFLARE_API_TOKEN=${deps.master.apiToken}\nCLOUDFLARE_ACCOUNT_ID=${deps.master.accountId}\n`,
57
+ { mode: 0o600 },
58
+ );
59
+
60
+ // Strip every account-wide token from every sub-account secrets file.
61
+ const strippedFiles: string[] = [];
62
+ if (existsSync(deps.accountsDir)) {
63
+ for (const accountId of readdirSync(deps.accountsDir)) {
64
+ const p = join(deps.accountsDir, accountId, "secrets", "cloudflare.env");
65
+ if (!existsSync(p)) continue;
66
+ const before = readFileSync(p, "utf8");
67
+ const after = stripAccountWideTokens(before);
68
+ if (after !== before) {
69
+ writeFileSync(p, after, { mode: 0o600 });
70
+ strippedFiles.push(p);
71
+ }
72
+ }
73
+ }
74
+
75
+ return { registered: d1.length + r2.length, strippedFiles };
76
+ }
77
+
78
+ // --- on-device CLI (untested glue) ----------------------------------------
79
+
80
+ function findMasterFromAccounts(accountsDir: string): { apiToken: string; accountId: string } | null {
81
+ if (!existsSync(accountsDir)) return null;
82
+ for (const accountId of readdirSync(accountsDir)) {
83
+ const p = join(accountsDir, accountId, "secrets", "cloudflare.env");
84
+ if (!existsSync(p)) continue;
85
+ const env: Record<string, string> = {};
86
+ for (const line of readFileSync(p, "utf8").split("\n")) {
87
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
88
+ if (m) env[m[1]] = m[2].trim();
89
+ }
90
+ if (env.CLOUDFLARE_API_TOKEN && env.CLOUDFLARE_ACCOUNT_ID) {
91
+ return { apiToken: env.CLOUDFLARE_API_TOKEN, accountId: env.CLOUDFLARE_ACCOUNT_ID };
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+
97
+ async function main(): Promise<void> {
98
+ const mappingFile = process.argv[2];
99
+ if (!mappingFile || !existsSync(mappingFile)) {
100
+ console.error(`[remediate] error: mapping file not found: ${mappingFile ?? "(none)"}`);
101
+ process.exit(2);
102
+ }
103
+ const platformRoot = process.env.PLATFORM_ROOT;
104
+ if (!platformRoot) {
105
+ console.error("[remediate] error: PLATFORM_ROOT not set");
106
+ process.exit(2);
107
+ }
108
+ const accountsDir = resolve(platformRoot, "..", "data", "accounts");
109
+ const configPath = join(platformRoot, "config", "cloudflare-house.env");
110
+ const mapping = JSON.parse(readFileSync(mappingFile, "utf8")) as Record<string, string>;
111
+
112
+ // The master is wherever it lives today: the house file if already relocated,
113
+ // else the first sub-account secrets file that still carries it.
114
+ let master: { apiToken: string; accountId: string };
115
+ try {
116
+ master = readHouseCredential(platformRoot);
117
+ } catch {
118
+ const found = findMasterFromAccounts(accountsDir);
119
+ if (!found) {
120
+ console.error("[remediate] error: no Cloudflare master token found in house config or account secrets");
121
+ process.exit(2);
122
+ return;
123
+ }
124
+ master = found;
125
+ }
126
+
127
+ const cf = makeCfExec(master);
128
+ const neo4j = (await import("neo4j-driver")) as any;
129
+ const driver = neo4j.default.driver(
130
+ process.env.NEO4J_URI,
131
+ neo4j.default.auth.basic(process.env.NEO4J_USER ?? "neo4j", process.env.NEO4J_PASSWORD ?? ""),
132
+ );
133
+ const session = driver.session();
134
+ try {
135
+ const result = await runRemediation({
136
+ cfD1: async () => (await cf.d1List()).map((d) => ({ name: d.name, cfResourceId: d.uuid })),
137
+ cfR2: async () => (await cf.r2List()).map((b) => ({ name: b.name, cfResourceId: b.name })),
138
+ mapping,
139
+ register: (r) => registerResource(session, r),
140
+ accountsDir,
141
+ master,
142
+ configPath,
143
+ });
144
+ console.error(`[remediate] done registered=${result.registered} stripped=${result.strippedFiles.length}`);
145
+ } finally {
146
+ await session.close();
147
+ await driver.close();
148
+ }
149
+ }
150
+
151
+ if (require.main === module) {
152
+ main().catch((err) => {
153
+ console.error(`[remediate] error: ${(err as Error).message}`);
154
+ process.exit(1);
155
+ });
156
+ }