elanous 0.2.5 → 0.2.6
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/README.md +5 -4
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7527.943753b6ed829f09.js → 1883.c91a2648af4842fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.js → 2984-977cfa339fcd65a5.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/{4853-35fdbe6bdbcdc0fc.js → 3165-95515ca5f299a120.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5391-8793f88b03b30161.js → 3995-bee512a7063036ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 4240-7153881b0b377306.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5329.8b7e4bca684b6eec.js +5 -0
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5348-bbf838e119661eee.js → 763-7d0e1913b5074e98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8617.465ca22a319c95b3.js → 7633.0654fe6ea259c1ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9238.2cd5f47f4bf7a2c3.js → 8250.4a2c5553a07cdf47.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4daa2798-1363d725c79f3593.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6f3850d-7f324c6566002113.js → 91ff6dda-3b33d42d20657429.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-84cd419494e16b0f.js → page-c3890e90bd232732.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{06ef2eba-d1b99ac6d2ea663c.js → beb5cf10-9adcc10b83930090.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-2bf59c720fe9542c.js → webpack-37b8bf78a3c6eec5.js} +1 -1
- package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +21 -21
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +21 -21
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +21 -21
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +20 -20
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +20 -20
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +20 -20
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +21 -21
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +21 -21
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +21 -21
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +20 -20
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +21 -21
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +21 -21
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +20 -20
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +20 -20
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +21 -21
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +21 -21
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +21 -21
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +21 -21
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +20 -20
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +21 -21
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +21 -21
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +21 -21
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +21 -21
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +21 -21
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +20 -20
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +21 -21
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +21 -21
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +21 -21
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +21 -21
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +21 -21
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +21 -21
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +21 -21
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +21 -21
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/package.json +1 -1
- package/plugins/card-followup/README.md +33 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +16 -0
- package/plugins/card-followup/graphs/recipes.yaml +12 -0
- package/plugins/card-followup/graphs/run-step.ts +172 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/geo-check/README.md +13 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +177 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/README.md +30 -0
- package/scripts/coord-tracks.json +2 -2
- package/scripts/daemon-port-baseline.txt +1 -2
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/release-loop/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +46 -13
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/ux/record-page.ts +4 -1
- package/skills/explainer-video/SKILL.md +18 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +128 -0
- package/skills/explainer-video/engine/reel.sh +8 -0
- package/skills/explainer-video/engine/vo.ts +58 -16
- package/src/autopilot/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/track-agent.ts +3 -3
- package/src/autopilot/track-courier.ts +2 -2
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +27 -0
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/release-cli.ts +14 -11
- package/src/domains/outbound-alert.ts +5 -5
- package/src/field/field-media.ts +245 -0
- package/src/index.ts +18 -24
- package/src/nexus/api/field-uploads.ts +143 -0
- package/src/nexus/api/graph-approvals.ts +50 -1
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +9 -0
- package/src/notify/llm-budget-alert.ts +119 -0
- package/src/plugins/maker/plugin-maker.ts +208 -0
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +28 -2
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-implement/run-ledger-compact.ts +1 -1
- package/src/session/index.ts +23 -10
- package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
- package/src/telegram.ts +108 -1
- package/src/user-config.ts +4 -0
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1751.50b96ea9c37b49b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
- package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0ac5d053f0444d21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-bccce0babceae30f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
- /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import sodium from 'libsodium-wrappers';
|
|
3
|
+
|
|
4
|
+
/** Maximum size of a complete encrypted frame, including sequence and MAC. */
|
|
5
|
+
export const MAX_FRAME_BYTES = 1024 * 1024;
|
|
6
|
+
const SEQUENCE_BYTES = 8;
|
|
7
|
+
const MAC_BYTES = 16;
|
|
8
|
+
const NONCE_BYTES = 24;
|
|
9
|
+
const MAX_SEQUENCE = (1n << 64n) - 1n;
|
|
10
|
+
|
|
11
|
+
export type ChannelResult<T> = { ok: true; value: T } | { ok: false; reason: string };
|
|
12
|
+
export interface DaemonKeyPair {
|
|
13
|
+
publicKey: Uint8Array;
|
|
14
|
+
secretKey: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
export interface SerializedDaemonKeyPair {
|
|
17
|
+
publicKey: string;
|
|
18
|
+
secretKey: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function rejected<T>(reason: string): ChannelResult<T> {
|
|
22
|
+
return { ok: false, reason };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validKey(key: Uint8Array): boolean {
|
|
26
|
+
return key instanceof Uint8Array && key.length === 32;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function equalKeys(left: Uint8Array, right: Uint8Array): boolean {
|
|
30
|
+
return validKey(left) && validKey(right) && timingSafeEqual(left, right);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The daemon persists this pair; the phone creates a fresh pair per connection. */
|
|
34
|
+
export async function generateDaemonKeyPair(seed?: Uint8Array): Promise<DaemonKeyPair> {
|
|
35
|
+
await sodium.ready;
|
|
36
|
+
if (seed !== undefined && !validKey(seed)) throw new RangeError('X25519 seed must be 32 bytes');
|
|
37
|
+
const pair = seed === undefined ? sodium.crypto_kx_keypair() : sodium.crypto_kx_seed_keypair(seed);
|
|
38
|
+
return { publicKey: pair.publicKey, secretKey: pair.privateKey };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function serverIdFromPublicKey(publicKey: Uint8Array): string {
|
|
42
|
+
if (!validKey(publicKey)) throw new RangeError('X25519 public key must be 32 bytes');
|
|
43
|
+
return createHash('sha256').update(publicKey).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serializeDaemonKeyPair(pair: DaemonKeyPair): SerializedDaemonKeyPair {
|
|
47
|
+
if (!validKey(pair.publicKey) || !validKey(pair.secretKey)) throw new RangeError('invalid X25519 key pair');
|
|
48
|
+
return { publicKey: Buffer.from(pair.publicKey).toString('base64'), secretKey: Buffer.from(pair.secretKey).toString('base64') };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function deserializeDaemonKeyPair(serialized: SerializedDaemonKeyPair): Promise<ChannelResult<DaemonKeyPair>> {
|
|
52
|
+
await sodium.ready;
|
|
53
|
+
if (typeof serialized?.publicKey !== 'string' || typeof serialized?.secretKey !== 'string' ||
|
|
54
|
+
!/^[A-Za-z0-9+/]{43}=$/.test(serialized.publicKey) || !/^[A-Za-z0-9+/]{43}=$/.test(serialized.secretKey)) {
|
|
55
|
+
return rejected('invalid daemon key serialization');
|
|
56
|
+
}
|
|
57
|
+
const publicKey = new Uint8Array(Buffer.from(serialized.publicKey, 'base64'));
|
|
58
|
+
const secretKey = new Uint8Array(Buffer.from(serialized.secretKey, 'base64'));
|
|
59
|
+
try {
|
|
60
|
+
if (!validKey(publicKey) || !validKey(secretKey) ||
|
|
61
|
+
!equalKeys(sodium.crypto_scalarmult_base(secretKey), publicKey)) {
|
|
62
|
+
return rejected('invalid daemon key pair');
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return rejected('invalid daemon key pair');
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, value: { publicKey, secretKey } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sequenceBytes(sequence: bigint): Uint8Array {
|
|
71
|
+
const bytes = new Uint8Array(SEQUENCE_BYTES);
|
|
72
|
+
new DataView(bytes.buffer).setBigUint64(0, sequence, false);
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function nonceFor(sequence: bigint): Uint8Array {
|
|
77
|
+
const nonce = new Uint8Array(NONCE_BYTES);
|
|
78
|
+
nonce.set(sequenceBytes(sequence), NONCE_BYTES - SEQUENCE_BYTES);
|
|
79
|
+
return nonce;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Each direction uses its own crypto_kx key and monotonically increasing sequence. */
|
|
83
|
+
export class EncryptedChannel {
|
|
84
|
+
private sendSequence = 0n;
|
|
85
|
+
private receiveSequence = 0n;
|
|
86
|
+
private readonly sendKey: Uint8Array;
|
|
87
|
+
private readonly receiveKey: Uint8Array;
|
|
88
|
+
|
|
89
|
+
constructor(sendKey: Uint8Array, receiveKey: Uint8Array) {
|
|
90
|
+
if (!validKey(sendKey) || !validKey(receiveKey)) throw new RangeError('invalid session keys');
|
|
91
|
+
this.sendKey = new Uint8Array(sendKey);
|
|
92
|
+
this.receiveKey = new Uint8Array(receiveKey);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
seal(plaintext: Uint8Array): ChannelResult<Uint8Array> {
|
|
96
|
+
if (!(plaintext instanceof Uint8Array)) return rejected('invalid plaintext');
|
|
97
|
+
if (plaintext.length + SEQUENCE_BYTES + MAC_BYTES > MAX_FRAME_BYTES) return rejected('frame exceeds 1MB');
|
|
98
|
+
if (this.sendSequence > MAX_SEQUENCE) return rejected('send sequence exhausted');
|
|
99
|
+
const header = sequenceBytes(this.sendSequence);
|
|
100
|
+
try {
|
|
101
|
+
const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
|
|
102
|
+
plaintext, header, null, nonceFor(this.sendSequence), this.sendKey,
|
|
103
|
+
);
|
|
104
|
+
const frame = new Uint8Array(header.length + ciphertext.length);
|
|
105
|
+
frame.set(header);
|
|
106
|
+
frame.set(ciphertext, header.length);
|
|
107
|
+
this.sendSequence++;
|
|
108
|
+
return { ok: true, value: frame };
|
|
109
|
+
} catch {
|
|
110
|
+
return rejected('frame encryption failed');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
open(frame: Uint8Array): ChannelResult<Uint8Array> {
|
|
115
|
+
if (!(frame instanceof Uint8Array) || frame.length < SEQUENCE_BYTES + MAC_BYTES) return rejected('invalid frame');
|
|
116
|
+
if (frame.length > MAX_FRAME_BYTES) return rejected('frame exceeds 1MB');
|
|
117
|
+
if (this.receiveSequence > MAX_SEQUENCE) return rejected('receive sequence exhausted');
|
|
118
|
+
const sequence = new DataView(frame.buffer, frame.byteOffset, SEQUENCE_BYTES).getBigUint64(0, false);
|
|
119
|
+
if (sequence < this.receiveSequence) return rejected('replayed frame');
|
|
120
|
+
if (sequence > this.receiveSequence) return rejected('out-of-order frame');
|
|
121
|
+
const header = frame.subarray(0, SEQUENCE_BYTES);
|
|
122
|
+
try {
|
|
123
|
+
const plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
|
|
124
|
+
null, frame.subarray(SEQUENCE_BYTES), header, nonceFor(sequence), this.receiveKey,
|
|
125
|
+
);
|
|
126
|
+
this.receiveSequence++;
|
|
127
|
+
return { ok: true, value: plaintext };
|
|
128
|
+
} catch {
|
|
129
|
+
return rejected('frame authentication failed');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PhoneChannel {
|
|
135
|
+
/** Sent to the daemon during handshake; the private key stays on the phone. */
|
|
136
|
+
phonePublicKey: Uint8Array;
|
|
137
|
+
serverId: string;
|
|
138
|
+
channel: EncryptedChannel;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Pin the daemon's public key before deriving any traffic keys. */
|
|
142
|
+
export async function createPhoneChannel(
|
|
143
|
+
daemonPublicKey: Uint8Array,
|
|
144
|
+
expectedDaemonPublicKey: Uint8Array,
|
|
145
|
+
phoneSeed?: Uint8Array,
|
|
146
|
+
): Promise<ChannelResult<PhoneChannel>> {
|
|
147
|
+
await sodium.ready;
|
|
148
|
+
if (!validKey(daemonPublicKey) || !equalKeys(daemonPublicKey, expectedDaemonPublicKey)) {
|
|
149
|
+
return rejected('wrong daemon public key');
|
|
150
|
+
}
|
|
151
|
+
if (phoneSeed !== undefined && !validKey(phoneSeed)) return rejected('invalid phone seed');
|
|
152
|
+
const phoneKeys = phoneSeed === undefined ? sodium.crypto_kx_keypair() : sodium.crypto_kx_seed_keypair(phoneSeed);
|
|
153
|
+
try {
|
|
154
|
+
const keys = sodium.crypto_kx_client_session_keys(phoneKeys.publicKey, phoneKeys.privateKey, daemonPublicKey);
|
|
155
|
+
return { ok: true, value: {
|
|
156
|
+
phonePublicKey: phoneKeys.publicKey,
|
|
157
|
+
serverId: serverIdFromPublicKey(daemonPublicKey),
|
|
158
|
+
channel: new EncryptedChannel(keys.sharedTx, keys.sharedRx),
|
|
159
|
+
} };
|
|
160
|
+
} catch {
|
|
161
|
+
return rejected('invalid daemon public key');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Called with the phone's ephemeral public key; crypto_kx supplies opposite Tx/Rx keys. */
|
|
166
|
+
export async function acceptDaemonChannel(
|
|
167
|
+
daemonKeys: DaemonKeyPair,
|
|
168
|
+
phonePublicKey: Uint8Array,
|
|
169
|
+
): Promise<ChannelResult<EncryptedChannel>> {
|
|
170
|
+
await sodium.ready;
|
|
171
|
+
if (!validKey(daemonKeys?.publicKey) || !validKey(daemonKeys?.secretKey)) {
|
|
172
|
+
return rejected('invalid daemon key pair');
|
|
173
|
+
}
|
|
174
|
+
if (!validKey(phonePublicKey)) return rejected('invalid phone public key');
|
|
175
|
+
try {
|
|
176
|
+
if (!equalKeys(sodium.crypto_scalarmult_base(daemonKeys.secretKey), daemonKeys.publicKey)) {
|
|
177
|
+
return rejected('invalid daemon key pair');
|
|
178
|
+
}
|
|
179
|
+
const keys = sodium.crypto_kx_server_session_keys(daemonKeys.publicKey, daemonKeys.secretKey, phonePublicKey);
|
|
180
|
+
return { ok: true, value: new EncryptedChannel(keys.sharedTx, keys.sharedRx) };
|
|
181
|
+
} catch {
|
|
182
|
+
return rejected('invalid handshake key');
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/relay/mux.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { MAX_FRAME_BYTES } from './channel.js';
|
|
2
|
+
import type { ChannelResult } from './channel.js';
|
|
3
|
+
|
|
4
|
+
export type MuxFrame =
|
|
5
|
+
| { streamId: number; kind: 'open'; protocol: 'http'; method: string; path: string; headers: Record<string, string> }
|
|
6
|
+
| { streamId: number; kind: 'open'; protocol: 'ws'; path: string; headers: Record<string, string> }
|
|
7
|
+
| { streamId: number; kind: 'data'; data: Uint8Array }
|
|
8
|
+
| { streamId: number; kind: 'close'; reason: string };
|
|
9
|
+
|
|
10
|
+
// u32 BE length (remaining bytes), u32 BE stream ID, u8 kind (1=open, 2=data, 3=close).
|
|
11
|
+
// Open: u8 protocol (1=http, 2=ws), [http: u16 method bytes + UTF-8 method],
|
|
12
|
+
// u32 path bytes + UTF-8 path, u16 header count, then u16 name bytes + UTF-8 name,
|
|
13
|
+
// u16 value bytes + UTF-8 value per header. Data: remaining raw bytes.
|
|
14
|
+
// Close: u16 reason bytes + UTF-8 reason. One frame per encrypted plaintext.
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
17
|
+
const HEADER_BYTES = 9;
|
|
18
|
+
// EncryptedChannel appends an 8-byte sequence and 16-byte MAC to each plaintext.
|
|
19
|
+
export const MAX_MUX_FRAME_BYTES = MAX_FRAME_BYTES - 24;
|
|
20
|
+
|
|
21
|
+
type Result<T> = ChannelResult<T>;
|
|
22
|
+
function reject<T>(reason: string): Result<T> { return { ok: false, reason }; }
|
|
23
|
+
|
|
24
|
+
class Writer {
|
|
25
|
+
private parts: Uint8Array[] = [];
|
|
26
|
+
length = 0;
|
|
27
|
+
bytes(value: Uint8Array) { this.parts.push(value); this.length += value.length; }
|
|
28
|
+
number(value: number, size: 1 | 2 | 4) {
|
|
29
|
+
const bytes = new Uint8Array(size);
|
|
30
|
+
const view = new DataView(bytes.buffer);
|
|
31
|
+
if (size === 1) view.setUint8(0, value);
|
|
32
|
+
else if (size === 2) view.setUint16(0, value, false);
|
|
33
|
+
else view.setUint32(0, value, false);
|
|
34
|
+
this.bytes(bytes);
|
|
35
|
+
}
|
|
36
|
+
text(value: string, size: 2 | 4): boolean {
|
|
37
|
+
if (typeof value !== 'string') return false;
|
|
38
|
+
const bytes = encoder.encode(value);
|
|
39
|
+
if (bytes.length > (size === 2 ? 0xffff : 0xffffffff)) return false;
|
|
40
|
+
this.number(bytes.length, size);
|
|
41
|
+
this.bytes(bytes);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
finish(): Uint8Array {
|
|
45
|
+
const out = new Uint8Array(this.length);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const part of this.parts) { out.set(part, offset); offset += part.length; }
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class Reader {
|
|
53
|
+
offset = 0;
|
|
54
|
+
constructor(readonly bytes: Uint8Array) {}
|
|
55
|
+
number(size: 1 | 2 | 4): number | undefined {
|
|
56
|
+
if (this.offset + size > this.bytes.length) return undefined;
|
|
57
|
+
const view = new DataView(this.bytes.buffer, this.bytes.byteOffset + this.offset, size);
|
|
58
|
+
this.offset += size;
|
|
59
|
+
return size === 1 ? view.getUint8(0) : size === 2 ? view.getUint16(0, false) : view.getUint32(0, false);
|
|
60
|
+
}
|
|
61
|
+
text(size: 2 | 4): string | undefined {
|
|
62
|
+
const length = this.number(size);
|
|
63
|
+
if (length === undefined || this.offset + length > this.bytes.length) return undefined;
|
|
64
|
+
const bytes = this.bytes.subarray(this.offset, this.offset + length);
|
|
65
|
+
this.offset += length;
|
|
66
|
+
return decoder.decode(bytes);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validId(id: number): boolean { return Number.isInteger(id) && id >= 0 && id <= 0xffffffff; }
|
|
71
|
+
|
|
72
|
+
/** Share one instance between the encoder and decoder of a single endpoint. */
|
|
73
|
+
export class MuxStreamState {
|
|
74
|
+
private readonly active = new Set<number>();
|
|
75
|
+
|
|
76
|
+
isOpen(id: number): boolean { return this.active.has(id); }
|
|
77
|
+
opened(id: number): void { this.active.add(id); }
|
|
78
|
+
closed(id: number): void { this.active.delete(id); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** No network I/O; shares stream IDs with the receiving decoder when supplied a state. */
|
|
82
|
+
export class MuxEncoder {
|
|
83
|
+
constructor(private readonly streams: MuxStreamState = new MuxStreamState()) {}
|
|
84
|
+
|
|
85
|
+
encode(frame: MuxFrame): Result<Uint8Array> {
|
|
86
|
+
if (!frame || !validId(frame.streamId)) return reject('invalid stream ID');
|
|
87
|
+
if (frame.kind !== 'open' && frame.kind !== 'data' && frame.kind !== 'close') return reject('unknown frame kind');
|
|
88
|
+
if (frame.kind === 'open' ? this.streams.isOpen(frame.streamId) : !this.streams.isOpen(frame.streamId)) {
|
|
89
|
+
return reject(frame.kind === 'open' ? 'stream already open' : 'stream not open');
|
|
90
|
+
}
|
|
91
|
+
const writer = new Writer();
|
|
92
|
+
writer.number(0, 4);
|
|
93
|
+
writer.number(frame.streamId, 4);
|
|
94
|
+
if (frame.kind === 'open') {
|
|
95
|
+
writer.number(1, 1);
|
|
96
|
+
if (frame.protocol !== 'http' && frame.protocol !== 'ws') return reject('unknown protocol');
|
|
97
|
+
writer.number(frame.protocol === 'http' ? 1 : 2, 1);
|
|
98
|
+
if (frame.protocol === 'http' && (!frame.method || !writer.text(frame.method, 2))) return reject('invalid method');
|
|
99
|
+
if (!frame.path || !writer.text(frame.path, 4)) return reject('invalid path');
|
|
100
|
+
if (!frame.headers || typeof frame.headers !== 'object' || Array.isArray(frame.headers)) return reject('invalid headers');
|
|
101
|
+
const entries = Object.entries(frame.headers);
|
|
102
|
+
if (entries.length > 0xffff) return reject('invalid headers');
|
|
103
|
+
writer.number(entries.length, 2);
|
|
104
|
+
for (const [name, value] of entries) {
|
|
105
|
+
if (!name || !writer.text(name, 2) || !writer.text(value, 2)) return reject('invalid headers');
|
|
106
|
+
}
|
|
107
|
+
} else if (frame.kind === 'data') {
|
|
108
|
+
if (!(frame.data instanceof Uint8Array)) return reject('invalid data');
|
|
109
|
+
writer.number(2, 1);
|
|
110
|
+
writer.bytes(frame.data);
|
|
111
|
+
} else if (frame.kind === 'close') {
|
|
112
|
+
writer.number(3, 1);
|
|
113
|
+
if (!writer.text(frame.reason, 2)) return reject('invalid reason');
|
|
114
|
+
}
|
|
115
|
+
if (writer.length > MAX_MUX_FRAME_BYTES) return reject('frame exceeds 1MB');
|
|
116
|
+
const out = writer.finish();
|
|
117
|
+
new DataView(out.buffer).setUint32(0, out.length - 4, false);
|
|
118
|
+
if (frame.kind === 'open') this.streams.opened(frame.streamId);
|
|
119
|
+
if (frame.kind === 'close') this.streams.closed(frame.streamId);
|
|
120
|
+
return { ok: true, value: out };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Rejected frames never change stream state. Share state with the sending encoder per endpoint. */
|
|
125
|
+
export class MuxDecoder {
|
|
126
|
+
constructor(private readonly streams: MuxStreamState = new MuxStreamState()) {}
|
|
127
|
+
|
|
128
|
+
decode(bytes: Uint8Array): Result<MuxFrame> {
|
|
129
|
+
if (!(bytes instanceof Uint8Array)) return reject('invalid frame');
|
|
130
|
+
if (bytes.length > MAX_MUX_FRAME_BYTES) return reject('frame exceeds 1MB');
|
|
131
|
+
if (bytes.length < HEADER_BYTES) return reject('truncated frame');
|
|
132
|
+
const reader = new Reader(bytes);
|
|
133
|
+
const length = reader.number(4)!;
|
|
134
|
+
if (length !== bytes.length - 4) return reject(length > bytes.length - 4 ? 'truncated frame' : 'trailing frame bytes');
|
|
135
|
+
const streamId = reader.number(4)!;
|
|
136
|
+
const kind = reader.number(1)!;
|
|
137
|
+
if (kind !== 1 && kind !== 2 && kind !== 3) return reject('unknown frame kind');
|
|
138
|
+
if (kind === 1 ? this.streams.isOpen(streamId) : !this.streams.isOpen(streamId)) {
|
|
139
|
+
return reject(kind === 1 ? 'stream already open' : 'stream not open');
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
let frame: MuxFrame;
|
|
143
|
+
if (kind === 1) {
|
|
144
|
+
const protocolByte = reader.number(1);
|
|
145
|
+
if (protocolByte !== 1 && protocolByte !== 2) return reject(protocolByte === undefined ? 'truncated frame' : 'unknown protocol');
|
|
146
|
+
const method = protocolByte === 1 ? reader.text(2) : undefined;
|
|
147
|
+
if (protocolByte === 1 && method === undefined) return reject('truncated frame');
|
|
148
|
+
const path = reader.text(4);
|
|
149
|
+
const count = reader.number(2);
|
|
150
|
+
if (path === undefined || count === undefined) return reject('truncated frame');
|
|
151
|
+
if (!path || (protocolByte === 1 && !method)) return reject('invalid open frame');
|
|
152
|
+
const entries: [string, string][] = [];
|
|
153
|
+
const names = new Set<string>();
|
|
154
|
+
for (let i = 0; i < count; i++) {
|
|
155
|
+
const name = reader.text(2);
|
|
156
|
+
const value = reader.text(2);
|
|
157
|
+
if (name === undefined || value === undefined) return reject('truncated frame');
|
|
158
|
+
if (!name || names.has(name)) return reject('invalid headers');
|
|
159
|
+
names.add(name);
|
|
160
|
+
entries.push([name, value]);
|
|
161
|
+
}
|
|
162
|
+
const headers = Object.fromEntries(entries);
|
|
163
|
+
frame = protocolByte === 1
|
|
164
|
+
? { streamId, kind: 'open', protocol: 'http', method: method!, path, headers }
|
|
165
|
+
: { streamId, kind: 'open', protocol: 'ws', path, headers };
|
|
166
|
+
} else if (kind === 2) {
|
|
167
|
+
frame = { streamId, kind: 'data', data: bytes.slice(reader.offset) };
|
|
168
|
+
reader.offset = bytes.length;
|
|
169
|
+
} else {
|
|
170
|
+
const reason = reader.text(2);
|
|
171
|
+
if (reason === undefined) return reject('truncated frame');
|
|
172
|
+
frame = { streamId, kind: 'close', reason };
|
|
173
|
+
}
|
|
174
|
+
if (reader.offset !== bytes.length) return reject('trailing frame bytes');
|
|
175
|
+
if (kind === 1) this.streams.opened(streamId);
|
|
176
|
+
if (kind === 3) this.streams.closed(streamId);
|
|
177
|
+
return { ok: true, value: frame };
|
|
178
|
+
} catch {
|
|
179
|
+
return reject('invalid UTF-8 frame');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"daemonSeedHex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
|
|
3
|
+
"phoneSeedHex": "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
|
|
4
|
+
"daemonPublicKeyHex": "0e0216223f147143d32615a91189c288c1728cba3cc5f9f621b1026e03d83129",
|
|
5
|
+
"phonePublicKeyHex": "99f4674ecc87c0b8e712f192b8f49e7442a9376b4875967ababa28471019a93e",
|
|
6
|
+
"serverId": "4bc447d530c4044729c44372319cebfcbe079462826a6d39ea4828b149f9c685",
|
|
7
|
+
"phoneToDaemonKeySha256": "1fd0e0a3bd4e5afff9efb91653c4e67f549b2aaf26e8fea9a5fb263cbd987ea0",
|
|
8
|
+
"daemonToPhoneKeySha256": "534f8e2462d2bfe07a31cc43a2fd7a46d6109c31289c971c17626324b9806ee8",
|
|
9
|
+
"nonceHex": "000000000000000000000000000000000000000000000000",
|
|
10
|
+
"request": {
|
|
11
|
+
"streamId": 16909060,
|
|
12
|
+
"kind": "open",
|
|
13
|
+
"protocol": "http",
|
|
14
|
+
"method": "POST",
|
|
15
|
+
"path": "/v1/health",
|
|
16
|
+
"headers": {
|
|
17
|
+
"authorization": "Bearer vector-token"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"requestMuxHex": "000000400102030401010004504f53540000000a2f76312f6865616c74680001000d617574686f72697a6174696f6e001342656172657220766563746f722d746f6b656e",
|
|
21
|
+
"requestEncryptedHex": "0000000000000000d793343f86630442fcf3812bc1375c4b102aa1ffd546ac4ecbd5f5f4e9e4b3c2de47616a01df19b25b317cd0c5f3c2f0aa7a8b12f307c9872c5f03765a0b1c0e6b6f46dcbda1758ae286b475870eb7ad957f409e",
|
|
22
|
+
"response": {
|
|
23
|
+
"streamId": 16909060,
|
|
24
|
+
"kind": "data",
|
|
25
|
+
"dataHex": "00ff1020"
|
|
26
|
+
},
|
|
27
|
+
"responseMuxHex": "00000009010203040200ff1020",
|
|
28
|
+
"responseEncryptedHex": "0000000000000000bf589589e25e990a1c7da5fb7a773f0a798e84d1f444d51bd8e3e0bdb4"
|
|
29
|
+
}
|
|
@@ -6,12 +6,14 @@ import { debug } from '../debug/log.js';
|
|
|
6
6
|
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
7
7
|
|
|
8
8
|
export type ChecklistStatus = 'green' | 'yellow' | 'red' | 'done';
|
|
9
|
+
export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
|
|
9
10
|
export interface ChecklistItem {
|
|
10
11
|
id: string;
|
|
11
12
|
title: string;
|
|
12
13
|
status: ChecklistStatus;
|
|
13
14
|
owner?: string;
|
|
14
15
|
evidence?: string;
|
|
16
|
+
disposition?: ChecklistDisposition;
|
|
15
17
|
updatedAt: string;
|
|
16
18
|
updatedBy: string;
|
|
17
19
|
}
|
|
@@ -121,12 +123,13 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
121
123
|
});
|
|
122
124
|
}
|
|
123
125
|
|
|
124
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string }, by: string): Checklist {
|
|
126
|
+
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition }, by: string): Checklist {
|
|
125
127
|
return mutate(v, (data) => {
|
|
126
128
|
const item = data.items.find((i) => i.id === id);
|
|
127
129
|
if (!item) throw new Error(`없는 칸: ${id}`);
|
|
128
130
|
if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new Error(`잘못된 상태: ${patch.status}`);
|
|
129
|
-
|
|
131
|
+
if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new Error(`잘못된 처분: ${patch.disposition}`);
|
|
132
|
+
const fields = (['evidence', 'owner', 'status', 'disposition'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
|
|
130
133
|
if (fields.length === 0) return false;
|
|
131
134
|
const at = new Date().toISOString();
|
|
132
135
|
for (const field of fields) {
|
|
@@ -169,6 +172,29 @@ export function summarizeChecklist(data: Checklist): ChecklistSummary {
|
|
|
169
172
|
return summary;
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
export interface ChecklistGate {
|
|
176
|
+
ok: boolean;
|
|
177
|
+
red: string[];
|
|
178
|
+
undecided: string[];
|
|
179
|
+
blocked: string[];
|
|
180
|
+
moved: string[];
|
|
181
|
+
knownIssues: Array<{ id: string; title: string; evidence: string }>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function checklistGate(v: string): ChecklistGate {
|
|
185
|
+
const result: ChecklistGate = { ok: true, red: [], undecided: [], blocked: [], moved: [], knownIssues: [] };
|
|
186
|
+
for (const item of listChecklist(v).items) {
|
|
187
|
+
if (item.status === 'red') result.red.push(item.id);
|
|
188
|
+
if (item.status !== 'yellow') continue;
|
|
189
|
+
if (item.disposition === 'block') result.blocked.push(item.id);
|
|
190
|
+
else if (item.disposition === 'move') result.moved.push(item.id);
|
|
191
|
+
else if (item.disposition === 'known-issue') result.knownIssues.push({ id: item.id, title: item.title, evidence: item.evidence ?? '' });
|
|
192
|
+
else result.undecided.push(item.id);
|
|
193
|
+
}
|
|
194
|
+
result.ok = result.red.length === 0 && result.undecided.length === 0 && result.blocked.length === 0;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
172
198
|
export function seedFromRoadmap(v: string, markdown: string): Checklist {
|
|
173
199
|
return mutate(v, (data) => {
|
|
174
200
|
let added = false;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import trackData from '../../scripts/coord-tracks.json';
|
|
2
|
+
|
|
3
|
+
export type SeatEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
alias?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SeatAddress = { seats: string[]; body: string };
|
|
10
|
+
|
|
11
|
+
const registry: readonly SeatEntry[] = trackData.tracks.filter((entry) => 'title' in entry);
|
|
12
|
+
const addressLine = /^@([A-Za-z][A-Za-z0-9_-]*(?:,[A-Za-z][A-Za-z0-9_-]*)*)(?:[ \t]+|\r?\n|(?![\s\S]))/gm;
|
|
13
|
+
|
|
14
|
+
/** Parse an address only when its @ begins a line; leave the instruction text intact. */
|
|
15
|
+
export function parseSeatAddress(text: string): SeatAddress | null {
|
|
16
|
+
const match = addressLine.exec(text);
|
|
17
|
+
addressLine.lastIndex = 0;
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
|
|
20
|
+
const seats = match[1]!.split(',');
|
|
21
|
+
const body = `${text.slice(0, match.index)}${text.slice(match.index + match[0].length)}`;
|
|
22
|
+
return { seats, body };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Resolve against the seat registry, preferring titles to ids to legacy aliases. */
|
|
26
|
+
export function resolveSeat(address: string, seats: readonly SeatEntry[] = registry): SeatEntry | undefined {
|
|
27
|
+
const needle = address.replace(/^@/, '').toLocaleLowerCase();
|
|
28
|
+
if (!needle) return undefined;
|
|
29
|
+
for (const field of ['title', 'id', 'alias'] as const) {
|
|
30
|
+
const found = seats.find((seat) => seat[field]?.toLocaleLowerCase() === needle);
|
|
31
|
+
if (found) return found;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -280,7 +280,7 @@ function recordedString(entries: readonly RunLedgerEntry[], field: string): stri
|
|
|
280
280
|
return undefined;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
function summarizeTerminalLedger(runId: string, entries: readonly RunLedgerEntry[]): RunLedgerSummary {
|
|
283
|
+
export function summarizeTerminalLedger(runId: string, entries: readonly RunLedgerEntry[]): RunLedgerSummary {
|
|
284
284
|
const last = entries.at(-1)?.timestamp;
|
|
285
285
|
if (!last || !Number.isFinite(Date.parse(last)) || !hasTerminalRunStatus(entries)) throw new Error('ledger changed since plan');
|
|
286
286
|
const terminator = [...entries].reverse().find((entry) => entry.event === 'human-stop' || entry.event === 'terminal'
|
package/src/session/index.ts
CHANGED
|
@@ -434,12 +434,20 @@ function readIndex(root: string = sessionRoot()): SessionMeta[] {
|
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
|
|
437
|
+
// 여러 프로세스(`elanous ask` 동시 실행 등)가 같은 목록을 «읽고-고치고-통째로 쓰기» 한다.
|
|
438
|
+
// 쓰기 직전 잠금 안에서 디스크의 최신 목록을 다시 읽어 id 로 합치고, 임시 파일 이름은 프로세스마다 다르게 한다
|
|
439
|
+
// (10-01 MK 재현: 고정 `index.json.tmp` → 한쪽 ENOENT · 덮어쓰기로 남의 세션이 목록에서 사라짐).
|
|
440
|
+
function writeIndex(entries: SessionMeta[], root: string = sessionRoot(), removed: Iterable<string> = []): void {
|
|
438
441
|
ensureRoots(root);
|
|
439
442
|
const p = indexPath(root);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
+
withDirLock(join(root, '.index.lock'), INDEX_LOCK_TIMEOUT_MS, () => {
|
|
444
|
+
const mine = new Set(entries.map((m) => m.id));
|
|
445
|
+
const gone = new Set(removed);
|
|
446
|
+
const peers = readIndex(root).filter((m) => !mine.has(m.id) && !gone.has(m.id));
|
|
447
|
+
const tmp = `${p}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
448
|
+
writeFileSync(tmp, JSON.stringify([...peers, ...entries], null, 2) + '\n', 'utf-8');
|
|
449
|
+
renameSync(tmp, p);
|
|
450
|
+
});
|
|
443
451
|
}
|
|
444
452
|
|
|
445
453
|
// ── Tier 1 Phase 3 — listener primitive ──────────────────────────────
|
|
@@ -661,7 +669,7 @@ export function rewriteSessionMessages(
|
|
|
661
669
|
const body = messages
|
|
662
670
|
.map(m => JSON.stringify(m.turn_id !== undefined ? m : { ...m, turn_id: mintTurnUri() }))
|
|
663
671
|
.join('\n');
|
|
664
|
-
const tmp = file
|
|
672
|
+
const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
665
673
|
writeFileSync(tmp, body ? body + '\n' : '', 'utf-8');
|
|
666
674
|
renameSync(tmp, file);
|
|
667
675
|
|
|
@@ -800,8 +808,13 @@ function residentSessionLockPath(root: string): string {
|
|
|
800
808
|
|
|
801
809
|
function withResidentSessionLock<T>(root: string, action: () => T): T {
|
|
802
810
|
ensureRoots(root);
|
|
803
|
-
|
|
804
|
-
|
|
811
|
+
return withDirLock(residentSessionLockPath(root), RESIDENT_SESSION_LOCK_TIMEOUT_MS, action);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const INDEX_LOCK_TIMEOUT_MS = 10_000;
|
|
815
|
+
|
|
816
|
+
function withDirLock<T>(lock: string, timeoutMs: number, action: () => T): T {
|
|
817
|
+
const deadline = Date.now() + timeoutMs;
|
|
805
818
|
for (;;) {
|
|
806
819
|
try {
|
|
807
820
|
mkdirSync(lock);
|
|
@@ -814,7 +827,7 @@ function withResidentSessionLock<T>(root: string, action: () => T): T {
|
|
|
814
827
|
continue;
|
|
815
828
|
}
|
|
816
829
|
} catch { continue; }
|
|
817
|
-
if (Date.now() >= deadline) throw new Error(`timed out acquiring
|
|
830
|
+
if (Date.now() >= deadline) throw new Error(`timed out acquiring lock ${lock}`);
|
|
818
831
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
819
832
|
void error;
|
|
820
833
|
}
|
|
@@ -855,7 +868,7 @@ export function deleteSession(id: string, root: string = sessionRoot()): boolean
|
|
|
855
868
|
const before = idx.length;
|
|
856
869
|
const next = idx.filter(m => m.id !== id);
|
|
857
870
|
if (next.length === before) return false;
|
|
858
|
-
writeIndex(next, root);
|
|
871
|
+
writeIndex(next, root, [id]);
|
|
859
872
|
// Clear active marker if it pointed at the deleted session.
|
|
860
873
|
if (getActiveSessionId() === id) clearActiveSessionId();
|
|
861
874
|
return true;
|
|
@@ -876,7 +889,7 @@ export function deleteSessions(ids: string[], root: string = sessionRoot()): num
|
|
|
876
889
|
const idx = readIndex(root);
|
|
877
890
|
const next = idx.filter(m => !set.has(m.id));
|
|
878
891
|
const removed = idx.length - next.length;
|
|
879
|
-
if (removed > 0) writeIndex(next, root);
|
|
892
|
+
if (removed > 0) writeIndex(next, root, set);
|
|
880
893
|
const active = getActiveSessionId();
|
|
881
894
|
if (active && set.has(active)) clearActiveSessionId();
|
|
882
895
|
return removed;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const POD_BUN_CACHE_HOST_PATH = 'POD_BUN_CACHE_HOST_PATH';
|
|
2
|
+
|
|
3
|
+
/** A host-backed Bun install cache; only opt into it when the mounted directory is writable. */
|
|
4
|
+
export function podBunCacheVolume(hostPath: string) {
|
|
5
|
+
return {
|
|
6
|
+
volume: { name: 'bun-cache', hostPath: { path: hostPath, type: 'DirectoryOrCreate' } },
|
|
7
|
+
volumeMount: { name: 'bun-cache', mountPath: '/bun-cache', readOnly: false },
|
|
8
|
+
shellPrefix: 'if [ -d /bun-cache ] && [ -w /bun-cache ]; then export BUN_INSTALL_CACHE_DIR=/bun-cache; fi;',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** First Bun install timing in a combined shard log (including a no-op install). */
|
|
13
|
+
export function parseInstallSeconds(log: string): number | null {
|
|
14
|
+
const match = /(?:\d+ packages? installed|Checked \d+ installs across \d+ packages(?: \(no changes\))?)\s*\[(\d+(?:\.\d+)?)(ms|s)\]/.exec(log);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
const duration = Number(match[1]);
|
|
17
|
+
return match[2] === 'ms' ? duration / 1000 : duration;
|
|
18
|
+
}
|