appduct 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +122 -52
  2. package/dist/artifact-inspect.d.ts +0 -1
  3. package/dist/bin.d.ts +0 -1
  4. package/dist/bin.js +1250 -2
  5. package/dist/cli/command-options.d.ts +8 -1
  6. package/dist/cli/create-cli.d.ts +0 -1
  7. package/dist/cli/dispatch.d.ts +10 -1
  8. package/dist/cli/envelope.d.ts +19 -0
  9. package/dist/cli/global-flags.d.ts +30 -0
  10. package/dist/cli/open-target.d.ts +25 -8
  11. package/dist/cli/result-types.d.ts +31 -5
  12. package/dist/cli/router.d.ts +78 -0
  13. package/dist/cli/routes/daemon/index.d.ts +9 -0
  14. package/dist/cli/routes/daemon/index.js +848 -0
  15. package/dist/cli/routes/daemon/run.d.ts +3 -0
  16. package/dist/cli/routes/daemon/run.js +3246 -0
  17. package/dist/cli/routes/daemon/start.d.ts +3 -0
  18. package/dist/cli/routes/daemon/start.js +1677 -0
  19. package/dist/cli/routes/daemon/status.d.ts +4 -0
  20. package/dist/cli/routes/daemon/status.js +1685 -0
  21. package/dist/cli/routes/daemon/stop.d.ts +4 -0
  22. package/dist/cli/routes/daemon/stop.js +1655 -0
  23. package/dist/cli/routes/doctor.d.ts +3 -0
  24. package/dist/cli/routes/doctor.js +1075 -0
  25. package/dist/cli/routes/events.d.ts +3 -0
  26. package/dist/cli/routes/events.js +1915 -0
  27. package/dist/cli/routes/init.d.ts +3 -0
  28. package/dist/cli/routes/init.js +1486 -0
  29. package/dist/cli/routes/invoke.d.ts +3 -0
  30. package/dist/cli/routes/invoke.js +1893 -0
  31. package/dist/cli/routes/keygen.d.ts +3 -0
  32. package/dist/cli/routes/keygen.js +928 -0
  33. package/dist/cli/routes/link.d.ts +3 -0
  34. package/dist/cli/routes/link.js +2714 -0
  35. package/dist/cli/routes/ls.d.ts +3 -0
  36. package/dist/cli/routes/ls.js +1668 -0
  37. package/dist/cli/routes/mcp.d.ts +5 -0
  38. package/dist/cli/routes/mcp.js +3929 -0
  39. package/dist/cli/routes/revoke.d.ts +3 -0
  40. package/dist/cli/routes/revoke.js +1684 -0
  41. package/dist/cli/routes/tools.d.ts +3 -0
  42. package/dist/cli/routes/tools.js +1860 -0
  43. package/dist/cli/runner.d.ts +3 -11
  44. package/dist/cli/types.d.ts +9 -6
  45. package/dist/cli/version-guard.d.ts +20 -0
  46. package/dist/cli.d.ts +0 -1
  47. package/dist/client/app-client.d.ts +4 -4
  48. package/dist/client/bootstrap.d.ts +4 -4
  49. package/dist/client/connect.d.ts +0 -1
  50. package/dist/client/errors.d.ts +0 -1
  51. package/dist/client/index.d.ts +1 -2
  52. package/dist/client/index.js +2389 -16
  53. package/dist/commands/daemon/context.d.ts +14 -0
  54. package/dist/commands/daemon/run.d.ts +11 -0
  55. package/dist/commands/daemon/start.d.ts +5 -0
  56. package/dist/commands/daemon/status.d.ts +5 -0
  57. package/dist/commands/daemon/stop.d.ts +5 -0
  58. package/dist/commands/doctor.d.ts +0 -1
  59. package/dist/commands/events.d.ts +0 -1
  60. package/dist/commands/init.d.ts +17 -4
  61. package/dist/commands/invoke.d.ts +0 -1
  62. package/dist/commands/keygen.d.ts +0 -1
  63. package/dist/commands/link.d.ts +1 -2
  64. package/dist/commands/ls.d.ts +0 -1
  65. package/dist/commands/mcp.d.ts +2 -4
  66. package/dist/commands/revoke.d.ts +0 -1
  67. package/dist/commands/tools.d.ts +19 -3
  68. package/dist/daemon/address.d.ts +0 -1
  69. package/dist/daemon/audit.d.ts +6 -11
  70. package/dist/daemon/calls.d.ts +0 -1
  71. package/dist/daemon/config.d.ts +13 -16
  72. package/dist/daemon/daemon.d.ts +0 -1
  73. package/dist/daemon/event-bus.d.ts +0 -1
  74. package/dist/daemon/links.d.ts +0 -1
  75. package/dist/daemon/listener.d.ts +0 -1
  76. package/dist/daemon/log-rotation.d.ts +0 -1
  77. package/dist/daemon/pidfile.d.ts +14 -8
  78. package/dist/daemon/policy.d.ts +0 -1
  79. package/dist/daemon/registry.d.ts +0 -1
  80. package/dist/daemon/rpc-errors.d.ts +16 -0
  81. package/dist/daemon/rpc-server.d.ts +0 -11
  82. package/dist/daemon/sessions.d.ts +0 -1
  83. package/dist/daemon/socket-probe.d.ts +0 -1
  84. package/dist/daemon/state-dir.d.ts +0 -1
  85. package/dist/daemon/timers.d.ts +0 -1
  86. package/dist/daemon/tls.d.ts +0 -1
  87. package/dist/errors.d.ts +0 -1
  88. package/dist/host-certificate.d.ts +0 -1
  89. package/dist/index.d.ts +0 -1
  90. package/dist/index.js +1307 -3
  91. package/dist/key-material.d.ts +0 -1
  92. package/dist/link.d.ts +8 -7
  93. package/dist/mcp/app-tools.d.ts +153 -0
  94. package/dist/mcp/connect-tool.d.ts +14 -8
  95. package/dist/mcp/daemon-tools.d.ts +2 -11
  96. package/dist/mcp/events-tool.d.ts +0 -1
  97. package/dist/mcp/server.d.ts +33 -14
  98. package/dist/native-scheme.d.ts +0 -1
  99. package/dist/output.d.ts +8 -11
  100. package/dist/package-root.d.ts +11 -0
  101. package/dist/package-version.d.ts +0 -1
  102. package/dist/qr-terminal.d.ts +0 -1
  103. package/dist/rpc/client.d.ts +3 -3
  104. package/dist/scheme.d.ts +60 -2
  105. package/dist/spki-pin.d.ts +0 -1
  106. package/package.json +6 -3
  107. package/dist/artifact-inspect.d.ts.map +0 -1
  108. package/dist/artifact-inspect.js +0 -334
  109. package/dist/bin.d.ts.map +0 -1
  110. package/dist/cli/command-options.d.ts.map +0 -1
  111. package/dist/cli/command-options.js +0 -95
  112. package/dist/cli/create-cli.d.ts.map +0 -1
  113. package/dist/cli/create-cli.js +0 -56
  114. package/dist/cli/dispatch.d.ts.map +0 -1
  115. package/dist/cli/dispatch.js +0 -275
  116. package/dist/cli/open-target.d.ts.map +0 -1
  117. package/dist/cli/open-target.js +0 -438
  118. package/dist/cli/result-types.d.ts.map +0 -1
  119. package/dist/cli/result-types.js +0 -6
  120. package/dist/cli/runner.d.ts.map +0 -1
  121. package/dist/cli/runner.js +0 -111
  122. package/dist/cli/types.d.ts.map +0 -1
  123. package/dist/cli/types.js +0 -10
  124. package/dist/cli.d.ts.map +0 -1
  125. package/dist/cli.js +0 -2
  126. package/dist/client/app-client.d.ts.map +0 -1
  127. package/dist/client/app-client.js +0 -223
  128. package/dist/client/bootstrap.d.ts.map +0 -1
  129. package/dist/client/bootstrap.js +0 -108
  130. package/dist/client/connect.d.ts.map +0 -1
  131. package/dist/client/connect.js +0 -38
  132. package/dist/client/errors.d.ts.map +0 -1
  133. package/dist/client/errors.js +0 -40
  134. package/dist/client/index.d.ts.map +0 -1
  135. package/dist/commands/daemon.d.ts +0 -22
  136. package/dist/commands/daemon.d.ts.map +0 -1
  137. package/dist/commands/daemon.js +0 -128
  138. package/dist/commands/doctor.d.ts.map +0 -1
  139. package/dist/commands/doctor.js +0 -42
  140. package/dist/commands/events.d.ts.map +0 -1
  141. package/dist/commands/events.js +0 -76
  142. package/dist/commands/init.d.ts.map +0 -1
  143. package/dist/commands/init.js +0 -231
  144. package/dist/commands/invoke.d.ts.map +0 -1
  145. package/dist/commands/invoke.js +0 -54
  146. package/dist/commands/keygen.d.ts.map +0 -1
  147. package/dist/commands/keygen.js +0 -46
  148. package/dist/commands/link.d.ts.map +0 -1
  149. package/dist/commands/link.js +0 -43
  150. package/dist/commands/ls.d.ts.map +0 -1
  151. package/dist/commands/ls.js +0 -7
  152. package/dist/commands/mcp.d.ts.map +0 -1
  153. package/dist/commands/mcp.js +0 -98
  154. package/dist/commands/revoke.d.ts.map +0 -1
  155. package/dist/commands/revoke.js +0 -7
  156. package/dist/commands/tools.d.ts.map +0 -1
  157. package/dist/commands/tools.js +0 -52
  158. package/dist/daemon/address.d.ts.map +0 -1
  159. package/dist/daemon/address.js +0 -71
  160. package/dist/daemon/audit.d.ts.map +0 -1
  161. package/dist/daemon/audit.js +0 -237
  162. package/dist/daemon/calls.d.ts.map +0 -1
  163. package/dist/daemon/calls.js +0 -158
  164. package/dist/daemon/config.d.ts.map +0 -1
  165. package/dist/daemon/config.js +0 -170
  166. package/dist/daemon/daemon.d.ts.map +0 -1
  167. package/dist/daemon/daemon.js +0 -560
  168. package/dist/daemon/event-bus.d.ts.map +0 -1
  169. package/dist/daemon/event-bus.js +0 -111
  170. package/dist/daemon/links.d.ts.map +0 -1
  171. package/dist/daemon/links.js +0 -118
  172. package/dist/daemon/listener.d.ts.map +0 -1
  173. package/dist/daemon/listener.js +0 -132
  174. package/dist/daemon/log-rotation.d.ts.map +0 -1
  175. package/dist/daemon/log-rotation.js +0 -136
  176. package/dist/daemon/pidfile.d.ts.map +0 -1
  177. package/dist/daemon/pidfile.js +0 -90
  178. package/dist/daemon/policy.d.ts.map +0 -1
  179. package/dist/daemon/policy.js +0 -24
  180. package/dist/daemon/registry.d.ts.map +0 -1
  181. package/dist/daemon/registry.js +0 -31
  182. package/dist/daemon/rpc-server.d.ts.map +0 -1
  183. package/dist/daemon/rpc-server.js +0 -203
  184. package/dist/daemon/sessions.d.ts.map +0 -1
  185. package/dist/daemon/sessions.js +0 -429
  186. package/dist/daemon/socket-probe.d.ts.map +0 -1
  187. package/dist/daemon/socket-probe.js +0 -28
  188. package/dist/daemon/state-dir.d.ts.map +0 -1
  189. package/dist/daemon/state-dir.js +0 -43
  190. package/dist/daemon/timers.d.ts.map +0 -1
  191. package/dist/daemon/timers.js +0 -12
  192. package/dist/daemon/tls.d.ts.map +0 -1
  193. package/dist/daemon/tls.js +0 -79
  194. package/dist/errors.d.ts.map +0 -1
  195. package/dist/errors.js +0 -162
  196. package/dist/host-certificate.d.ts.map +0 -1
  197. package/dist/host-certificate.js +0 -111
  198. package/dist/index.d.ts.map +0 -1
  199. package/dist/key-material.d.ts.map +0 -1
  200. package/dist/key-material.js +0 -42
  201. package/dist/link.d.ts.map +0 -1
  202. package/dist/link.js +0 -117
  203. package/dist/mcp/connect-tool.d.ts.map +0 -1
  204. package/dist/mcp/connect-tool.js +0 -396
  205. package/dist/mcp/daemon-tools.d.ts.map +0 -1
  206. package/dist/mcp/daemon-tools.js +0 -29
  207. package/dist/mcp/events-tool.d.ts.map +0 -1
  208. package/dist/mcp/events-tool.js +0 -322
  209. package/dist/mcp/server.d.ts.map +0 -1
  210. package/dist/mcp/server.js +0 -538
  211. package/dist/mcp/tool-mapping.d.ts +0 -58
  212. package/dist/mcp/tool-mapping.d.ts.map +0 -1
  213. package/dist/mcp/tool-mapping.js +0 -143
  214. package/dist/mcp/tool-namespace.d.ts +0 -40
  215. package/dist/mcp/tool-namespace.d.ts.map +0 -1
  216. package/dist/mcp/tool-namespace.js +0 -65
  217. package/dist/native-scheme.d.ts.map +0 -1
  218. package/dist/native-scheme.js +0 -465
  219. package/dist/output.d.ts.map +0 -1
  220. package/dist/output.js +0 -342
  221. package/dist/package-version.d.ts.map +0 -1
  222. package/dist/package-version.js +0 -47
  223. package/dist/qr-terminal.d.ts.map +0 -1
  224. package/dist/qr-terminal.js +0 -45
  225. package/dist/rpc/client.d.ts.map +0 -1
  226. package/dist/rpc/client.js +0 -819
  227. package/dist/scheme.d.ts.map +0 -1
  228. package/dist/scheme.js +0 -321
  229. package/dist/spki-pin.d.ts.map +0 -1
  230. package/dist/spki-pin.js +0 -14
@@ -1,28 +0,0 @@
1
- /**
2
- * "Is anything listening on the control socket?" — the cheapest liveness signal the state dir
3
- * offers, and the one that answers fastest when a daemon is up.
4
- *
5
- * It lives in its own module rather than in `rpc/client.ts` (its first caller) because
6
- * `daemon/log-rotation.ts` needs it too, and `rpc/client.ts` already imports *that*. Keeping the
7
- * probe here means neither has to import the other.
8
- */
9
- import { connect } from "node:net";
10
- /**
11
- * Resolves `true` when a connection to `socketPath` is accepted. A refusal, a missing socket file,
12
- * or any other error resolves `false` — this never rejects, so callers can use it as a plain
13
- * predicate. Note it proves a listener exists *right now*; it cannot prove one will not appear a
14
- * moment later.
15
- */
16
- export const isSocketConnectable = (socketPath) => {
17
- return new Promise((resolve) => {
18
- const socket = connect(socketPath);
19
- socket.once("connect", () => {
20
- socket.destroy();
21
- resolve(true);
22
- });
23
- socket.once("error", () => {
24
- socket.destroy();
25
- resolve(false);
26
- });
27
- });
28
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"state-dir.d.ts","sourceRoot":"","sources":["../../src/daemon/state-dir.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,aAAa,GAAG;IAC1B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,oGAAoG;IACpG,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,sGAAsG;AACtG,eAAO,MAAM,eAAe,GAAI,WAAW,MAAM,KAAG,MAEnD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,UAAU,MAAM,KAAG,aAWnD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,aAAa,KAAG,MAEpD,CAAC;AAEF,wGAAwG;AACxG,eAAO,MAAM,cAAc,GAAU,UAAU,MAAM,KAAG,OAAO,CAAC,aAAa,CAU5E,CAAC"}
@@ -1,43 +0,0 @@
1
- /**
2
- * State directory resolution and layout (ARCHITECTURE.md §3).
3
- *
4
- * Default `~/.appduct/`, overridable with `APPDUCT_STATE_DIR` (tests rely on the
5
- * override). Created lazily with mode `0700`.
6
- */
7
- import { homedir } from "node:os";
8
- import { join } from "node:path";
9
- import { mkdir, chmod } from "node:fs/promises";
10
- /** Resolves the state directory: explicit `override`, else `APPDUCT_STATE_DIR`, else `~/.appduct`. */
11
- export const resolveStateDir = (override) => {
12
- return override ?? process.env.APPDUCT_STATE_DIR ?? join(homedir(), ".appduct");
13
- };
14
- export const getStateDirPaths = (stateDir) => {
15
- return {
16
- root: stateDir,
17
- socketPath: join(stateDir, "daemon.sock"),
18
- pidFilePath: join(stateDir, "daemon.pid"),
19
- logFilePath: join(stateDir, "daemon.log"),
20
- keyPath: join(stateDir, "key.pem"),
21
- configPath: join(stateDir, "config.json"),
22
- auditDir: join(stateDir, "audit"),
23
- spawnLockPath: join(stateDir, "daemon.spawn.lock"),
24
- };
25
- };
26
- /**
27
- * Single seam for the control-socket address. Today this is always the UDS path; a Windows
28
- * named-pipe implementation (`\\.\pipe\appduct-<user>`) plugs in here without touching any
29
- * caller (ARCHITECTURE.md §13 — best-effort, not blocking v2.0).
30
- */
31
- export const getSocketPath = (paths) => {
32
- return paths.socketPath;
33
- };
34
- /** Creates the state dir (and `audit/`) with mode `0700` if missing; tightens the mode if it exists. */
35
- export const ensureStateDir = async (stateDir) => {
36
- const paths = getStateDirPaths(stateDir);
37
- await mkdir(paths.auditDir, { recursive: true });
38
- await chmod(paths.root, 0o700);
39
- // `mkdir` honors the process umask (commonly 0o022, yielding drwxr-xr-x); tighten explicitly so
40
- // `audit/` matches every other path under the state dir (ARCHITECTURE.md §3).
41
- await chmod(paths.auditDir, 0o700);
42
- return paths;
43
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"timers.d.ts","sourceRoot":"","sources":["../../src/daemon/timers.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AACxD,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAE5D,MAAM,MAAM,QAAQ,GAAG;IACrB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,WAAW,CAAC;IAC9D,YAAY,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC5C,WAAW,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,cAAc,CAAC;IAClE,aAAa,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;CACjD,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,QAK1B,CAAC"}
@@ -1,12 +0,0 @@
1
- /**
2
- * Injectable timer seam (LOOP.md: "use injectable clock/timer seams for tests"). Every daemon
3
- * module that schedules a TTL/grace/pre-claim/keepalive timer takes a {@link TimerFns} so tests
4
- * can substitute an implementation that fires deterministically instead of waiting on real wall
5
- * time, and so every timer this package creates can be traced back to a single seam for cleanup.
6
- */
7
- export const systemTimers = {
8
- setTimeout: (callback, ms) => setTimeout(callback, ms),
9
- clearTimeout: (handle) => clearTimeout(handle),
10
- setInterval: (callback, ms) => setInterval(callback, ms),
11
- clearInterval: (handle) => clearInterval(handle),
12
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"tls.d.ts","sourceRoot":"","sources":["../../src/daemon/tls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;0CAE0C;AAC1C,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAElD;6EAC6E;AAC7E,eAAO,MAAM,cAAc,GAAU,SAAS,MAAM,KAAG,OAAO,CAAC,MAAM,CAWpE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GAAU,SAAS,MAAM,EAAE,OAAO,SAAS,KAAG,OAAO,CAAC,MAAM,CAiBhG,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,iBAAiB,CAAC;CACtC,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,sDAAsD;IACtD,OAAO,EAAE,MAAM,WAAW,CAAC;IAC3B,2GAA2G;IAC3G,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/D,4GAA4G;IAC5G,UAAU,EAAE,MAAM,MAAM,EAAE,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,iBAAiB,CAAC;IACvC,yFAAyF;IACzF,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,SAAS,uBAAuB,KAAG,OAAO,CAAC,UAAU,CAuB3F,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,iBAAiB,EAAE,MAAM,MAAM,KAAG,aAE1E,CAAC"}
@@ -1,79 +0,0 @@
1
- /**
2
- * Host key loading and leaf-certificate minting (ARCHITECTURE.md §3, §8). Reuses
3
- * `host-certificate.ts` for the actual X.509 generation; this module
4
- * owns key-file hygiene (refusing over-permissive key files) and re-minting the leaf when
5
- * the advertised address changes so the certificate's SAN stays valid.
6
- *
7
- * The SPKI pin is derived from the key's public key alone (`spki-pin.ts`), so it never changes
8
- * across re-mints — only the certificate's SAN entries do.
9
- *
10
- * Opt-in hardening, dev-mode zero-config bootstrap: a *missing* key file is no longer fatal.
11
- * `createTlsManager` auto-generates one (same material/hygiene as `appduct keygen`, via
12
- * `key-material.ts`) so a first-time `appduct daemon` needs no manual keygen step, and reports
13
- * the new key's SPKI fingerprint through the optional `warn` callback. An existing key with bad
14
- * permissions is still refused outright — auto-generation only fills the *missing* case, it never
15
- * silently replaces a key that's already there.
16
- */
17
- import { stat, readFile } from "node:fs/promises";
18
- import { generateHostCertificate } from "../host-certificate.js";
19
- import { generatePrivateKeyPem, writePrivateKeyAtomically } from "../key-material.js";
20
- import { getSpkiPinFromPrivateKeyPem } from "../spki-pin.js";
21
- export class HostKeyError extends Error {
22
- constructor(message) {
23
- super(message);
24
- this.name = "HostKeyError";
25
- }
26
- }
27
- /** Refuses a key file that is group/world-readable. Missing files are handled by the caller
28
- * (`loadOrGenerateHostKeyPem`), which auto-generates instead of throwing. */
29
- export const loadHostKeyPem = async (keyPath) => {
30
- const fileStat = await stat(keyPath);
31
- if ((fileStat.mode & 0o077) !== 0) {
32
- throw new HostKeyError(`Appduct host key at "${keyPath}" is group- or world-readable (mode ${(fileStat.mode & 0o777).toString(8)}). ` +
33
- `Refusing to load it — run "chmod 600 ${keyPath}" or re-run "appduct keygen".`);
34
- }
35
- return readFile(keyPath, "utf8");
36
- };
37
- /**
38
- * Loads the host key at `keyPath`, generating one in its place if none exists yet (dev-mode
39
- * zero-config bootstrap). Only the ENOENT case auto-generates: any other `stat`/`readFile` failure
40
- * (including bad permissions) still propagates as before, so a deliberately hardened/misconfigured
41
- * key file is never silently overwritten.
42
- */
43
- export const loadOrGenerateHostKeyPem = async (keyPath, warn) => {
44
- try {
45
- return await loadHostKeyPem(keyPath);
46
- }
47
- catch (error) {
48
- if (error.code !== "ENOENT") {
49
- throw error;
50
- }
51
- }
52
- const keyPem = generatePrivateKeyPem();
53
- await writePrivateKeyAtomically(keyPath, keyPem);
54
- warn?.(`Appduct: no host key found at "${keyPath}" — generated one (dev mode). ` +
55
- `SPKI pin: ${getSpkiPinFromPrivateKeyPem(keyPem)}`);
56
- return keyPem;
57
- };
58
- export const createTlsManager = async (options) => {
59
- const keyPem = await loadOrGenerateHostKeyPem(options.keyPath, options.warn);
60
- const mint = async (address) => {
61
- const { certPem, spkiPin } = await generateHostCertificate(keyPem, address.address);
62
- return { certPem, keyPem, spkiPin, advertisedAddress: address };
63
- };
64
- let material = await mint(options.detectAddress());
65
- return {
66
- current: () => material,
67
- refresh: async (address) => {
68
- const next = address ?? options.detectAddress();
69
- if (next.address !== material.advertisedAddress.address || next.family !== material.advertisedAddress.family) {
70
- material = await mint(next);
71
- }
72
- return material;
73
- },
74
- pinnedKeys: () => [material.spkiPin],
75
- };
76
- };
77
- export const toAgentEndpoint = (address, port) => {
78
- return { family: address.family, address: address.address, port };
79
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA4CpE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;gBAEf,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAMnE;AAED,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAE/D,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAEpE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAEpE,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAEjE,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAE9D,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAEpE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAElE,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAEpE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,KAAG,eAEnE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,eAE3D,CAAC;AAUF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,QAqD3C,CAAC;AAEF;oGACoG;AACpG,eAAO,MAAM,mBAAmB,GAAI,OAAO,OAAO,KAAG,MAmBpD,CAAC"}
package/dist/errors.js DELETED
@@ -1,162 +0,0 @@
1
- import { DaemonRpcError, DaemonVersionMismatchError, isDaemonUnreachableError } from "./rpc/client.js";
2
- const EXIT_CODE_BY_ERROR_TYPE = {
3
- usage_error: 64,
4
- validation_error: 65,
5
- // EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
6
- // missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
7
- // code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
8
- // assertion failure (docs/tasks/08-appduct-doctor.md).
9
- inspection_error: 66,
10
- connection_error: 70,
11
- session_error: 71,
12
- tool_error: 72,
13
- permission_error: 77,
14
- internal_error: 1,
15
- // `doctor --assert-present`/`--assert-absent`: the artifact was inspected successfully and the
16
- // assertion did not hold. Distinct from `inspection_error` (66) — this means the answer is known
17
- // and it's "no", not "couldn't tell" — and distinct from `internal_error` (1) so a caller can't
18
- // conflate a normal assertion failure with an unexpected crash.
19
- assertion_error: 3,
20
- };
21
- /**
22
- * Maps the daemon's wire `error.data.type` (ARCHITECTURE.md §5) onto a sysexits class for exit-code
23
- * purposes only — the CLI error's rendered `type` field still preserves the wire type verbatim
24
- * (see {@link toCliError}); this table never leaks into rendered output.
25
- */
26
- const RPC_ERROR_EXIT_CLASS = {
27
- no_session: "session_error",
28
- ambiguous_session: "session_error",
29
- unknown_session: "session_error",
30
- session_not_active: "session_error",
31
- session_suspended: "session_error",
32
- tool_not_found: "tool_error",
33
- tool_input_validation_error: "tool_error",
34
- tool_output_validation_error: "tool_error",
35
- tool_execution_error: "tool_error",
36
- tool_serialization_error: "tool_error",
37
- tool_timeout: "tool_error",
38
- tool_cancelled: "tool_error",
39
- policy_denied: "permission_error",
40
- invalid_request: "validation_error",
41
- };
42
- export class AppductCliError extends Error {
43
- type;
44
- details;
45
- constructor(type, message, details) {
46
- super(message);
47
- this.name = "AppductCliError";
48
- this.type = type;
49
- this.details = details;
50
- }
51
- }
52
- export const usageError = (message, details) => {
53
- return new AppductCliError("usage_error", message, details);
54
- };
55
- export const validationError = (message, details) => {
56
- return new AppductCliError("validation_error", message, details);
57
- };
58
- export const connectionError = (message, details) => {
59
- return new AppductCliError("connection_error", message, details);
60
- };
61
- export const sessionError = (message, details) => {
62
- return new AppductCliError("session_error", message, details);
63
- };
64
- export const toolError = (message, details) => {
65
- return new AppductCliError("tool_error", message, details);
66
- };
67
- export const permissionError = (message, details) => {
68
- return new AppductCliError("permission_error", message, details);
69
- };
70
- export const internalError = (message, details) => {
71
- return new AppductCliError("internal_error", message, details);
72
- };
73
- export const inspectionError = (message, details) => {
74
- return new AppductCliError("inspection_error", message, details);
75
- };
76
- export const assertionError = (message, details) => {
77
- return new AppductCliError("assertion_error", message, details);
78
- };
79
- export const isAppductCliError = (value) => {
80
- return value instanceof AppductCliError;
81
- };
82
- /** True for a `DaemonRpcError` whose `data.type` is a known wire `ErrorType` (always the case for
83
- * `RpcApplicationError`-originated failures; JSON-RPC transport-level errors like "Method not
84
- * found" carry no `data` and fall through to `internal_error`). */
85
- const rpcErrorClass = (error) => {
86
- const type = error.data?.type;
87
- return type ? RPC_ERROR_EXIT_CLASS[type] : undefined;
88
- };
89
- /**
90
- * Converts any error thrown from a command handler into the CLI's rendered `CliError` shape.
91
- * RPC/app-originated failures (`DaemonRpcError` with a preserved `data.type`) keep that type
92
- * **verbatim** in `type` — never re-wrapped under a generic bucket (ARCHITECTURE.md §5) — so e.g.
93
- * `invoke --json` on a failing tool call renders `error.type === "tool_execution_error"`, not
94
- * `"tool_error"`. `getExitCodeForError` still buckets that same error into a sysexits class.
95
- */
96
- export const toCliError = (error) => {
97
- if (isAppductCliError(error)) {
98
- return {
99
- type: error.type,
100
- message: error.message,
101
- details: error.details,
102
- };
103
- }
104
- // Daemon/CLI version drift that could not be resolved automatically (issue #30). Rendered as a
105
- // `connection_error` — the daemon is reachable, but not one this client can safely talk to —
106
- // with both versions and the live-session count in `details` so `--json` consumers can act on it
107
- // without parsing the message.
108
- if (error instanceof DaemonVersionMismatchError) {
109
- return {
110
- type: "connection_error",
111
- message: error.message,
112
- details: {
113
- daemon_version: error.daemonVersion,
114
- client_version: error.clientVersion,
115
- session_count: error.sessionCount,
116
- pending_link_count: error.pendingLinkCount,
117
- },
118
- };
119
- }
120
- if (error instanceof DaemonRpcError && error.data?.type) {
121
- return {
122
- type: error.data.type,
123
- message: error.message,
124
- details: error.data.details,
125
- };
126
- }
127
- if (isDaemonUnreachableError(error)) {
128
- return {
129
- type: "connection_error",
130
- message: error instanceof Error ? error.message : "The Appduct daemon is unreachable.",
131
- };
132
- }
133
- if (error instanceof Error) {
134
- return {
135
- type: "internal_error",
136
- message: error.message,
137
- };
138
- }
139
- return {
140
- type: "internal_error",
141
- message: "An unexpected error occurred.",
142
- details: error,
143
- };
144
- };
145
- /** Computes the process exit code for the *original* thrown error (not the rendered `CliError`) so
146
- * that the sysexits classification never depends on — or leaks into — the rendered `type` field. */
147
- export const getExitCodeForError = (error) => {
148
- if (isAppductCliError(error)) {
149
- return EXIT_CODE_BY_ERROR_TYPE[error.type];
150
- }
151
- if (error instanceof DaemonVersionMismatchError) {
152
- return EXIT_CODE_BY_ERROR_TYPE.connection_error;
153
- }
154
- if (error instanceof DaemonRpcError) {
155
- const cliErrorClass = rpcErrorClass(error);
156
- return EXIT_CODE_BY_ERROR_TYPE[cliErrorClass ?? "internal_error"];
157
- }
158
- if (isDaemonUnreachableError(error)) {
159
- return EXIT_CODE_BY_ERROR_TYPE.connection_error;
160
- }
161
- return EXIT_CODE_BY_ERROR_TYPE.internal_error;
162
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"host-certificate.d.ts","sourceRoot":"","sources":["../src/host-certificate.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAyB1B,KAAK,wBAAwB,GAAG;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAwHF,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,EACd,cAAc,MAAM,KACnB,OAAO,CAAC,wBAAwB,CAyClC,CAAC"}
@@ -1,111 +0,0 @@
1
- import "reflect-metadata";
2
- import { createPrivateKey, createPublicKey, randomBytes, webcrypto, } from "node:crypto";
3
- import { BasicConstraintsExtension, DNS, ExtendedKeyUsage, ExtendedKeyUsageExtension, IP, KeyUsageFlags, KeyUsagesExtension, SubjectAlternativeNameExtension, SubjectKeyIdentifierExtension, X509CertificateGenerator, } from "@peculiar/x509";
4
- import { getSpkiPinFromPrivateKeyPem } from "./spki-pin.js";
5
- const CERT_VALIDITY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
6
- const CERT_CLOCK_SKEW_MS = 5 * 60 * 1000;
7
- const toArrayBuffer = (view) => {
8
- return Uint8Array.from(view).buffer;
9
- };
10
- const decodeBase64Url = (value) => {
11
- return Uint8Array.from(Buffer.from(value, "base64url"));
12
- };
13
- const resolveEcHash = (curve) => {
14
- switch (curve) {
15
- case "P-256":
16
- return "SHA-256";
17
- case "P-384":
18
- return "SHA-384";
19
- case "P-521":
20
- return "SHA-512";
21
- default:
22
- throw new Error(`Unsupported EC named curve "${curve}" for Appduct host certificates.`);
23
- }
24
- };
25
- const importSigningKeyPair = async (keyPem) => {
26
- const privateKeyObject = createPrivateKey(keyPem);
27
- const publicKeyObject = createPublicKey(privateKeyObject);
28
- const privateKeyDer = Buffer.from(privateKeyObject.export({ format: "der", type: "pkcs8" }));
29
- const publicKeyDer = Buffer.from(publicKeyObject.export({ format: "der", type: "spki" }));
30
- const jwk = privateKeyObject.export({ format: "jwk" });
31
- if (jwk.kty === "RSA") {
32
- const modulusLength = privateKeyObject.asymmetricKeyDetails?.modulusLength;
33
- if (typeof modulusLength !== "number" || typeof jwk.e !== "string") {
34
- throw new Error("Appduct could not determine RSA key details for certificate generation.");
35
- }
36
- const publicExponent = decodeBase64Url(jwk.e);
37
- const importAlgorithm = {
38
- name: "RSASSA-PKCS1-v1_5",
39
- hash: "SHA-256",
40
- modulusLength,
41
- publicExponent,
42
- };
43
- return {
44
- keyType: "rsa",
45
- privateKey: await webcrypto.subtle.importKey("pkcs8", toArrayBuffer(privateKeyDer), importAlgorithm, false, ["sign"]),
46
- publicKey: await webcrypto.subtle.importKey("spki", toArrayBuffer(publicKeyDer), importAlgorithm, true, ["verify"]),
47
- signingAlgorithm: {
48
- name: "RSASSA-PKCS1-v1_5",
49
- hash: "SHA-256",
50
- },
51
- };
52
- }
53
- if (jwk.kty === "EC") {
54
- const namedCurve = jwk.crv;
55
- if (typeof namedCurve !== "string") {
56
- throw new Error("Appduct could not determine the EC named curve for certificate generation.");
57
- }
58
- const importAlgorithm = {
59
- name: "ECDSA",
60
- namedCurve,
61
- };
62
- const signingAlgorithm = {
63
- ...importAlgorithm,
64
- hash: resolveEcHash(namedCurve),
65
- };
66
- return {
67
- keyType: "ec",
68
- privateKey: await webcrypto.subtle.importKey("pkcs8", toArrayBuffer(privateKeyDer), importAlgorithm, false, ["sign"]),
69
- publicKey: await webcrypto.subtle.importKey("spki", toArrayBuffer(publicKeyDer), importAlgorithm, true, ["verify"]),
70
- signingAlgorithm: {
71
- name: "ECDSA",
72
- hash: resolveEcHash(namedCurve),
73
- },
74
- };
75
- }
76
- throw new Error(`Appduct does not support "${jwk.kty ?? privateKeyObject.asymmetricKeyType}" host keys.`);
77
- };
78
- export const generateHostCertificate = async (keyPem, advertisedIp) => {
79
- const keys = await importSigningKeyPair(keyPem);
80
- const sanEntries = [
81
- { type: DNS, value: "localhost" },
82
- { type: IP, value: "127.0.0.1" },
83
- ];
84
- if (advertisedIp !== "127.0.0.1") {
85
- sanEntries.push({ type: IP, value: advertisedIp });
86
- }
87
- const keyUsage = KeyUsageFlags.digitalSignature |
88
- (keys.keyType === "rsa" ? KeyUsageFlags.keyEncipherment : KeyUsageFlags.keyAgreement);
89
- const certificate = await X509CertificateGenerator.createSelfSigned({
90
- name: "CN=Appduct Host",
91
- notBefore: new Date(Date.now() - CERT_CLOCK_SKEW_MS),
92
- notAfter: new Date(Date.now() + CERT_VALIDITY_WINDOW_MS),
93
- signingAlgorithm: keys.signingAlgorithm,
94
- keys: {
95
- privateKey: keys.privateKey,
96
- publicKey: keys.publicKey,
97
- },
98
- extensions: [
99
- new BasicConstraintsExtension(false, undefined, true),
100
- new KeyUsagesExtension(keyUsage, true),
101
- new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], false),
102
- new SubjectAlternativeNameExtension(sanEntries, false),
103
- await SubjectKeyIdentifierExtension.create(keys.publicKey),
104
- ],
105
- });
106
- const certPem = certificate.toString("pem");
107
- return {
108
- certPem,
109
- spkiPin: getSpkiPinFromPrivateKeyPem(keyPem),
110
- };
111
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"key-material.d.ts","sourceRoot":"","sources":["../src/key-material.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,eAAO,MAAM,qBAAqB,QAAO,MAYxC,CAAC;AAEF;;;+DAG+D;AAC/D,eAAO,MAAM,yBAAyB,GAAU,MAAM,MAAM,EAAE,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAgB1F,CAAC"}
@@ -1,42 +0,0 @@
1
- /**
2
- * Host private-key generation and atomic persistence (ARCHITECTURE.md §3, §10). Shared by
3
- * `appduct keygen` (explicit, user-invoked) and the daemon's zero-config dev-mode
4
- * auto-generation at startup (`daemon/tls.ts`) — both call sites need byte-identical key material
5
- * (RSA-2048, PKCS8 PEM) and identical file hygiene (0600, atomic rename via a `wx`-flagged temp
6
- * file so a concurrent writer can never observe a partial key).
7
- */
8
- import { generateKeyPairSync } from "node:crypto";
9
- import { dirname } from "node:path";
10
- import { mkdir, rename, rm, writeFile } from "node:fs/promises";
11
- export const generatePrivateKeyPem = () => {
12
- const { privateKey } = generateKeyPairSync("rsa", {
13
- modulusLength: 2048,
14
- publicExponent: 0x10001,
15
- });
16
- return privateKey
17
- .export({
18
- format: "pem",
19
- type: "pkcs8",
20
- })
21
- .toString("utf8");
22
- };
23
- /** Writes `keyPem` at `path` atomically (temp file + rename) with mode 0600. The temp file uses
24
- * `wx` so two concurrent writers can never interleave; callers that want to overwrite an existing
25
- * `path` still succeed, since `rename` replaces the destination on POSIX regardless of whether it
26
- * already existed — only the *temp* path must be exclusive. */
27
- export const writePrivateKeyAtomically = async (path, keyPem) => {
28
- const temporaryPath = `${path}.${process.pid}.${Date.now()}.tmp`;
29
- await mkdir(dirname(path), { recursive: true });
30
- try {
31
- await writeFile(temporaryPath, keyPem, {
32
- encoding: "utf8",
33
- mode: 0o600,
34
- flag: "wx",
35
- });
36
- await rename(temporaryPath, path);
37
- }
38
- catch (error) {
39
- await rm(temporaryPath, { force: true }).catch(() => { });
40
- throw error;
41
- }
42
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../src/link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAe,KAAK,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEzF,OAAO,EAUL,KAAK,MAAM,EACX,KAAK,UAAU,EAChB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAc,KAAK,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAU3D;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,EAAE,QAAQ,gBAAgB,KAAG,MACc,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;yCACqC;IACrC,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;0FACsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;mDAC+C;IAC/C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;sCACkC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;2GAEuG;IACvG,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;qFACiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,aAAa,CAAC;IACxB,oBAAoB;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB,CAAC;AAEF;;wFAEwF;AACxF,eAAO,MAAM,QAAQ,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,cAAc,CA8F/E,CAAC"}
package/dist/link.js DELETED
@@ -1,117 +0,0 @@
1
- /**
2
- * Shared core of `link.create` + deep-link composition + optional emulator/simulator delivery
3
- * (ARCHITECTURE.md §8, §10): mints a pending session via `link.create`, then composes
4
- * `<scheme>:///?appduct=<payload>&pin=<spki-pin>` and optionally delivers it to a booted
5
- * Android emulator/device or iOS simulator. Used by `commands/link.ts` (`appduct link`) and
6
- * `client/bootstrap.ts` (`appduct/client`'s `link()`) so this shape — scheme resolution, the
7
- * `pin` query param, the `127.0.0.1` emulator/simulator address override (which the experimental
8
- * `ios-device` target deliberately opts out of) — can't drift between the CLI and the programmatic
9
- * client.
10
- *
11
- * The `pin` param is separate from the `appduct` bootstrap blob (opt-in hardening dev-mode) — it
12
- * is never part of that binary v2 payload, so old apps that don't know about it simply ignore it.
13
- * It carries the daemon's SPKI pin (same value as `daemon.status`'s `pinnedKeys[0]` / `appduct
14
- * keygen`'s output) so a debug build with no build-time `cliPins` configured can trust it for this
15
- * one connection instead of requiring a native rebuild just to test locally. The pin is standard
16
- * (non-URL-safe) base64 (`sha256/<44-char-base64>`, may contain `+`/`/`/`=`), so it is percent-
17
- * encoded here; native/JS parsers use `URLSearchParams`, which decodes it back.
18
- */
19
- import { RPC_METHODS } from "@appduct/shared";
20
- import { deliverToOpenTarget, isOpenTarget, isValidBundleId, invalidBundleIdMessage, isLoopbackAddress, loopbackAddressMessage, usesLoopbackAddress, MISSING_BUNDLE_ID_MESSAGE, OPEN_TARGETS, } from "./cli/open-target.js";
21
- import { loadConfig } from "./daemon/config.js";
22
- import { getStateDirPaths } from "./daemon/state-dir.js";
23
- import { usageError } from "./errors.js";
24
- import { callDaemon } from "./rpc/client.js";
25
- import { resolveSchemeOrThrow } from "./scheme.js";
26
- /** The emulator/simulator fast path forces `127.0.0.1`: the daemon's wss listener already binds
27
- * all interfaces, and both delivery mechanisms (adb reverse, the iOS simulator's shared host
28
- * network) make the daemon reachable there regardless of the machine's real LAN address. A
29
- * physical iOS device (`ios-device`) has no such tunnel, so it keeps the detected LAN address —
30
- * see `usesLoopbackAddress`. */
31
- const OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
32
- /**
33
- * The deep-link shape, in one place: `<scheme>:///?appduct=<payload>&pin=<spki-pin>`. Exported
34
- * so `mcp/connect-tool.ts` composes byte-identical links rather than a second, drifting copy — the
35
- * `pin` param went missing from the MCP side exactly because there were two of these.
36
- *
37
- * The pin is standard (non-URL-safe) base64 (`sha256/<44 chars>`, possibly containing `+`/`/`/`=`),
38
- * so it is percent-encoded; parsers use `URLSearchParams`, which decodes it back. It is appended
39
- * *after* the bootstrap blob, so anything reading `appduct=` must stop at the `&`.
40
- */
41
- export const composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
42
- /** Throws `usageError`/`AppductCliError` on validation failure — `commands/link.ts` renders
43
- * that straight through the CLI's own error path; `client/bootstrap.ts` converts it to a
44
- * `AppductError` via `toAppductError` (which already understands `AppductCliError`). */
45
- export const mintLink = async (options) => {
46
- if (options.target !== undefined && !isOpenTarget(options.target)) {
47
- throw usageError(`"target" must be one of ${OPEN_TARGETS.map((target) => `"${target}"`).join(", ")} (got "${options.target}").`);
48
- }
49
- if (options.device !== undefined && options.target === undefined) {
50
- throw usageError('"device" only applies alongside a target.');
51
- }
52
- if (options.bundleId !== undefined && options.target !== "ios-device") {
53
- throw usageError('"bundleId" only applies alongside target "ios-device".');
54
- }
55
- if (options.relaunch !== undefined && options.target !== "ios-device") {
56
- throw usageError('"relaunch" only applies alongside target "ios-device".');
57
- }
58
- const paths = getStateDirPaths(options.stateDir);
59
- const config = await loadConfig(paths);
60
- const scheme = await resolveSchemeOrThrow({
61
- flagScheme: options.scheme,
62
- env: options.schemeEnv,
63
- cwd: options.cwd,
64
- configScheme: config.scheme,
65
- stateConfigPath: paths.configPath,
66
- stateDirRoot: paths.root,
67
- });
68
- // `ios-device` needs a bundle id `devicectl` can launch; resolved (and required) *before* the
69
- // link is minted so a missing one is a plain usage error rather than a stranded pending session.
70
- const bundleId = options.target === "ios-device" ? (options.bundleId ?? config.iosBundleId) : undefined;
71
- if (options.target === "ios-device" && !bundleId) {
72
- throw usageError(MISSING_BUNDLE_ID_MESSAGE);
73
- }
74
- if (bundleId !== undefined && !isValidBundleId(bundleId)) {
75
- throw usageError(invalidBundleIdMessage(bundleId));
76
- }
77
- const result = await callDaemon(RPC_METHODS.linkCreate, {
78
- ttlSeconds: options.ttlSeconds,
79
- addressOverride: options.target && usesLoopbackAddress(options.target) ? OPEN_TARGET_ADDRESS_OVERRIDE : undefined,
80
- }, { stateDir: options.stateDir, spawn: options.spawn, autoSpawn: options.autoSpawn });
81
- // A physical device is the one target that cannot reach a loopback address, and
82
- // `detectAdvertisedAddress` falls back to `127.0.0.1` when it finds no routable interface. Left
83
- // unchecked, the link would be delivered happily and then never claimed — a silent hang in
84
- // `wait_for_session` with nothing pointing at the cause.
85
- if (options.target === "ios-device" && isLoopbackAddress(result.endpoint.address)) {
86
- throw usageError(loopbackAddressMessage(result.endpoint.address));
87
- }
88
- const deepLink = composeDeepLink(scheme, result);
89
- if (options.target) {
90
- await deliverToOpenTarget({
91
- target: options.target,
92
- deepLink,
93
- wssPort: result.endpoint.port,
94
- device: options.device,
95
- bundleId,
96
- relaunch: options.relaunch,
97
- exec: options.exec,
98
- env: options.env,
99
- });
100
- return {
101
- sessionId: result.sessionId,
102
- deepLink,
103
- endpoint: result.endpoint,
104
- expiresAt: result.expiresAt,
105
- pin: result.pin,
106
- delivered: true,
107
- target: options.target,
108
- };
109
- }
110
- return {
111
- sessionId: result.sessionId,
112
- deepLink,
113
- endpoint: result.endpoint,
114
- expiresAt: result.expiresAt,
115
- pin: result.pin,
116
- };
117
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"connect-tool.d.ts","sourceRoot":"","sources":["../../src/mcp/connect-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,EAUL,KAAK,MAAM,EACX,KAAK,UAAU,EAChB,MAAM,uBAAuB,CAAC;AAI/B,OAAO,EAKL,KAAK,OAAO,EACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,eAAO,MAAM,iBAAiB,oBAAoB,CAAC;AACnD,eAAO,MAAM,0BAA0B,6BAA6B,CAAC;AA6BrE,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;CA2B1B,CAAC;AAEX,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;CAiBnC,CAAC;AAEX;;mGAEmG;AACnG,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,cAAc,GAAG,sBAAsB;gBAAjE,IAAI,EAAE,iBAAiB,GAAG,cAAc,GAAG,sBAAsB,EAC1E,OAAO,EAAE,MAAM;CAKlB;AA6DD,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB;;0EAEsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;qFACiF;IACjF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;sEACkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,YAAY,CAAC,EAAE,IAAI,CAAC;IACpB;kDAC8C;IAC9C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAsEF,eAAO,MAAM,iBAAiB,GAC5B,SAAS,OAAO,EAChB,MAAM,eAAe,KACpB,OAAO,CAAC,iBAAiB,CA4I3B,CAAC;AA6BF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,OAAO,EAChB,MAAM,sBAAsB,KAC3B,OAAO,CAAC,wBAAwB,CAiHlC,CAAC"}