@rubytech/create-realagent-code 0.1.23 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +63 -16
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +50 -23
- package/payload/platform/plugins/admin/skills/admin-user-management/SKILL.md +47 -0
- package/payload/platform/plugins/admin/skills/commitment-followthrough/SKILL.md +60 -0
- package/payload/platform/plugins/admin/skills/file-presentation/SKILL.md +67 -0
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +111 -126
- package/payload/platform/plugins/admin/skills/session-management/SKILL.md +62 -0
- package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +37 -0
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +81 -1
- package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +241 -0
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +267 -28
- package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +291 -0
- package/payload/platform/plugins/cloudflare/skills/setup-tunnel/SKILL.md +42 -0
- package/payload/platform/plugins/contacts/PLUGIN.md +18 -9
- package/payload/platform/plugins/deep-research/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/deep-research/PLUGIN.md +7 -1
- package/payload/platform/plugins/deep-research/recipes/README.md +36 -0
- package/payload/platform/plugins/deep-research/skills/academic-verify/SKILL.md +75 -0
- package/payload/platform/plugins/deep-research/skills/book-mirror/SKILL.md +68 -0
- package/payload/platform/plugins/deep-research/skills/data-research/SKILL.md +108 -0
- package/payload/platform/plugins/deep-research/skills/strategic-reading/SKILL.md +69 -0
- package/payload/platform/plugins/docs/references/deployment.md +3 -2
- package/payload/platform/plugins/docs/references/platform.md +2 -0
- package/payload/platform/plugins/docs/references/troubleshooting.md +12 -0
- package/payload/platform/plugins/email/PLUGIN.md +18 -9
- package/payload/platform/plugins/email/mcp/dist/lib/claude-bridge.d.ts +17 -0
- package/payload/platform/plugins/email/mcp/dist/lib/claude-bridge.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/claude-bridge.js +185 -0
- package/payload/platform/plugins/email/mcp/dist/lib/claude-bridge.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js +34 -111
- package/payload/platform/plugins/email/mcp/dist/scripts/email-auto-respond.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.d.ts +7 -2
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js +7 -2
- package/payload/platform/plugins/email/mcp/dist/scripts/email-fetch.js.map +1 -1
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -0
- package/payload/platform/plugins/memory/PLUGIN.md +64 -29
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts +3 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +105 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +16 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/skills/archive-crawler/SKILL.md +67 -0
- package/payload/platform/plugins/memory/skills/concept-synthesis/SKILL.md +80 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +2 -0
- package/payload/platform/plugins/outlook/PLUGIN.md +14 -7
- package/payload/platform/plugins/replicate/PLUGIN.md +6 -3
- package/payload/platform/plugins/scheduling/PLUGIN.md +19 -8
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.d.ts +7 -3
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +7 -3
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +75 -0
- package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +61 -0
- package/payload/platform/plugins/tasks/PLUGIN.md +28 -14
- package/payload/platform/plugins/waitlist/PLUGIN.md +12 -6
- package/payload/platform/plugins/whatsapp/PLUGIN.md +25 -13
- package/payload/platform/plugins/workflows/PLUGIN.md +16 -8
- package/payload/platform/scripts/conversation-id-allowlist.txt +0 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +27 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +27 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +36 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +41 -3
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +25 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +54 -3
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +25 -0
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js +149 -0
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +38 -284
- package/payload/platform/templates/agents/admin/SOUL.md +4 -4
- package/payload/platform/templates/specialists/agents/content-producer.md +24 -69
- package/payload/platform/templates/specialists/agents/database-operator.md +49 -155
- package/payload/platform/templates/specialists/agents/personal-assistant.md +27 -177
- package/payload/platform/templates/specialists/agents/project-manager.md +29 -96
- package/payload/platform/templates/specialists/agents/research-assistant.md +36 -78
- package/payload/premium-plugins/real-agency/agents/compliance.md +14 -0
- package/payload/premium-plugins/real-agency/agents/negotiator.md +22 -0
- package/payload/premium-plugins/real-agency/agents/valuer.md +16 -0
- package/payload/premium-plugins/real-agency/plugins/estate-business/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agency/plugins/estate-business/PLUGIN.md +44 -13
- package/payload/premium-plugins/real-agency/plugins/estate-business/skills/commission-calculator/SKILL.md +40 -0
- package/payload/premium-plugins/real-agency/plugins/estate-business/skills/month-end-close/SKILL.md +69 -0
- package/payload/premium-plugins/real-agency/plugins/estate-business/skills/payment-batch-stager/SKILL.md +42 -0
- package/payload/premium-plugins/real-agency/plugins/estate-business/skills/period-reconciler/SKILL.md +42 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agency/plugins/estate-sales/PLUGIN.md +32 -13
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/chase-progression/SKILL.md +107 -0
- package/payload/premium-plugins/real-agency/plugins/estate-sales/skills/risk-scorer/SKILL.md +42 -0
- package/payload/premium-plugins/real-agency/plugins/leads/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agency/plugins/leads/PLUGIN.md +40 -10
- package/payload/premium-plugins/real-agency/plugins/leads/skills/chain-progression-tracker/SKILL.md +51 -0
- package/payload/premium-plugins/real-agency/plugins/leads/skills/diary-builder/SKILL.md +38 -0
- package/payload/premium-plugins/real-agency/plugins/leads/skills/enquiry-triage/SKILL.md +36 -0
- package/payload/premium-plugins/real-agency/plugins/leads/skills/morning-round/SKILL.md +72 -0
- package/payload/premium-plugins/real-agency/plugins/listings/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agency/plugins/listings/PLUGIN.md +82 -12
- package/payload/premium-plugins/real-agency/plugins/listings/skills/comparable-finder/SKILL.md +52 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/epc-checker/SKILL.md +38 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/listing-copy-writer/SKILL.md +55 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/local-market-stats/SKILL.md +33 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/new-instruction/SKILL.md +78 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/particulars-builder/SKILL.md +48 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/portal-launch-scheduler/SKILL.md +49 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/pricing-scenario-builder/SKILL.md +35 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/supplier-booker/SKILL.md +39 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/talk-track-composer/SKILL.md +36 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/terms-of-business-drafter/SKILL.md +54 -0
- package/payload/premium-plugins/real-agency/plugins/listings/skills/valuation-prep/SKILL.md +69 -0
- package/payload/premium-plugins/real-agency/plugins/loop/PLUGIN.md +35 -0
- package/payload/premium-plugins/real-agency/plugins/loop/skills/compliance-flag-checker/SKILL.md +53 -0
- package/payload/premium-plugins/real-agency/plugins/loop/skills/priority-ranker/SKILL.md +40 -0
- package/payload/premium-plugins/real-agency/plugins/loop/skills/tone-matched-drafter/SKILL.md +53 -0
- package/payload/premium-plugins/real-agency/plugins/loop/skills/variance-narrator/SKILL.md +50 -0
- package/payload/premium-plugins/real-agency/plugins/loop/skills/vendor-research/SKILL.md +54 -0
- package/payload/server/{chunk-2ZNKHCQB.js → chunk-2MRZBQMH.js} +1 -1
- package/payload/server/{chunk-GPUCA2RQ.js → chunk-NL7QLVAD.js} +0 -192
- package/payload/server/{chunk-IDKWGLM5.js → chunk-YPZFYTYP.js} +1 -247
- package/payload/server/{cloudflare-task-tracker-LYI5BTYI.js → cloudflare-task-tracker-QVOGHKWV.js} +2 -2
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/package.json +0 -2
- package/payload/server/public/assets/{Checkbox-D1OQD43b.js → Checkbox-YIF0payo.js} +1 -1
- package/payload/server/public/assets/{admin-czNBxWor.js → admin-DW8IJcLc.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BcwgT80u.js → architectureDiagram-Q4EWVU46-Bz8mlxZZ.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BMSyZUQA.js → blockDiagram-DXYQGD6D-DwV8Z8-i.js} +1 -1
- package/payload/server/public/assets/{brand-2cku8WFs.css → brand-DqiRNMlu.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DPRGY1jJ.js → c4Diagram-AHTNJAMY-DiUTejMp.js} +1 -1
- package/payload/server/public/assets/channel-PtVtoBEL.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-B7oQ3g1c.js → chunk-336JU56O-4mHZpBXe.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-C1P0yFXw.js → chunk-426QAEUC-Cbv0vrN9.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-LI7kOJd0.js → chunk-4TB4RGXK-BvLhId_2.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-CXQRGTQE.js → chunk-5FUZZQ4R-bBafOTkw.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-NSyzpXRy.js → chunk-5PVQY5BW-B0NqBKVy.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-voNwxbDs.js → chunk-EDXVE4YY-CFd4SqI6.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-CMEMPzYY.js → chunk-ENJZ2VHE-ajf2sb6c.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-hEbwu-pe.js → chunk-ICPOFSXX-pWg6bug7.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DxskDrLs.js → chunk-OYMX7WX6-OjEd-17c.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-D7TKgUo0.js → chunk-U2HBQHQK-DbEFSPoh.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-BvPUQEPm.js → chunk-X2U36JSP-COdNwrBb.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BY-RWQUW.js → chunk-YZCP3GAM-CHMWuY9B.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-DZvOYDY6.js → chunk-ZZ45TVLE-B-uDLQOB.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-RVH_SEhY.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Cm3rAb93.js +1 -0
- package/payload/server/public/assets/clone-BjY0Wzht.js +1 -0
- package/payload/server/public/assets/{dagre-KV5264BT-Cnj0mUZl.js → dagre-KV5264BT-CMEzmhIL.js} +1 -1
- package/payload/server/public/assets/{dagre-Bt-fpckL.js → dagre-bhIG_KnW.js} +1 -1
- package/payload/server/public/assets/data-K_kS__sL.js +1 -0
- package/payload/server/public/assets/{device-url-actions-Bjz3Xzbm.js → device-url-actions-AcOyLSeF.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-DjLzvOlx.js → diagram-5BDNPKRD-6RIoQhIL.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-DTfuRd-T.js → diagram-G4DWMVQ6-BSp36TVv.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-BaL2mCnx.js → diagram-MMDJMWI5-D54fo52D.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-C5InWY5R.js → diagram-TYMM5635-CWL8z-Pq.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DO7BXTpn.js → erDiagram-SMLLAGMA-AnnHBo3z.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DDdAKfLf.js → flowDiagram-DWJPFMVM-laWmBl5o.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-arJD8Utm.js → ganttDiagram-T4ZO3ILL-B94ko8ie.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-C55GH-OS.js → gitGraphDiagram-UUTBAWPF-DxzL1fxZ.js} +1 -1
- package/payload/server/public/assets/graph-DeEigyO_.js +1 -0
- package/payload/server/public/assets/graph-labels-C7I5QvNv.js +1 -0
- package/payload/server/public/assets/{graphlib-DL9PM7Ex.js → graphlib-CY-zIElM.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-BMSGqUbG.js → infoDiagram-42DDH7IO-BMTajIIr.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-Dw6BZ6BG.js → ishikawaDiagram-UXIWVN3A-B_QauE5O.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DrywUGXw.js → journeyDiagram-VCZTEJTY-DmlqSIih.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-DuwtVBBc.js → kanban-definition-6JOO6SKY-ZGDQT7xB.js} +1 -1
- package/payload/server/public/assets/{line-JAksyKHj.js → line-D13opgep.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-BMq-ApBW.js → mermaid-parser.core-C650Sual.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-tH4oX0Kh.js → mermaid.core-BqnQoXTp.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-D1OiiJga.js → mindmap-definition-QFDTVHPH-BS_8y-tY.js} +1 -1
- package/payload/server/public/assets/{page-BZpoS7iR.js → page-B_rpjIRr.js} +1 -1
- package/payload/server/public/assets/{page-CkvBvezS.js → page-qSH972X0.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-Ckwm69PW.js → pieDiagram-DEJITSTG-B5OmNvBO.js} +1 -1
- package/payload/server/public/assets/{public-C-dTMgXu.js → public-DDsYgotk.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-COw3yZ1j.js → quadrantDiagram-34T5L4WZ-DTYITdNo.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DqGzM4K-.js → requirementDiagram-MS252O5E-CRZWxH06.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-D-l1c_Pl.js → sankeyDiagram-XADWPNL6-DazRENhe.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BeIi0DtJ.js → sequenceDiagram-FGHM5R23-BcHTxmPy.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-C-jgegLk.js → stateDiagram-FHFEXIEX-DYU7nbqg.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BgljVtlp.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BGFKkYmi.js → timeline-definition-GMOUNBTQ-BKGmqkST.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-5NuIhJLS.js → vennDiagram-DHZGUBPP-BXvLPmX7.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-Be9ytVut.js → wardleyDiagram-NUSXRM2D-BCclUa3Z.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-DCyHg41R.js → xychartDiagram-5P7HB3ND-C-Xp-Eoc.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +1152 -2564
- package/payload/platform/scripts/check-sdk-oauth.mjs +0 -185
- package/payload/server/public/assets/channel-fxEghWew.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BsWzGW0N.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BGVa3h90.js +0 -1
- package/payload/server/public/assets/clone-Khvocke2.js +0 -1
- package/payload/server/public/assets/data-DBd-Buhp.js +0 -1
- package/payload/server/public/assets/graph-DUtVdnZ6.js +0 -1
- package/payload/server/public/assets/graph-labels-Dxfue-fP.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BaMs8Znv.js +0 -1
- /package/payload/server/public/assets/{brand-CSQuxS9w.js → brand-Bm671owU.js} +0 -0
|
@@ -279,6 +279,78 @@ cat "${CFG_DIR}/config.yml"
|
|
|
279
279
|
cloudflared --origincert "${CFG_DIR}/cert.pem" --config "${CFG_DIR}/config.yml" tunnel ingress validate
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
+
### Off-LAN SSH and SMB ingress (Task 009)
|
|
283
|
+
|
|
284
|
+
The same tunnel can also carry SSH and SMB to the Pi so an off-LAN
|
|
285
|
+
operator can SSH in or mount the brand share without VPN or port
|
|
286
|
+
forwarding. Insert SSH/SMB ingress rules between the HTTP rules and the
|
|
287
|
+
catch-all `http_status:404`:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
ingress:
|
|
291
|
+
- hostname: admin.maxy.bot
|
|
292
|
+
service: http://localhost:${PORT}
|
|
293
|
+
- hostname: public.maxy.bot
|
|
294
|
+
service: http://localhost:${PORT}
|
|
295
|
+
- hostname: ssh.maxy.bot
|
|
296
|
+
service: ssh://localhost:22
|
|
297
|
+
- hostname: smb.maxy.bot
|
|
298
|
+
service: tcp://localhost:445
|
|
299
|
+
- service: http_status:404
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Then route the new hostnames (Step 4 equivalent):
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel route dns --overwrite-dns "${TUNNEL_ID}" ssh.maxy.bot
|
|
306
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel route dns --overwrite-dns "${TUNNEL_ID}" smb.maxy.bot
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Order matters in the script path:** `setup-tunnel.sh` routes HTTPS
|
|
310
|
+
hostnames first and rewrites `config.yml` only after the HTTPS pass is
|
|
311
|
+
durable, so a failure on `ssh` or `smb` route DNS does not nuke the
|
|
312
|
+
HTTPS ingress; the failing hostname is logged as
|
|
313
|
+
`[tunnel-install] {ssh,smb}-ingress-deferred` and config.yml is rendered
|
|
314
|
+
without it.
|
|
315
|
+
|
|
316
|
+
**SMB depends on Task 034.** Before adding the SMB ingress, confirm the
|
|
317
|
+
brand stanza exists in `/etc/samba/smb.conf`:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
grep -q "^\[${BRAND}\]" /etc/samba/smb.conf && echo present || echo missing
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
If `missing`, provision Samba first (the installer's post-install step
|
|
324
|
+
does this on every Pi). The script skips the SMB ingress with
|
|
325
|
+
`[tunnel-install] smb-ingress-skipped reason=samba-not-provisioned`
|
|
326
|
+
rather than failing.
|
|
327
|
+
|
|
328
|
+
**Access policy is dashboard-only.** Cloudflare API/SDK is forbidden;
|
|
329
|
+
`cloudflared` CLI has no Access-application create subcommand. After
|
|
330
|
+
the ingress is in place, author the policy in the dashboard:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
Cloudflare → Zero Trust → Access → Applications → Add → Self-hosted
|
|
334
|
+
Application name: ssh.maxy.bot (or smb.maxy.bot)
|
|
335
|
+
Application domain: ssh.maxy.bot (or smb.maxy.bot)
|
|
336
|
+
Policy → Action: Allow
|
|
337
|
+
Include → Emails: <operator email>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Verify ingress validity:
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel ingress validate "${CFG_DIR}/config.yml"
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Then verify off-LAN reachability from a Mac:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
cloudflared access ssh --hostname ssh.<brand>.<rootdomain>
|
|
350
|
+
cloudflared access tcp --hostname smb.<brand>.<rootdomain> --url tcp://localhost:4445
|
|
351
|
+
# In Finder: Cmd-K → smb://localhost:4445
|
|
352
|
+
```
|
|
353
|
+
|
|
282
354
|
---
|
|
283
355
|
|
|
284
356
|
## Step 5b — Write tunnel.state
|
|
@@ -300,11 +372,19 @@ cat > "${CFG_DIR}/tunnel.state" <<EOF
|
|
|
300
372
|
"tunnelName": "${BRAND}-$(hostname -s)",
|
|
301
373
|
"domain": "maxy.bot",
|
|
302
374
|
"configPath": "${CFG_DIR}/config.yml",
|
|
303
|
-
"credentialsPath": "${CFG_DIR}/${TUNNEL_ID}.json"
|
|
375
|
+
"credentialsPath": "${CFG_DIR}/${TUNNEL_ID}.json",
|
|
376
|
+
"sshHostname": "ssh.maxy.bot",
|
|
377
|
+
"smbHostname": "smb.maxy.bot"
|
|
304
378
|
}
|
|
305
379
|
EOF
|
|
306
380
|
```
|
|
307
381
|
|
|
382
|
+
Omit `sshHostname` and `smbHostname` when those ingresses are not in
|
|
383
|
+
use. The fields are additive; the script's re-run path rehydrates them
|
|
384
|
+
on every invocation so an operator running `setup-tunnel.sh` without
|
|
385
|
+
`SSH_HOSTNAME` / `SMB_HOSTNAME` env vars does not silently drop the
|
|
386
|
+
SSH/SMB ingress.
|
|
387
|
+
|
|
308
388
|
**Why each field:** `resume-tunnel.sh` reads `tunnelId`, `domain`, `configPath` (all used; domain is for logging, tunnelId for the log line, configPath is passed as `--config` to cloudflared). `credentialsPath` and `tunnelName` are not read by `resume-tunnel.sh` itself but are consumed by other tools (e.g. `tunnel-status` in the cloudflared plugin), so write them anyway.
|
|
309
389
|
|
|
310
390
|
**Success:**
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Task 009 — acceptance grid for tunnel-ingress.ts pure layer.
|
|
2
|
+
//
|
|
3
|
+
// Runs under `node --test --experimental-strip-types` (no compile step). No
|
|
4
|
+
// filesystem reads except via temp files written by the test itself; no
|
|
5
|
+
// network; no shell-outs. Mirrors apt-resolve / samba-provision style.
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import {
|
|
13
|
+
renderConfigYml,
|
|
14
|
+
renderTunnelState,
|
|
15
|
+
readPersistedHostnames,
|
|
16
|
+
probeSambaStanza,
|
|
17
|
+
renderAccessPolicyActionRequired,
|
|
18
|
+
type IngressSpec,
|
|
19
|
+
type TunnelState,
|
|
20
|
+
} from "../tunnel-ingress.ts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// renderConfigYml
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
test("renderConfigYml: HTTPS-only matches pre-Task-009 shape byte-for-byte", () => {
|
|
27
|
+
const spec: IngressSpec = {
|
|
28
|
+
tunnelId: "abc-123",
|
|
29
|
+
credentialsPath: "/home/admin/.maxy/cloudflared/abc-123.json",
|
|
30
|
+
httpPort: 19200,
|
|
31
|
+
httpHostnames: ["admin.maxy.bot", "public.maxy.bot"],
|
|
32
|
+
};
|
|
33
|
+
assert.equal(
|
|
34
|
+
renderConfigYml(spec),
|
|
35
|
+
[
|
|
36
|
+
"tunnel: abc-123",
|
|
37
|
+
"credentials-file: /home/admin/.maxy/cloudflared/abc-123.json",
|
|
38
|
+
"ingress:",
|
|
39
|
+
" - hostname: admin.maxy.bot",
|
|
40
|
+
" service: http://localhost:19200",
|
|
41
|
+
" - hostname: public.maxy.bot",
|
|
42
|
+
" service: http://localhost:19200",
|
|
43
|
+
" - service: http_status:404",
|
|
44
|
+
"",
|
|
45
|
+
].join("\n"),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("renderConfigYml: SSH+SMB appear AFTER http hostnames and BEFORE catch-all", () => {
|
|
50
|
+
const spec: IngressSpec = {
|
|
51
|
+
tunnelId: "t-id",
|
|
52
|
+
credentialsPath: "/creds.json",
|
|
53
|
+
httpPort: 19200,
|
|
54
|
+
httpHostnames: ["admin.maxy.bot"],
|
|
55
|
+
sshHostname: "ssh.maxy.bot",
|
|
56
|
+
smbHostname: "smb.maxy.bot",
|
|
57
|
+
};
|
|
58
|
+
const out = renderConfigYml(spec);
|
|
59
|
+
const idxHttp = out.indexOf("admin.maxy.bot");
|
|
60
|
+
const idxSsh = out.indexOf("ssh.maxy.bot");
|
|
61
|
+
const idxSmb = out.indexOf("smb.maxy.bot");
|
|
62
|
+
const idx404 = out.indexOf("http_status:404");
|
|
63
|
+
assert.ok(idxHttp < idxSsh, "HTTP before SSH");
|
|
64
|
+
assert.ok(idxSsh < idxSmb, "SSH before SMB");
|
|
65
|
+
assert.ok(idxSmb < idx404, "SMB before catch-all 404");
|
|
66
|
+
assert.match(out, /\s+service: ssh:\/\/localhost:22$/m);
|
|
67
|
+
assert.match(out, /\s+service: tcp:\/\/localhost:445$/m);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("renderConfigYml: SSH only — no SMB block when smbHostname is null", () => {
|
|
71
|
+
const spec: IngressSpec = {
|
|
72
|
+
tunnelId: "t-id",
|
|
73
|
+
credentialsPath: "/creds.json",
|
|
74
|
+
httpPort: 19200,
|
|
75
|
+
httpHostnames: ["admin.maxy.bot"],
|
|
76
|
+
sshHostname: "ssh.maxy.bot",
|
|
77
|
+
smbHostname: null,
|
|
78
|
+
};
|
|
79
|
+
const out = renderConfigYml(spec);
|
|
80
|
+
assert.ok(out.includes("ssh.maxy.bot"));
|
|
81
|
+
assert.ok(!out.includes("tcp://localhost:445"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("renderConfigYml: empty string hostnames are treated as absent", () => {
|
|
85
|
+
const spec: IngressSpec = {
|
|
86
|
+
tunnelId: "t-id",
|
|
87
|
+
credentialsPath: "/creds.json",
|
|
88
|
+
httpPort: 19200,
|
|
89
|
+
httpHostnames: ["admin.maxy.bot"],
|
|
90
|
+
sshHostname: "",
|
|
91
|
+
smbHostname: "",
|
|
92
|
+
};
|
|
93
|
+
const out = renderConfigYml(spec);
|
|
94
|
+
assert.ok(!out.includes("ssh://localhost:22"));
|
|
95
|
+
assert.ok(!out.includes("tcp://localhost:445"));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// renderTunnelState
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
test("renderTunnelState: pre-Task-009 fields preserved when ssh/smb absent", () => {
|
|
103
|
+
const state: TunnelState = {
|
|
104
|
+
tunnelId: "abc",
|
|
105
|
+
tunnelName: "maxy",
|
|
106
|
+
domain: "admin.maxy.bot",
|
|
107
|
+
configPath: "/home/admin/.maxy/cloudflared/config.yml",
|
|
108
|
+
credentialsPath: "/home/admin/.maxy/cloudflared/abc.json",
|
|
109
|
+
};
|
|
110
|
+
const parsed = JSON.parse(renderTunnelState(state));
|
|
111
|
+
assert.deepEqual(parsed, {
|
|
112
|
+
tunnelId: "abc",
|
|
113
|
+
tunnelName: "maxy",
|
|
114
|
+
domain: "admin.maxy.bot",
|
|
115
|
+
configPath: "/home/admin/.maxy/cloudflared/config.yml",
|
|
116
|
+
credentialsPath: "/home/admin/.maxy/cloudflared/abc.json",
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("renderTunnelState: ssh/smb hostnames persist when present", () => {
|
|
121
|
+
const state: TunnelState = {
|
|
122
|
+
tunnelId: "abc",
|
|
123
|
+
tunnelName: "maxy",
|
|
124
|
+
domain: "admin.maxy.bot",
|
|
125
|
+
configPath: "/cfg.yml",
|
|
126
|
+
credentialsPath: "/creds.json",
|
|
127
|
+
sshHostname: "ssh.maxy.bot",
|
|
128
|
+
smbHostname: "smb.maxy.bot",
|
|
129
|
+
};
|
|
130
|
+
const parsed = JSON.parse(renderTunnelState(state));
|
|
131
|
+
assert.equal(parsed.sshHostname, "ssh.maxy.bot");
|
|
132
|
+
assert.equal(parsed.smbHostname, "smb.maxy.bot");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// readPersistedHostnames
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
test("readPersistedHostnames: returns nulls when file missing", () => {
|
|
140
|
+
const got = readPersistedHostnames("/nope/does/not/exist.json");
|
|
141
|
+
assert.deepEqual(got, { sshHostname: null, smbHostname: null });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("readPersistedHostnames: round-trips ssh/smb hostnames from disk", () => {
|
|
145
|
+
const dir = mkdtempSync(join(tmpdir(), "tunnel-state-"));
|
|
146
|
+
const path = join(dir, "tunnel.state");
|
|
147
|
+
try {
|
|
148
|
+
writeFileSync(
|
|
149
|
+
path,
|
|
150
|
+
JSON.stringify({
|
|
151
|
+
tunnelId: "abc",
|
|
152
|
+
tunnelName: "maxy",
|
|
153
|
+
domain: "admin.maxy.bot",
|
|
154
|
+
configPath: "/cfg.yml",
|
|
155
|
+
credentialsPath: "/creds.json",
|
|
156
|
+
sshHostname: "ssh.maxy.bot",
|
|
157
|
+
smbHostname: "smb.maxy.bot",
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
const got = readPersistedHostnames(path);
|
|
161
|
+
assert.deepEqual(got, { sshHostname: "ssh.maxy.bot", smbHostname: "smb.maxy.bot" });
|
|
162
|
+
} finally {
|
|
163
|
+
rmSync(dir, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("readPersistedHostnames: malformed JSON returns nulls (no throw)", () => {
|
|
168
|
+
const dir = mkdtempSync(join(tmpdir(), "tunnel-state-bad-"));
|
|
169
|
+
const path = join(dir, "tunnel.state");
|
|
170
|
+
try {
|
|
171
|
+
writeFileSync(path, "not json");
|
|
172
|
+
assert.deepEqual(readPersistedHostnames(path), { sshHostname: null, smbHostname: null });
|
|
173
|
+
} finally {
|
|
174
|
+
rmSync(dir, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// probeSambaStanza
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
test("probeSambaStanza: true when [<brand>] header present", () => {
|
|
183
|
+
const dir = mkdtempSync(join(tmpdir(), "smb-probe-"));
|
|
184
|
+
const path = join(dir, "smb.conf");
|
|
185
|
+
try {
|
|
186
|
+
writeFileSync(path, "[global]\nworkgroup = WORKGROUP\n\n[maxy]\n path = /home/admin\n");
|
|
187
|
+
assert.equal(probeSambaStanza("maxy", path), true);
|
|
188
|
+
} finally {
|
|
189
|
+
rmSync(dir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("probeSambaStanza: false when only peer brand present (isolation)", () => {
|
|
194
|
+
const dir = mkdtempSync(join(tmpdir(), "smb-probe-peer-"));
|
|
195
|
+
const path = join(dir, "smb.conf");
|
|
196
|
+
try {
|
|
197
|
+
writeFileSync(path, "[global]\n\n[realagent]\n path = /home/admin\n");
|
|
198
|
+
assert.equal(probeSambaStanza("maxy", path), false);
|
|
199
|
+
} finally {
|
|
200
|
+
rmSync(dir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("probeSambaStanza: false when smb.conf missing", () => {
|
|
205
|
+
assert.equal(probeSambaStanza("maxy", "/nope/smb.conf"), false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("probeSambaStanza: brand names with regex meta characters are escaped", () => {
|
|
209
|
+
const dir = mkdtempSync(join(tmpdir(), "smb-probe-regex-"));
|
|
210
|
+
const path = join(dir, "smb.conf");
|
|
211
|
+
try {
|
|
212
|
+
writeFileSync(path, "[some.brand]\n path = /x\n");
|
|
213
|
+
assert.equal(probeSambaStanza("some.brand", path), true);
|
|
214
|
+
assert.equal(probeSambaStanza("someXbrand", path), false);
|
|
215
|
+
} finally {
|
|
216
|
+
rmSync(dir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// renderAccessPolicyActionRequired
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
test("renderAccessPolicyActionRequired: empty string when neither hostname set", () => {
|
|
225
|
+
assert.equal(
|
|
226
|
+
renderAccessPolicyActionRequired({ operatorEmail: "x@y.com" }),
|
|
227
|
+
"",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("renderAccessPolicyActionRequired: lists each hostname with operator email", () => {
|
|
232
|
+
const out = renderAccessPolicyActionRequired({
|
|
233
|
+
sshHostname: "ssh.maxy.bot",
|
|
234
|
+
smbHostname: "smb.maxy.bot",
|
|
235
|
+
operatorEmail: "joel@example.com",
|
|
236
|
+
});
|
|
237
|
+
assert.match(out, /SSH: ssh\.maxy\.bot/);
|
|
238
|
+
assert.match(out, /SMB: smb\.maxy\.bot/);
|
|
239
|
+
assert.match(out, /Emails: joel@example\.com/);
|
|
240
|
+
assert.match(out, /Zero Trust → Access → Applications/);
|
|
241
|
+
});
|
|
@@ -27,15 +27,20 @@
|
|
|
27
27
|
|
|
28
28
|
set -euo pipefail
|
|
29
29
|
|
|
30
|
+
# Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
|
|
31
|
+
# (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
|
|
32
|
+
# BASH_SOURCE[0] points at $HOME, not the scripts directory where the sibling
|
|
33
|
+
# helpers (_stream-log.sh, tunnel-ingress.ts) live. Factored to SCRIPT_DIR so
|
|
34
|
+
# Task 600's stream-log-contract scanner can statically resolve the
|
|
35
|
+
# tunnel-ingress.ts target (Task 054).
|
|
36
|
+
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
|
37
|
+
|
|
30
38
|
# --------------------------------------------------------------------------
|
|
31
39
|
# Shared stream-log helpers (require STREAM_LOG_PATH, phase_line, …).
|
|
32
40
|
# --------------------------------------------------------------------------
|
|
33
41
|
|
|
34
42
|
# shellcheck source=_stream-log.sh
|
|
35
|
-
|
|
36
|
-
# (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
|
|
37
|
-
# BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
|
|
38
|
-
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
43
|
+
source "${SCRIPT_DIR}/_stream-log.sh"
|
|
39
44
|
require_stream_log_path setup-tunnel
|
|
40
45
|
|
|
41
46
|
# --------------------------------------------------------------------------
|
|
@@ -53,7 +58,39 @@ PORT="$2"
|
|
|
53
58
|
shift 2
|
|
54
59
|
HOSTNAMES=("$@")
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
# --------------------------------------------------------------------------
|
|
62
|
+
# Task 009 — SSH/SMB ingress env vars.
|
|
63
|
+
#
|
|
64
|
+
# SSH and SMB hostnames arrive via environment, NOT positional argv, so the
|
|
65
|
+
# existing form/endpoint contract (which passes admin/public/apex as
|
|
66
|
+
# positionals) keeps working unchanged. The action-runner sets these env
|
|
67
|
+
# vars when the operator submits the cloudflare-setup form with the SSH
|
|
68
|
+
# and SMB fields populated; absent env vars mean "no SSH/SMB ingress this
|
|
69
|
+
# run" — except when tunnel.state already persists them, in which case
|
|
70
|
+
# rehydrate from there.
|
|
71
|
+
#
|
|
72
|
+
# SSH_HOSTNAME e.g. ssh.maxy.bot → ingress service: ssh://localhost:22
|
|
73
|
+
# SMB_HOSTNAME e.g. smb.maxy.bot → ingress service: tcp://localhost:445
|
|
74
|
+
# OPERATOR_EMAIL e.g. joel@x.com → printed in the ACTION REQUIRED
|
|
75
|
+
# dashboard click-path for the
|
|
76
|
+
# Access policy (operator-authored;
|
|
77
|
+
# CF API is banned per
|
|
78
|
+
# feedback_cf_api_total_eradication)
|
|
79
|
+
#
|
|
80
|
+
# Re-run idempotency: if env vars are unset, the script reads sshHostname /
|
|
81
|
+
# smbHostname from the existing tunnel.state (if present) so a re-run
|
|
82
|
+
# without env vars doesn't silently drop the SSH/SMB ingress. tunnel.state
|
|
83
|
+
# is rewritten with the resolved hostnames at the end of Step 5b.
|
|
84
|
+
# --------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
SSH_HOSTNAME="${SSH_HOSTNAME:-}"
|
|
87
|
+
SMB_HOSTNAME="${SMB_HOSTNAME:-}"
|
|
88
|
+
OPERATOR_EMAIL="${OPERATOR_EMAIL:-}"
|
|
89
|
+
SETUP_TUNNEL_DRY_RUN="${SETUP_TUNNEL_DRY_RUN:-0}"
|
|
90
|
+
|
|
91
|
+
phase_line setup-tunnel step=start brand="${BRAND}" port="${PORT}" hostnames="${HOSTNAMES[*]}" \
|
|
92
|
+
ssh_hostname="${SSH_HOSTNAME:-none}" smb_hostname="${SMB_HOSTNAME:-none}" \
|
|
93
|
+
dry_run="${SETUP_TUNNEL_DRY_RUN}"
|
|
57
94
|
|
|
58
95
|
# --------------------------------------------------------------------------
|
|
59
96
|
# Step 0: Set brand context (paths + dirs). Corresponds to runbook Step 0.
|
|
@@ -636,39 +673,241 @@ for H in "${HOSTNAMES[@]}"; do
|
|
|
636
673
|
done
|
|
637
674
|
|
|
638
675
|
# --------------------------------------------------------------------------
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
#
|
|
676
|
+
# Task 009 — Rehydrate SSH/SMB hostnames from tunnel.state when env unset.
|
|
677
|
+
#
|
|
678
|
+
# A re-run without SSH_HOSTNAME / SMB_HOSTNAME should keep the previously
|
|
679
|
+
# configured ingress rather than silently dropping them. The Node helper
|
|
680
|
+
# reads tunnel.state (if present) and prints a JSON {sshHostname, smbHostname}.
|
|
642
681
|
# --------------------------------------------------------------------------
|
|
643
682
|
|
|
683
|
+
TUNNEL_INGRESS_TS="${SCRIPT_DIR}/tunnel-ingress.ts"
|
|
684
|
+
if [ ! -f "${TUNNEL_INGRESS_TS}" ]; then
|
|
685
|
+
phase_line setup-tunnel step=ingress-renderer-resolve result=error \
|
|
686
|
+
reason=helper-missing path="${TUNNEL_INGRESS_TS}"
|
|
687
|
+
echo "ERROR: tunnel-ingress.ts is missing at ${TUNNEL_INGRESS_TS}" >&2
|
|
688
|
+
exit 1
|
|
689
|
+
fi
|
|
690
|
+
|
|
691
|
+
if [ -z "${SSH_HOSTNAME}${SMB_HOSTNAME}" ] && [ -f "${CFG_DIR}/tunnel.state" ]; then
|
|
692
|
+
PERSISTED_JSON="$(node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" read-state "${CFG_DIR}/tunnel.state" 2>/dev/null || echo '{}')"
|
|
693
|
+
PERSISTED_SSH="$(printf '%s' "${PERSISTED_JSON}" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const o=JSON.parse(d);process.stdout.write(o.sshHostname||"")}catch{}})')"
|
|
694
|
+
PERSISTED_SMB="$(printf '%s' "${PERSISTED_JSON}" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{try{const o=JSON.parse(d);process.stdout.write(o.smbHostname||"")}catch{}})')"
|
|
695
|
+
if [ -n "${PERSISTED_SSH}" ]; then
|
|
696
|
+
SSH_HOSTNAME="${PERSISTED_SSH}"
|
|
697
|
+
phase_line setup-tunnel step=ssh-hostname-rehydrated hostname="${SSH_HOSTNAME}"
|
|
698
|
+
fi
|
|
699
|
+
if [ -n "${PERSISTED_SMB}" ]; then
|
|
700
|
+
SMB_HOSTNAME="${PERSISTED_SMB}"
|
|
701
|
+
phase_line setup-tunnel step=smb-hostname-rehydrated hostname="${SMB_HOSTNAME}"
|
|
702
|
+
fi
|
|
703
|
+
fi
|
|
704
|
+
|
|
705
|
+
# --------------------------------------------------------------------------
|
|
706
|
+
# Task 009 — Samba presence probe (gates SMB ingress on Task 034 stanza).
|
|
707
|
+
#
|
|
708
|
+
# Per Task 034, the brand's Samba stanza `[<brand>]` is written to
|
|
709
|
+
# /etc/samba/smb.conf at install time. If the stanza is absent, this Pi
|
|
710
|
+
# has no SMB share to expose and the SMB ingress is skipped loudly.
|
|
711
|
+
# Probe via grep on the Pi filesystem (NOT brand.json — Samba is a
|
|
712
|
+
# post-install side effect, not a declared field).
|
|
713
|
+
# --------------------------------------------------------------------------
|
|
714
|
+
|
|
715
|
+
if [ -n "${SMB_HOSTNAME}" ]; then
|
|
716
|
+
if ! node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" probe-samba "${BRAND}" /etc/samba/smb.conf >/dev/null 2>&1; then
|
|
717
|
+
phase_line setup-tunnel step=smb-ingress-skipped reason=samba-not-provisioned \
|
|
718
|
+
hostname="${SMB_HOSTNAME}" brand="${BRAND}"
|
|
719
|
+
echo "[tunnel-install] smb-ingress-skipped reason=samba-not-provisioned brand=${BRAND}"
|
|
720
|
+
echo " Skipping SMB ingress: /etc/samba/smb.conf has no [${BRAND}] stanza."
|
|
721
|
+
echo " Provision Samba first (the installer's post-install step does this); then re-run."
|
|
722
|
+
SMB_HOSTNAME=""
|
|
723
|
+
fi
|
|
724
|
+
fi
|
|
725
|
+
|
|
726
|
+
# --------------------------------------------------------------------------
|
|
727
|
+
# Task 009 — Step 4b: Route DNS for SSH/SMB hostnames (after HTTPS pass).
|
|
728
|
+
#
|
|
729
|
+
# Two-phase ordering: HTTPS hostnames are routed first (loop above). If a
|
|
730
|
+
# subsequent SSH or SMB route DNS fails, emit `*-ingress-deferred` and
|
|
731
|
+
# continue WITHOUT exit 1, so the HTTPS ingress remains durable. Apex
|
|
732
|
+
# hostnames are not valid for SSH/SMB (the hostnames are subdomains by
|
|
733
|
+
# definition); no apex carve-out is needed.
|
|
734
|
+
# --------------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
route_extra_hostname() {
|
|
737
|
+
local kind="$1" # "ssh" or "smb"
|
|
738
|
+
local hostname="$2"
|
|
739
|
+
[ -z "${hostname}" ] && return 0
|
|
740
|
+
|
|
741
|
+
if [ "${SETUP_TUNNEL_DRY_RUN}" = "1" ]; then
|
|
742
|
+
phase_line setup-tunnel step="${kind}-route-dns" hostname="${hostname}" result=dry-run-skip
|
|
743
|
+
return 0
|
|
744
|
+
fi
|
|
745
|
+
|
|
746
|
+
phase_line setup-tunnel step="${kind}-route-dns" hostname="${hostname}" tunnel_id="${TUNNEL_ID}"
|
|
747
|
+
local ROUTE_LOG
|
|
748
|
+
ROUTE_LOG="$(mktemp -t "maxy-${kind}-route-dns.XXXXXX")"
|
|
749
|
+
if tee_subprocess_capture "setup-tunnel:cloudflared" -- \
|
|
750
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" \
|
|
751
|
+
tunnel route dns --overwrite-dns "${TUNNEL_ID}" "${hostname}" \
|
|
752
|
+
> "${ROUTE_LOG}"; then
|
|
753
|
+
phase_line setup-tunnel step="${kind}-route-dns" hostname="${hostname}" result=ok
|
|
754
|
+
rm -f "${ROUTE_LOG}"
|
|
755
|
+
return 0
|
|
756
|
+
fi
|
|
757
|
+
local ROUTE_RC=$?
|
|
758
|
+
local STDERR_BOUNDED
|
|
759
|
+
STDERR_BOUNDED="$(tr '\n' ' ' < "${ROUTE_LOG}" | head -c 400)"
|
|
760
|
+
phase_line setup-tunnel step="${kind}-ingress-deferred" hostname="${hostname}" \
|
|
761
|
+
reason=route-dns-failed exit="${ROUTE_RC}" stderr="${STDERR_BOUNDED}"
|
|
762
|
+
echo "WARNING: cloudflared tunnel route dns failed for ${hostname} (exit=${ROUTE_RC})." >&2
|
|
763
|
+
echo " HTTPS ingress remains durable; the ${kind} ingress is deferred." >&2
|
|
764
|
+
rm -f "${ROUTE_LOG}"
|
|
765
|
+
# Clear the hostname so the config.yml render below skips this entry.
|
|
766
|
+
if [ "${kind}" = "ssh" ]; then SSH_HOSTNAME=""; else SMB_HOSTNAME=""; fi
|
|
767
|
+
return 0
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
route_extra_hostname ssh "${SSH_HOSTNAME}"
|
|
771
|
+
route_extra_hostname smb "${SMB_HOSTNAME}"
|
|
772
|
+
|
|
773
|
+
# --------------------------------------------------------------------------
|
|
774
|
+
# Step 5: Write config.yml via the unit-tested Node renderer. Every HTTPS
|
|
775
|
+
# hostname (apex + subdomain) gets an ingress rule — apex still needs the
|
|
776
|
+
# rule for the connector to serve traffic once DNS is manually pointed.
|
|
777
|
+
# SSH and SMB rules are inserted between the HTTPS rules and the catch-all
|
|
778
|
+
# http_status:404 (renderer enforces ordering).
|
|
779
|
+
# --------------------------------------------------------------------------
|
|
780
|
+
|
|
781
|
+
INGRESS_SPEC_FILE="$(mktemp -t maxy-ingress-spec.XXXXXX.json)"
|
|
644
782
|
{
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
783
|
+
printf '{'
|
|
784
|
+
printf '"tunnelId":"%s",' "${TUNNEL_ID}"
|
|
785
|
+
printf '"credentialsPath":"%s",' "${CFG_DIR}/${TUNNEL_ID}.json"
|
|
786
|
+
printf '"httpPort":%s,' "${PORT}"
|
|
787
|
+
printf '"httpHostnames":['
|
|
788
|
+
FIRST=1
|
|
648
789
|
for H in "${HOSTNAMES[@]}"; do
|
|
649
|
-
|
|
650
|
-
|
|
790
|
+
if [ ${FIRST} -eq 1 ]; then FIRST=0; else printf ','; fi
|
|
791
|
+
printf '"%s"' "${H}"
|
|
651
792
|
done
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
793
|
+
printf '],'
|
|
794
|
+
printf '"sshHostname":"%s",' "${SSH_HOSTNAME}"
|
|
795
|
+
printf '"smbHostname":"%s"' "${SMB_HOSTNAME}"
|
|
796
|
+
printf '}'
|
|
797
|
+
} > "${INGRESS_SPEC_FILE}"
|
|
798
|
+
|
|
799
|
+
if [ "${SETUP_TUNNEL_DRY_RUN}" = "1" ]; then
|
|
800
|
+
echo "--- DRY RUN: rendered config.yml ---"
|
|
801
|
+
node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" render-config "${INGRESS_SPEC_FILE}"
|
|
802
|
+
echo "--- DRY RUN: end ---"
|
|
803
|
+
phase_line setup-tunnel step=config-yml-write result=dry-run-skip
|
|
804
|
+
else
|
|
805
|
+
if ! node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" render-config "${INGRESS_SPEC_FILE}" > "${CFG_DIR}/config.yml"; then
|
|
806
|
+
phase_line setup-tunnel step=config-yml-write result=error reason=renderer-failed
|
|
807
|
+
echo "ERROR: tunnel-ingress render-config failed" >&2
|
|
808
|
+
rm -f "${INGRESS_SPEC_FILE}"
|
|
809
|
+
exit 1
|
|
810
|
+
fi
|
|
811
|
+
echo "wrote ${CFG_DIR}/config.yml"
|
|
812
|
+
fi
|
|
813
|
+
rm -f "${INGRESS_SPEC_FILE}"
|
|
814
|
+
|
|
815
|
+
if [ -n "${SSH_HOSTNAME}" ]; then
|
|
816
|
+
phase_line setup-tunnel step=ssh-ingress-added hostname="${SSH_HOSTNAME}" service=ssh://localhost:22
|
|
817
|
+
echo "[tunnel-install] ssh-ingress-added hostname=${SSH_HOSTNAME} service=ssh://localhost:22"
|
|
818
|
+
fi
|
|
819
|
+
if [ -n "${SMB_HOSTNAME}" ]; then
|
|
820
|
+
phase_line setup-tunnel step=smb-ingress-added hostname="${SMB_HOSTNAME}" service=tcp://localhost:445
|
|
821
|
+
echo "[tunnel-install] smb-ingress-added hostname=${SMB_HOSTNAME} service=tcp://localhost:445"
|
|
822
|
+
fi
|
|
655
823
|
|
|
656
824
|
# --------------------------------------------------------------------------
|
|
657
|
-
# Step 5b: Write tunnel.state
|
|
658
|
-
# the
|
|
659
|
-
#
|
|
825
|
+
# Step 5b: Write tunnel.state via the same renderer. Additive sshHostname /
|
|
826
|
+
# smbHostname fields let the next re-run rehydrate them. Required by
|
|
827
|
+
# resume-tunnel.sh — without it the brand.service's ExecStartPre exits
|
|
828
|
+
# silently without spawning a connector.
|
|
660
829
|
# --------------------------------------------------------------------------
|
|
661
830
|
|
|
662
|
-
|
|
831
|
+
STATE_SPEC_FILE="$(mktemp -t maxy-tunnel-state.XXXXXX.json)"
|
|
663
832
|
{
|
|
664
|
-
|
|
665
|
-
"
|
|
666
|
-
"
|
|
667
|
-
"
|
|
668
|
-
"
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
833
|
+
printf '{'
|
|
834
|
+
printf '"tunnelId":"%s",' "${TUNNEL_ID}"
|
|
835
|
+
printf '"tunnelName":"%s",' "${TUNNEL_NAME}"
|
|
836
|
+
printf '"domain":"%s",' "${HOSTNAMES[0]}"
|
|
837
|
+
printf '"configPath":"%s",' "${CFG_DIR}/config.yml"
|
|
838
|
+
printf '"credentialsPath":"%s",' "${CFG_DIR}/${TUNNEL_ID}.json"
|
|
839
|
+
printf '"sshHostname":"%s",' "${SSH_HOSTNAME}"
|
|
840
|
+
printf '"smbHostname":"%s"' "${SMB_HOSTNAME}"
|
|
841
|
+
printf '}'
|
|
842
|
+
} > "${STATE_SPEC_FILE}"
|
|
843
|
+
|
|
844
|
+
if [ "${SETUP_TUNNEL_DRY_RUN}" = "1" ]; then
|
|
845
|
+
echo "--- DRY RUN: rendered tunnel.state ---"
|
|
846
|
+
node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" render-state "${STATE_SPEC_FILE}"
|
|
847
|
+
echo "--- DRY RUN: end ---"
|
|
848
|
+
phase_line setup-tunnel step=tunnel-state-write result=dry-run-skip
|
|
849
|
+
else
|
|
850
|
+
if ! node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" render-state "${STATE_SPEC_FILE}" > "${CFG_DIR}/tunnel.state"; then
|
|
851
|
+
phase_line setup-tunnel step=tunnel-state-write result=error reason=renderer-failed
|
|
852
|
+
echo "ERROR: tunnel-ingress render-state failed" >&2
|
|
853
|
+
rm -f "${STATE_SPEC_FILE}"
|
|
854
|
+
exit 1
|
|
855
|
+
fi
|
|
856
|
+
echo "wrote ${CFG_DIR}/tunnel.state"
|
|
857
|
+
fi
|
|
858
|
+
rm -f "${STATE_SPEC_FILE}"
|
|
859
|
+
|
|
860
|
+
# --------------------------------------------------------------------------
|
|
861
|
+
# Task 009 — Access policy ACTION REQUIRED.
|
|
862
|
+
#
|
|
863
|
+
# The Cloudflare Access policy that gates these new SSH/SMB hostnames must
|
|
864
|
+
# be authored in the dashboard by the operator: CF API/SDK is banned
|
|
865
|
+
# (feedback_cf_api_total_eradication) and `cloudflared` CLI has no
|
|
866
|
+
# Access-application create subcommand. Print the click-path with the
|
|
867
|
+
# resolved hostnames + operator email substituted in. operatorEmail comes
|
|
868
|
+
# from OPERATOR_EMAIL env, falling back to brand.json `.operatorEmail`.
|
|
869
|
+
# --------------------------------------------------------------------------
|
|
870
|
+
|
|
871
|
+
if [ -n "${SSH_HOSTNAME}${SMB_HOSTNAME}" ]; then
|
|
872
|
+
RESOLVED_EMAIL="${OPERATOR_EMAIL}"
|
|
873
|
+
if [ -z "${RESOLVED_EMAIL}" ] && [ -n "${SETUP_TUNNEL_BRAND_JSON}" ] && command -v jq >/dev/null 2>&1; then
|
|
874
|
+
RESOLVED_EMAIL="$(jq -r '.operatorEmail // empty' "${SETUP_TUNNEL_BRAND_JSON}" 2>/dev/null || true)"
|
|
875
|
+
fi
|
|
876
|
+
if [ -z "${RESOLVED_EMAIL}" ]; then
|
|
877
|
+
RESOLVED_EMAIL="<operator email — set OPERATOR_EMAIL or brand.json .operatorEmail>"
|
|
878
|
+
phase_line setup-tunnel step=operator-email-unresolved reason=env-and-brand-json-missing
|
|
879
|
+
fi
|
|
880
|
+
|
|
881
|
+
if [ -n "${SSH_HOSTNAME}" ]; then
|
|
882
|
+
phase_line setup-tunnel step=ssh-access-policy-required hostname="${SSH_HOSTNAME}" allow_emails="${RESOLVED_EMAIL}"
|
|
883
|
+
echo "[tunnel-install] ssh-access-policy-required hostname=${SSH_HOSTNAME} allow-emails=${RESOLVED_EMAIL}"
|
|
884
|
+
fi
|
|
885
|
+
if [ -n "${SMB_HOSTNAME}" ]; then
|
|
886
|
+
phase_line setup-tunnel step=smb-access-policy-required hostname="${SMB_HOSTNAME}" allow_emails="${RESOLVED_EMAIL}"
|
|
887
|
+
echo "[tunnel-install] smb-access-policy-required hostname=${SMB_HOSTNAME} allow-emails=${RESOLVED_EMAIL}"
|
|
888
|
+
fi
|
|
889
|
+
|
|
890
|
+
AR_INPUT_FILE="$(mktemp -t maxy-access-req.XXXXXX.json)"
|
|
891
|
+
{
|
|
892
|
+
printf '{'
|
|
893
|
+
printf '"sshHostname":"%s",' "${SSH_HOSTNAME}"
|
|
894
|
+
printf '"smbHostname":"%s",' "${SMB_HOSTNAME}"
|
|
895
|
+
printf '"operatorEmail":"%s"' "${RESOLVED_EMAIL}"
|
|
896
|
+
printf '}'
|
|
897
|
+
} > "${AR_INPUT_FILE}"
|
|
898
|
+
node --experimental-strip-types --no-warnings "${TUNNEL_INGRESS_TS}" action-req "${AR_INPUT_FILE}" || true
|
|
899
|
+
rm -f "${AR_INPUT_FILE}"
|
|
900
|
+
fi
|
|
901
|
+
|
|
902
|
+
# --------------------------------------------------------------------------
|
|
903
|
+
# Dry-run short-circuit: exit before onboarding / service restart.
|
|
904
|
+
# --------------------------------------------------------------------------
|
|
905
|
+
if [ "${SETUP_TUNNEL_DRY_RUN}" = "1" ]; then
|
|
906
|
+
phase_line setup-tunnel step=done dry_run=1
|
|
907
|
+
echo ""
|
|
908
|
+
echo "DRY RUN complete. No cloudflared mutations made; no service restart."
|
|
909
|
+
exit 0
|
|
910
|
+
fi
|
|
672
911
|
|
|
673
912
|
# --------------------------------------------------------------------------
|
|
674
913
|
# Step 6: Persist onboarding step-7 completion BEFORE arming the restart.
|