@astrale-os/cli 1.0.0-beta.3 → 1.0.0-beta.31

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 (345) hide show
  1. package/README.md +84 -6
  2. package/dist/astrale.js +57586 -60753
  3. package/dist/public/connect-core.js +2158 -30706
  4. package/dist/public/keys/index.js +1016 -29541
  5. package/dist/public/paths/index.js +370 -28654
  6. package/dist/types/admin/contract.d.ts +26 -0
  7. package/dist/types/admin/instance/client.d.ts +20 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +6 -10
  10. package/dist/types/connection/auth.d.ts +3 -0
  11. package/dist/types/connection/call.d.ts +3 -0
  12. package/dist/types/connection/command.d.ts +27 -0
  13. package/dist/types/connection/credential.d.ts +14 -0
  14. package/dist/types/connection/errors.d.ts +1 -0
  15. package/dist/types/connection/exchange.d.ts +9 -0
  16. package/dist/types/connection/failure/classify.d.ts +2 -0
  17. package/dist/types/connection/failure/index.d.ts +5 -0
  18. package/dist/types/connection/failure/model.d.ts +25 -0
  19. package/dist/types/connection/failure/render.d.ts +3 -0
  20. package/dist/types/connection/index.d.ts +8 -0
  21. package/dist/types/connection/reasons.d.ts +37 -0
  22. package/dist/types/connection/self.d.ts +37 -0
  23. package/dist/types/connection/session.d.ts +31 -0
  24. package/dist/types/connection/target.d.ts +38 -0
  25. package/dist/types/errors.d.ts +1 -1
  26. package/dist/types/lib/admin-instance.d.ts +17 -0
  27. package/dist/types/lib/admin-target.d.ts +11 -36
  28. package/dist/types/lib/config.d.ts +13 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +25 -173
  31. package/dist/types/lib/instance-target.d.ts +1 -1
  32. package/dist/types/lib/instance.d.ts +30 -88
  33. package/dist/types/lib/log.d.ts +5 -3
  34. package/dist/types/lib/output.d.ts +1 -1
  35. package/dist/types/lib/proc.d.ts +43 -0
  36. package/dist/types/lib/self.d.ts +9 -0
  37. package/dist/types/lib/update.d.ts +109 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/dist/types/state/exchange-credentials.d.ts +6 -2
  40. package/dist/types/state/files.d.ts +2 -0
  41. package/dist/types/state/index.d.ts +3 -2
  42. package/dist/types/state/paths.d.ts +2 -0
  43. package/dist/types/state/session-routes.d.ts +10 -0
  44. package/package.json +16 -19
  45. package/src/__tests__/fixtures/publication.ts +20 -0
  46. package/src/admin/.spec/architecture.md +12 -5
  47. package/src/admin/__tests__/fixture.ts +25 -0
  48. package/src/admin/catalog/.spec/api.d.ts +1 -3
  49. package/src/admin/catalog/.spec/architecture.md +5 -4
  50. package/src/admin/catalog/__tests__/client.test.ts +143 -69
  51. package/src/admin/catalog/client.ts +40 -54
  52. package/src/admin/catalog/model.ts +3 -4
  53. package/src/admin/contract.ts +47 -0
  54. package/src/admin/graph/.spec/api.d.ts +4 -10
  55. package/src/admin/graph/nodes.ts +1 -1
  56. package/src/admin/instance/.spec/api.d.ts +5 -9
  57. package/src/admin/instance/.spec/architecture.md +7 -7
  58. package/src/admin/instance/__tests__/client.test.ts +157 -175
  59. package/src/admin/instance/client.ts +39 -128
  60. package/src/admin/instance/index.ts +1 -2
  61. package/src/admin/instance/model.ts +12 -18
  62. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  63. package/src/commands/__tests__/call-describe.test.ts +46 -33
  64. package/src/commands/__tests__/call.test.ts +34 -0
  65. package/src/commands/__tests__/domain-install-operation.test.ts +30 -22
  66. package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
  67. package/src/commands/__tests__/domain-list.test.ts +5 -25
  68. package/src/commands/__tests__/install-direct.test.ts +115 -8
  69. package/src/commands/__tests__/install-identity-override.test.ts +6 -17
  70. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  71. package/src/commands/__tests__/instance-status.test.ts +139 -0
  72. package/src/commands/__tests__/introspect-parse.test.ts +80 -1
  73. package/src/commands/__tests__/logs.test.ts +250 -2
  74. package/src/commands/__tests__/read-commands.test.ts +119 -11
  75. package/src/commands/__tests__/token-ttl.test.ts +49 -4
  76. package/src/commands/__tests__/update.test.ts +25 -3
  77. package/src/commands/__tests__/view.test.ts +50 -2
  78. package/src/commands/auth/logout.ts +2 -1
  79. package/src/commands/browser.ts +29 -0
  80. package/src/commands/call-describe.ts +60 -68
  81. package/src/commands/call.ts +71 -59
  82. package/src/commands/domain/install.ts +73 -46
  83. package/src/commands/domain/list.ts +1 -1
  84. package/src/commands/domain/publish.ts +14 -11
  85. package/src/commands/domain/uninstall.ts +2 -2
  86. package/src/commands/get.ts +15 -16
  87. package/src/commands/identity/__tests__/register.test.ts +3 -3
  88. package/src/commands/identity/register.ts +8 -8
  89. package/src/commands/instance/create.ts +8 -16
  90. package/src/commands/instance/delete.ts +12 -9
  91. package/src/commands/instance/list.ts +2 -2
  92. package/src/commands/instance/status.ts +66 -20
  93. package/src/commands/instance/use.ts +16 -12
  94. package/src/commands/introspect.ts +53 -41
  95. package/src/commands/logs.ts +123 -36
  96. package/src/commands/mutate.ts +6 -5
  97. package/src/commands/query.ts +39 -36
  98. package/src/commands/session/analyze.ts +26 -4
  99. package/src/commands/token.ts +42 -27
  100. package/src/commands/ui/__tests__/commands.test.ts +152 -0
  101. package/src/commands/ui/add.ts +51 -0
  102. package/src/commands/ui/doctor.ts +21 -0
  103. package/src/commands/ui/init.ts +38 -0
  104. package/src/commands/ui/list.ts +26 -0
  105. package/src/commands/ui/preset-apply.ts +19 -0
  106. package/src/commands/ui/preset-list.ts +12 -0
  107. package/src/commands/ui/shared.ts +25 -0
  108. package/src/commands/update.ts +90 -48
  109. package/src/commands/view.ts +82 -37
  110. package/src/connection/.spec/api.d.ts +7 -8
  111. package/src/connection/.spec/architecture.md +14 -5
  112. package/src/connection/.spec/flows/session.ts +2 -2
  113. package/src/connection/.spec/laws/connection.ts +6 -2
  114. package/src/connection/.spec/layout.ts +1 -0
  115. package/src/connection/__tests__/auth.test.ts +1 -0
  116. package/src/connection/__tests__/credential.test.ts +21 -1
  117. package/src/connection/__tests__/errors.test.ts +27 -20
  118. package/src/connection/__tests__/exchange.test.ts +64 -11
  119. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  120. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  121. package/src/connection/__tests__/self.test.ts +31 -11
  122. package/src/connection/__tests__/session.test.ts +8 -4
  123. package/src/connection/__tests__/target.test.ts +2 -0
  124. package/src/connection/auth.ts +13 -0
  125. package/src/connection/call.ts +1 -1
  126. package/src/connection/credential.ts +2 -2
  127. package/src/connection/errors.ts +1 -355
  128. package/src/connection/exchange.ts +110 -61
  129. package/src/connection/failure/classify.ts +110 -0
  130. package/src/connection/failure/index.ts +18 -0
  131. package/src/connection/failure/model.ts +28 -0
  132. package/src/connection/failure/render.ts +116 -0
  133. package/src/connection/reasons.ts +16 -0
  134. package/src/connection/self.ts +39 -21
  135. package/src/connection/session.ts +27 -9
  136. package/src/errors.ts +3 -2
  137. package/src/graph/.spec/api.d.ts +13 -6
  138. package/src/graph/.spec/laws/documents.ts +7 -7
  139. package/src/graph/.spec/layout.ts +9 -1
  140. package/src/graph/__tests__/mutation.test.ts +2 -2
  141. package/src/graph/__tests__/query.test.ts +35 -9
  142. package/src/graph/class.ts +30 -0
  143. package/src/graph/index.ts +1 -0
  144. package/src/graph/query.ts +27 -40
  145. package/src/identity/.spec/api.d.ts +2 -2
  146. package/src/identity/__tests__/fixtures/registry-journey.ts +13 -1
  147. package/src/identity/__tests__/registration.test.ts +1 -1
  148. package/src/identity/__tests__/registry.test.ts +4 -0
  149. package/src/identity/registration.ts +3 -3
  150. package/src/identity/registry.ts +2 -0
  151. package/src/keys/__tests__/keys.test.ts +4 -0
  152. package/src/keys/credential.ts +1 -0
  153. package/src/lib/__tests__/admin-target.test.ts +12 -0
  154. package/src/lib/__tests__/binary.test.ts +16 -1
  155. package/src/lib/__tests__/browser-retention.test.ts +212 -0
  156. package/src/lib/__tests__/command-dx.test.ts +1 -1
  157. package/src/lib/__tests__/config.test.ts +27 -0
  158. package/src/lib/__tests__/domain-publication.test.ts +10 -16
  159. package/src/lib/__tests__/idp.test.ts +34 -4
  160. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  161. package/src/lib/__tests__/instance-target.test.ts +7 -39
  162. package/src/lib/__tests__/log-errors.test.ts +64 -0
  163. package/src/lib/__tests__/skills.test.ts +834 -0
  164. package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
  165. package/src/lib/__tests__/update.test.ts +264 -11
  166. package/src/lib/__tests__/view-assets.test.ts +25 -0
  167. package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
  168. package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
  169. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  170. package/src/lib/__tests__/view-server.test.ts +26 -3
  171. package/src/lib/__tests__/view-session.test.ts +2 -1
  172. package/src/lib/admin-instance.ts +14 -8
  173. package/src/lib/admin-target.ts +21 -2
  174. package/src/lib/binary.ts +22 -5
  175. package/src/lib/browser-retention.ts +210 -0
  176. package/src/lib/command-dx.ts +9 -9
  177. package/src/lib/config.ts +12 -1
  178. package/src/lib/domain-publication.ts +9 -7
  179. package/src/lib/failure-debug.ts +26 -0
  180. package/src/lib/idp.ts +34 -3
  181. package/src/lib/instance-candidates.ts +2 -2
  182. package/src/lib/instance-target.ts +3 -14
  183. package/src/lib/instance.ts +16 -11
  184. package/src/lib/log.ts +16 -9
  185. package/src/lib/output.ts +1 -1
  186. package/src/lib/proc.ts +7 -2
  187. package/src/lib/provision-instance.ts +34 -57
  188. package/src/lib/skills/lock.ts +117 -0
  189. package/src/lib/skills/sync.ts +878 -0
  190. package/src/lib/skills.ts +1 -52
  191. package/src/lib/update.ts +298 -45
  192. package/src/lib/validation.ts +2 -2
  193. package/src/lib/view/assets.ts +11 -2
  194. package/src/lib/view/external-open-intent.ts +41 -0
  195. package/src/lib/view/external-open-origins.ts +37 -0
  196. package/src/lib/view/host-capabilities.ts +17 -0
  197. package/src/lib/view/resolve.ts +1 -1
  198. package/src/lib/view/server.ts +18 -3
  199. package/src/lib/view/session.ts +2 -0
  200. package/src/program/.spec/api.d.ts +1 -0
  201. package/src/program/__tests__/program.test.ts +201 -7
  202. package/src/program/argv.ts +14 -0
  203. package/src/program/build.ts +26 -4
  204. package/src/program/command.ts +1 -0
  205. package/src/program/index.ts +1 -0
  206. package/src/program/registry.ts +2 -1
  207. package/src/setup/__tests__/instance-step.test.ts +27 -10
  208. package/src/setup/steps/instance.ts +22 -15
  209. package/src/setup/steps/skills.ts +40 -38
  210. package/src/state/.spec/api.d.ts +21 -2
  211. package/src/state/.spec/architecture.md +18 -4
  212. package/src/state/.spec/layout.ts +1 -0
  213. package/src/state/__tests__/exchange-credentials.test.ts +88 -42
  214. package/src/state/__tests__/files.test.ts +24 -1
  215. package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
  216. package/src/state/__tests__/paths.test.ts +1 -0
  217. package/src/state/__tests__/session-routes.test.ts +138 -0
  218. package/src/state/exchange-credentials.ts +22 -9
  219. package/src/state/files.ts +41 -0
  220. package/src/state/index.ts +3 -1
  221. package/src/state/paths.ts +3 -0
  222. package/src/state/session-routes.ts +34 -0
  223. package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
  224. package/src/telemetry/__tests__/recorder.test.ts +1 -0
  225. package/src/telemetry/__tests__/retention.test.ts +267 -0
  226. package/src/telemetry/__tests__/settings.test.ts +102 -0
  227. package/src/telemetry/__tests__/store-scan.test.ts +128 -0
  228. package/src/telemetry/__tests__/trigger.test.ts +33 -4
  229. package/src/telemetry/analyze.ts +24 -2
  230. package/src/telemetry/recorder.ts +15 -4
  231. package/src/telemetry/retention.ts +176 -0
  232. package/src/telemetry/session.ts +18 -12
  233. package/src/telemetry/settings.ts +64 -11
  234. package/src/telemetry/store.ts +92 -9
  235. package/src/telemetry/trigger.ts +16 -28
  236. package/src/ui/.spec/api.d.ts +14 -0
  237. package/src/ui/.spec/architecture.md +14 -0
  238. package/src/ui/.spec/laws.ts +50 -0
  239. package/src/ui/.spec/layout.ts +16 -0
  240. package/src/ui/__tests__/ui.test.ts +1742 -0
  241. package/src/ui/index.ts +13 -0
  242. package/src/ui/lock.ts +89 -0
  243. package/src/ui/model.ts +83 -0
  244. package/src/ui/operations.ts +1044 -0
  245. package/src/ui/project.ts +333 -0
  246. package/src/ui/release.ts +398 -0
  247. package/src/ui/runner.ts +18 -0
  248. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  249. package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
  250. package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
  251. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  252. package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
  253. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
  254. package/studio/client/dist/index.html +4 -3
  255. package/studio/package.json +8 -14
  256. package/studio/server/agent/ask.test.ts +1 -1
  257. package/studio/server/agent/bridge/grant.test.ts +1 -1
  258. package/studio/server/agent/bridge/routes.test.ts +1 -1
  259. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  260. package/studio/server/agent/harness/gateway/token.test.ts +1 -1
  261. package/studio/server/agent/harness/gateway/token.ts +3 -3
  262. package/studio/server/agent/prompts/anchors.test.ts +41 -32
  263. package/studio/server/agent/prompts/anchors.ts +23 -51
  264. package/studio/server/agent/prompts/system.test.ts +4 -4
  265. package/studio/server/agent/prompts/system.ts +5 -5
  266. package/studio/server/agent/routes.test.ts +1 -1
  267. package/studio/server/agent/run/coordinator.test.ts +1 -1
  268. package/studio/server/agent/run/preparation.ts +1 -1
  269. package/studio/server/api/canvas.ts +0 -1
  270. package/studio/server/api/context.ts +1 -1
  271. package/studio/server/api/deployment.ts +1 -1
  272. package/studio/server/api/views.ts +2 -2
  273. package/studio/server/api/workspace.ts +1 -1
  274. package/studio/server/api-agent-loadout.test.ts +1 -1
  275. package/studio/server/api.test.ts +1 -1
  276. package/studio/server/cache.test.ts +24 -18
  277. package/studio/server/cache.ts +10 -54
  278. package/studio/server/cli-consumers.test.ts +32 -4
  279. package/studio/server/cli.test.ts +2 -2
  280. package/studio/server/domain.test.ts +50 -68
  281. package/studio/server/domain.ts +82 -63
  282. package/studio/server/handoff/copy.ts +2 -2
  283. package/studio/server/index.ts +1 -1
  284. package/studio/server/instances/probe.test.ts +1 -1
  285. package/studio/server/introspect/anatomy/views/routes.ts +46 -37
  286. package/studio/server/introspect/anatomy/views.ts +10 -16
  287. package/studio/server/introspect/anatomy-extras.test.ts +29 -56
  288. package/studio/server/introspect/anatomy.test.ts +22 -14
  289. package/studio/server/introspect/anatomy.ts +6 -9
  290. package/studio/server/introspect/bundle.ts +0 -3
  291. package/studio/server/introspect/canonical-schema.test.ts +155 -424
  292. package/studio/server/introspect/canonical-schema.ts +248 -601
  293. package/studio/server/introspect/core.ts +14 -22
  294. package/studio/server/introspect/diff.test.ts +21 -9
  295. package/studio/server/introspect/diff.ts +19 -64
  296. package/studio/server/introspect/extractor.ts +23 -122
  297. package/studio/server/introspect/overlay-tsmorph.test.ts +116 -241
  298. package/studio/server/introspect/overlay-tsmorph.ts +0 -1
  299. package/studio/server/introspect/overlay.ts +3 -51
  300. package/studio/server/introspect/revision.test.ts +24 -28
  301. package/studio/server/introspect/revision.ts +11 -22
  302. package/studio/server/introspect/runtime.test.ts +77 -195
  303. package/studio/server/introspect/runtime.ts +5 -67
  304. package/studio/server/introspect/schema-ir-json.test.ts +81 -0
  305. package/studio/server/introspect/schema-ir-json.ts +39 -69
  306. package/studio/server/introspect/schema-refs.test.ts +43 -88
  307. package/studio/server/introspect/schema-refs.ts +14 -49
  308. package/studio/server/introspect/source-overlay/handlers.ts +116 -636
  309. package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
  310. package/studio/server/introspect/source-overlay/spans.ts +15 -45
  311. package/studio/server/lifecycle.ts +4 -1
  312. package/studio/server/state/baseline.test.ts +14 -9
  313. package/studio/server/state/baseline.ts +3 -5
  314. package/studio/server/state/documents.test.ts +69 -0
  315. package/studio/server/state/documents.ts +57 -10
  316. package/studio/server/state/visibility.ts +1 -5
  317. package/studio/server/views/model.ts +3 -3
  318. package/studio/server/views/target.test.ts +18 -76
  319. package/studio/server/views/target.ts +25 -51
  320. package/studio/server/watch.test.ts +11 -11
  321. package/studio/server/watch.ts +8 -4
  322. package/studio/server/workspace/updates.ts +23 -1
  323. package/studio/server/workspace-watch.ts +2 -2
  324. package/studio/shared/contracts/runtime.ts +4 -0
  325. package/studio/shared/contracts/schema.ts +44 -131
  326. package/studio/shared/contracts/surface.test.ts +15 -17
  327. package/studio/shared/contracts/workspace.ts +5 -11
  328. package/studio/shared/schema/identity.test.ts +20 -44
  329. package/studio/shared/schema/identity.ts +9 -24
  330. package/studio/tsconfig.json +2 -1
  331. package/tsconfig.json +1 -1
  332. package/viewer/dist/index.html +21 -0
  333. package/viewer/dist/main.js +41 -62
  334. package/.check-workspace.cjs +0 -40
  335. package/src/admin/binding.ts +0 -168
  336. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  337. package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
  338. package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
  339. package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
  340. package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
  341. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  342. package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
  343. package/studio/server/introspect/core-extractor.ts +0 -139
  344. package/studio/server/introspect/overlay.test.ts +0 -72
  345. package/studio/server/introspect/source-overlay/annotations.ts +0 -14
@@ -19,6 +19,7 @@ import { fatal, log } from '../lib/log'
19
19
  import { isMachine, output, type RawOutputOpts } from '../lib/output'
20
20
  import { findFreePort } from '../lib/port'
21
21
  import { run, spawnHandle } from '../lib/proc'
22
+ import { admitExternalOpenOrigins } from '../lib/view/external-open-origins'
22
23
  import { withViewPortAllocationLock } from '../lib/view/port-allocation'
23
24
  import {
24
25
  candidateSlug,
@@ -42,7 +43,7 @@ import {
42
43
  import { snapshotText, waitForSettledSnapshot } from '../lib/view/snapshot'
43
44
 
44
45
  /**
45
- * `astrale view` — open ONE view in an emulated host shell, authenticated as
46
+ * `astrale view` — open ONE view in a local browser shell, authenticated as
46
47
  * the CLI identity, driveable by agent-browser (default) or a real browser.
47
48
  * Design + protocol details: VIEW_CLI_SPEC.md at the workspace root.
48
49
  */
@@ -62,6 +63,7 @@ type ViewOpts = KernelCommandOpts &
62
63
  sessions?: boolean
63
64
  close?: string | boolean
64
65
  all?: boolean
66
+ allowExternalOrigin?: string[]
65
67
  }
66
68
 
67
69
  const VIEW_PORT_BASE = 4419
@@ -89,9 +91,10 @@ export async function resolveSession(
89
91
  }
90
92
  const targetInput =
91
93
  parsed.kind === 'target' ? parsed.path : (opts.target ?? viewOwnerTarget(parsed.path))
92
- const { path: target } = await expandSelfInPath(targetInput, opts)
93
-
94
- const candidates = await withClientSession(opts, (ctx) => resolveViewCandidates(ctx, target))
94
+ const { target, candidates } = await withClientSession(opts, async (context) => {
95
+ const { path: target } = await expandSelfInPath(targetInput, context)
96
+ return { target, candidates: await resolveViewCandidates(context, target) }
97
+ })
95
98
 
96
99
  if (opts.list) {
97
100
  return { candidates }
@@ -150,16 +153,47 @@ async function chooseCandidate(
150
153
  * die once the CLI exits. The published CLI entry is node-runnable; a dev
151
154
  * checkout builds `dist/astrale.js` on demand (Bun is present there).
152
155
  */
153
- async function resolveServeRuntime(): Promise<{ file: string; args: string[] }> {
154
- const entry = process.argv[1]
155
- const node = await findOnPath('node')
156
- if (node && entry?.endsWith('.js') && existsSync(entry)) return { file: node, args: [entry] }
156
+ interface ServeRuntimeEnvironment {
157
+ readonly entry: string | undefined
158
+ readonly executable: string
159
+ readonly exists: typeof existsSync
160
+ readonly find: typeof findOnPath
161
+ }
162
+
163
+ export async function resolveServeRuntime(
164
+ environment: Partial<ServeRuntimeEnvironment> = {},
165
+ ): Promise<{ file: string; args: string[] }> {
166
+ const entry = environment.entry ?? process.argv[1]
167
+ const executable = environment.executable ?? process.execPath
168
+ const exists = environment.exists ?? existsSync
169
+ const find = environment.find ?? findOnPath
170
+ const node = await find('node')
171
+ if (node && entry?.endsWith('.js') && exists(entry)) return { file: node, args: [entry] }
157
172
  if (node && entry?.endsWith('.ts')) {
158
173
  const dist = join(dirname(entry), '..', 'dist', 'astrale.js')
159
174
  await ensureDevDist(entry, dist)
160
- if (existsSync(dist)) return { file: node, args: [dist] }
175
+ if (exists(dist)) return { file: node, args: [dist] }
176
+ }
177
+ return directServeRuntime(executable, entry, entry !== undefined && exists(entry))
178
+ }
179
+
180
+ /** Reinvoke a compiled executable without its virtual Bun filesystem entry. */
181
+ export function directServeRuntime(
182
+ executable: string,
183
+ entry: string | undefined,
184
+ entryExists = entry !== undefined && existsSync(entry),
185
+ ): { file: string; args: string[] } {
186
+ return {
187
+ file: executable,
188
+ args: entry && !entry.startsWith('/$bunfs/') && entryExists ? [entry] : [],
161
189
  }
162
- return { file: process.execPath, args: entry && existsSync(entry) ? [entry] : [] }
190
+ }
191
+
192
+ export function viewServeInvocation(
193
+ runtime: { file: string; args: string[] },
194
+ config: string,
195
+ ): { file: string; args: string[] } {
196
+ return { file: runtime.file, args: [...runtime.args, '__view-serve', '--config', config] }
163
197
  }
164
198
 
165
199
  async function findOnPath(name: string): Promise<string | null> {
@@ -228,6 +262,31 @@ async function startSession(view: ResolvedView, opts: ViewOpts): Promise<ViewSes
228
262
  )
229
263
  }
230
264
 
265
+ export function createViewServeConfig(
266
+ record: ViewSessionRecord,
267
+ opts: Pick<ViewOpts, 'allowExternalOrigin' | 'as' | 'creds' | 'instance' | 'timeout' | 'url'>,
268
+ kernelTarget: { url: string; kernelIssuer: string; caFile?: string },
269
+ ): ViewServeConfig {
270
+ return {
271
+ session: record,
272
+ kernel: {
273
+ url: opts.url,
274
+ instance: opts.instance,
275
+ as: opts.as,
276
+ creds: opts.creds,
277
+ timeout: opts.timeout,
278
+ },
279
+ proxy: {
280
+ kernelUrl: kernelTarget.url,
281
+ issuer: kernelTarget.kernelIssuer,
282
+ caFile: kernelTarget.caFile,
283
+ direct: isPublicHttps(kernelTarget.url) && !kernelTarget.caFile,
284
+ },
285
+ externalOrigins: admitExternalOpenOrigins(opts.allowExternalOrigin),
286
+ idleMs: IDLE_MS,
287
+ }
288
+ }
289
+
231
290
  /**
232
291
  * Called under the cross-process port-allocation lock. Keep the lock until the
233
292
  * detached child answers its readiness probe: only then is the selected port
@@ -261,34 +320,15 @@ async function startSessionLocked(
261
320
  identity: opts.creds ? '(pre-signed creds)' : (opts.as ?? defaultIdentity),
262
321
  createdAt: new Date().toISOString(),
263
322
  }
264
- const serveConfig: ViewServeConfig = {
265
- session: record,
266
- kernel: {
267
- url: opts.url,
268
- instance: opts.instance,
269
- as: opts.as,
270
- creds: opts.creds,
271
- timeout: opts.timeout,
272
- },
273
- proxy: {
274
- kernelUrl: kernelTarget.url,
275
- issuer: kernelTarget.kernelIssuer,
276
- caFile: kernelTarget.caFile,
277
- direct: isPublicHttps(kernelTarget.url) && !kernelTarget.caFile,
278
- },
279
- idleMs: IDLE_MS,
280
- }
323
+ const serveConfig = createViewServeConfig(record, opts, kernelTarget)
281
324
 
282
325
  await saveServeConfig(serveConfig)
283
326
  const logFd = await openSessionLog(id)
284
- const child = spawnHandle(
285
- runtime.file,
286
- [...runtime.args, '__view-serve', '--config', configPath(id)],
287
- {
288
- detached: true,
289
- stdio: ['ignore', logFd, logFd],
290
- },
291
- )
327
+ const invocation = viewServeInvocation(runtime, configPath(id))
328
+ const child = spawnHandle(invocation.file, invocation.args, {
329
+ detached: true,
330
+ stdio: ['ignore', logFd, logFd],
331
+ })
292
332
  child.unref()
293
333
  closeSync(logFd)
294
334
  if (!child.pid) throw new Error('Failed to spawn the view session server')
@@ -484,7 +524,7 @@ async function sessionsCommand(opts: ViewOpts): Promise<void> {
484
524
 
485
525
  export default {
486
526
  name: 'view',
487
- description: 'Open one view in an emulated host shell your agent can drive',
527
+ description: 'Open one view in a local browser shell your agent can drive',
488
528
  arguments: [
489
529
  {
490
530
  name: 'spec',
@@ -520,11 +560,15 @@ export default {
520
560
  description: 'Close a view session (bare: the only open one; with --all: every session)',
521
561
  },
522
562
  { flags: '--all', description: 'With --close: close every session' },
563
+ {
564
+ flags: '--allow-external-origin <origin...>',
565
+ description: 'Grant this View exact HTTPS origins it may open in a new browser context',
566
+ },
523
567
  ],
524
568
  afterHelpText: `
525
569
  What it does:
526
570
  Renders ONE view — no GUI, no cookies, no WorkOS. It resolves the view on the
527
- kernel, starts a loopback session server that emulates the shell host (real
571
+ kernel, starts a loopback session server that supplies the shell handshake (real
528
572
  handshake via @astrale-os/shell, token minted from YOUR CLI identity, kernel
529
573
  calls proxied), and opens the page headless in agent-browser. Driving stays
530
574
  agent-browser's job; auth follows --as/--creds/-i like any kernel command.
@@ -538,6 +582,7 @@ Examples:
538
582
  $ astrale view /:crm.example.dev:view.dashboard
539
583
  $ astrale view /:agents.astrale.ai:view.agent --target @f00d1234 --as alice
540
584
  $ astrale view @customer --snapshot
585
+ $ astrale view /:integrations.astrale.ai:view.application --allow-external-origin https://connect.nango.dev https://connect.composio.dev
541
586
  $ astrale view --list
542
587
  $ astrale view --sessions ; astrale view --close --all
543
588
  `,
@@ -1,8 +1,7 @@
1
- import type { Call } from '@astrale-os/kernel-client'
2
- import type { AuthApi } from '@astrale-os/kernel-client/auth'
3
- import type { GraphApi } from '@astrale-os/kernel-client/graph'
4
- import type { ClientSession } from '@astrale-os/kernel-client/session'
5
1
  import type { IssuerId } from '@astrale-os/sdk/auth'
2
+ import type { AuthApi } from '@astrale-os/sdk/auth'
3
+ import type { Call, GraphApi } from '@astrale-os/sdk/client'
4
+ import type { ClientSession } from '@astrale-os/sdk/client/session'
6
5
 
7
6
  /** Existing CLI connection flags accepted by Kernel-touching commands. */
8
7
  export interface ConnectionOptions {
@@ -78,17 +77,17 @@ export function withAdminClientSession<Value>(
78
77
  /** Expand @self through the effective principal returned by authenticated Identity.whoami. */
79
78
  export function expandSelfInPath(
80
79
  path: string,
81
- options: KernelCommandOpts,
80
+ context: ConnectionContext,
82
81
  ): Promise<{ readonly path: string; readonly meta?: SelfExpansionMeta }>
83
82
 
84
83
  /** Expand @self once across one Call path and its CLI-authored string parameters. */
85
84
  export function expandSelfInCall(
86
85
  path: string,
87
- parameters: readonly string[],
88
- options: KernelCommandOpts,
86
+ parameters: Readonly<Record<string, unknown>>,
87
+ context: ConnectionContext,
89
88
  ): Promise<{
90
89
  readonly path: string
91
- readonly parameters: readonly string[]
90
+ readonly parameters: Readonly<Record<string, unknown>>
92
91
  readonly meta?: SelfExpansionMeta
93
92
  }>
94
93
 
@@ -28,7 +28,15 @@ selection, and the single safe stale-route recovery. The CLI does not reproduce
28
28
  discover destination identity. `SessionAuth.resolve(call, signal)` returns source-Kernel authority and
29
29
  ClientSession supplies an audience-free Delegate with omitted attenuation, preserving the exact
30
30
  current Grant for routing. Connection itself persists no credential or route; the separate state
31
- owner persists only exchanged source credentials.
31
+ owner persists exchanged source credentials and the separate Kernel Client route artifact. A valid exchanged credential is selected by the
32
+ authenticated source issuer and subject before any live `whoami`; cache misses alone resolve the
33
+ registered Kernel User and perform delegation plus Domain exchange.
34
+ Exchange authority is bounded to five minutes and never outlives the current source credential,
35
+ matching the CLI route-age ceiling without forcing an unrelated two-minute refresh cycle.
36
+
37
+ Every ClientSession receives the CLI-owned `state/session-routes` representation capability. Kernel
38
+ Client still owns route keying, admission, expiry, and one safe stale/miss recovery; Connection does
39
+ not reproduce routing or add an Admin-only bypass.
32
40
 
33
41
  `--anonymous` deliberately suppresses ambient and bookmark-default identities by omitting the
34
42
  `SessionAuth` capability. It cannot be combined with `--as` or `--creds`; contradictory selections fail
@@ -40,7 +48,8 @@ customizes only the Fetch capability passed to Client. `withClientSession` and
40
48
  `withAdminClientSession` are terminal lifecycle boundaries: success, failure, and cancellation all
41
49
  close both the Client Session and its direct source-Auth client.
42
50
 
43
- The command boundary maps typed Client transport phase and delivery evidence without inspecting a
44
- private cause message. It preserves every admitted Kernel reason in machine output; human repair
45
- details are rendered only after the connection owner admits bounded public Function issues or one
46
- exact Query reason variant.
51
+ The command boundary projects typed Client failure identity, transport context, phase, and
52
+ invocation-only delivery evidence without inspecting a private cause message. Unknown native
53
+ failures become one honest unexpected diagnostic; their bounded cause graph is visible only under
54
+ explicit debug output. Every admitted Kernel reason remains available in machine output, while
55
+ human repair details require a bounded public Function issue or exact Query reason variant.
@@ -1,5 +1,5 @@
1
- import type { SessionAuth } from '@astrale-os/kernel-client/session'
2
- import type { Call } from '@astrale-os/kernel-protocol/invocation'
1
+ import type { Call } from '@astrale-os/sdk/client'
2
+ import type { SessionAuth } from '@astrale-os/sdk/client/session'
3
3
 
4
4
  import type { ConnectionContext, ConnectionOptions, ConnectionTarget } from '../api.js'
5
5
 
@@ -111,7 +111,7 @@ export const CLI_AUTH_REGISTRATION_TARGET = defineLaw({
111
111
  export const CLI_SELF_AUTHENTICATED_PRINCIPAL = defineLaw({
112
112
  id: 'CLI-SELF-AUTHENTICATED-PRINCIPAL',
113
113
  statement:
114
- '@self resolves only from authenticated Identity.whoami on the selected target, so delegated carriers use their effective principal and never an unverified JWT subject or stale local registration.',
114
+ '@self resolves inside the command-owned Client Session from authenticated Identity.whoami on the selected target, so resolution and dispatch share one lifecycle and delegated carriers never use an unverified JWT subject or stale local registration.',
115
115
  tests: [
116
116
  {
117
117
  file: '__tests__/self.test.ts',
@@ -183,7 +183,7 @@ export const CLI_CONNECTION_PUBLIC_SEMANTIC_REASON = defineLaw({
183
183
  export const CLI_CONNECTION_TYPED_ERROR_PRESENTATION = defineLaw({
184
184
  id: 'CLI-CONNECTION-TYPED-ERROR-PRESENTATION',
185
185
  statement:
186
- 'The command boundary maps typed Client transport phase and delivery evidence without parsing a private cause, preserves the admitted Kernel reason in machine output, and renders only bounded public Function issues or one exact Query repair variant for humans.',
186
+ 'The command boundary maps typed Client failure identity, transport context, phase, and invocation-only delivery evidence without parsing a private cause; acquisition never receives retry advice, unknown native failures have one non-blank unexpected diagnostic, and raw causes appear only under explicit debug output.',
187
187
  tests: [
188
188
  {
189
189
  file: '__tests__/errors.test.ts',
@@ -197,5 +197,9 @@ export const CLI_CONNECTION_TYPED_ERROR_PRESENTATION = defineLaw({
197
197
  file: '__tests__/reasons.test.ts',
198
198
  id: 'TEST-CLI-CONNECTION-ADMITS-BOUNDED-REASONS',
199
199
  },
200
+ {
201
+ file: '__tests__/failure-safety.test.ts',
202
+ id: 'TEST-CLI-CONNECTION-FAILURE-SAFETY',
203
+ },
200
204
  ],
201
205
  })
@@ -8,6 +8,7 @@ export default defineLayout({
8
8
  'command.ts',
9
9
  'credential.ts',
10
10
  'errors.ts',
11
+ 'failure/',
11
12
  'exchange.ts',
12
13
  'index.ts',
13
14
  'reasons.ts',
@@ -15,6 +15,7 @@ const config: AstraleConfig = {
15
15
  kernelIssuer: 'https://admin.eu.astrale.ai/api',
16
16
  },
17
17
  telemetry: { enabled: true },
18
+ browser: {},
18
19
  }
19
20
 
20
21
  describe('resolveKeyIdentityAuthOptions', () => {
@@ -1,4 +1,4 @@
1
- import type { SessionAuth } from '@astrale-os/kernel-client/session'
1
+ import type { SessionAuth } from '@astrale-os/sdk/client/session'
2
2
 
3
3
  import { issuer, type IssuerId } from '@astrale-os/sdk/auth'
4
4
  import { Path } from '@astrale-os/sdk/graph/path'
@@ -6,6 +6,7 @@ import { describe, expect, test } from 'bun:test'
6
6
 
7
7
  import type { AstraleConfig } from '../../lib/config'
8
8
 
9
+ import { bindCredentialIdentity } from '../auth'
9
10
  import { createCliCredential, createConnectionCredential } from '../credential'
10
11
 
11
12
  const SOURCE = issuer.accept('https://kernel.example')
@@ -17,9 +18,28 @@ const config: AstraleConfig = {
17
18
  issuer: 'https://cli.example',
18
19
  admin: { name: 'admin', url: SOURCE, kernelIssuer: SOURCE },
19
20
  telemetry: { enabled: false },
21
+ browser: {},
20
22
  }
21
23
 
22
24
  describe('connection credential', () => {
25
+ test('pins the bookmark identity while leaving explicit credentials unnamed', async () => {
26
+ const target = {
27
+ url: `${SOURCE}/invoke`,
28
+ kernelIssuer: SOURCE,
29
+ defaultIdentity: 'bookmark-owner',
30
+ }
31
+
32
+ await expect(bindCredentialIdentity({}, target)).resolves.toMatchObject({
33
+ as: 'bookmark-owner',
34
+ })
35
+ await expect(bindCredentialIdentity({ as: 'explicit-owner' }, target)).resolves.toEqual({
36
+ as: 'explicit-owner',
37
+ })
38
+ await expect(bindCredentialIdentity({ creds: 'opaque' }, target)).resolves.toEqual({
39
+ creds: 'opaque',
40
+ })
41
+ })
42
+
23
43
  /** @evidence TEST-CLI-CONNECTION-RESOLVES-SOURCE-AUTH-BEFORE-ROUTING */
24
44
  test('resolves source authority without destination knowledge', async () => {
25
45
  const audiences: IssuerId[] = []
@@ -1,7 +1,9 @@
1
- import { ResponseError, TransportError } from '@astrale-os/kernel-client'
1
+ import { ResponseError } from '@astrale-os/sdk/client'
2
+ import { Path } from '@astrale-os/sdk/graph/path'
2
3
  import { describe, expect, test } from 'bun:test'
3
4
 
4
5
  import { formatKernelError, functionInputIssues, schemaUpgradeHint } from '../errors'
6
+ import { transportFailure } from './failure-fixtures'
5
7
 
6
8
  const CONFLICT = 4001
7
9
  const VALIDATION = 1003
@@ -19,24 +21,28 @@ describe('formatKernelError', () => {
19
21
  writes.push(typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk))
20
22
  return true
21
23
  }) as typeof process.stderr.write
22
- const error = new TransportError('Publication discovery request failed.', {
23
- cause: Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' }),
24
- phase: 'connect',
25
- delivery: 'unknown',
26
- }) as TransportError & { url?: string }
27
- error.url = 'https://localhost:8443/kernel/host'
24
+ const error = Object.assign(
25
+ transportFailure('Publication discovery request failed.', 'unknown', {
26
+ kind: 'acquisition',
27
+ resource: 'publication',
28
+ }),
29
+ {
30
+ url: 'https://localhost:8443/kernel/host',
31
+ cause: Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' }),
32
+ },
33
+ )
28
34
  try {
29
- await formatKernelError(error, true)
35
+ await formatKernelError(error, true, 'https://localhost:8443/kernel/host')
30
36
  } finally {
31
37
  process.stderr.write = original
32
38
  }
33
39
 
34
40
  expect(JSON.parse(writes[0]!)).toMatchObject({
35
- error: 'CONNECTION_ERROR',
41
+ error: 'TRANSPORT_ERROR',
36
42
  message: 'Publication discovery request failed.',
37
43
  url: 'https://localhost:8443/kernel/host',
38
- phase: 'connect',
39
- delivery: 'unknown',
44
+ phase: 'unknown',
45
+ transport: { kind: 'acquisition', resource: 'publication' },
40
46
  })
41
47
  expect(writes[0]).not.toContain('ECONNREFUSED')
42
48
  })
@@ -50,9 +56,8 @@ describe('formatKernelError', () => {
50
56
  }) as typeof process.stderr.write
51
57
  try {
52
58
  await formatKernelError(
53
- new TransportError('Request timed out.', {
54
- cause: new Error('timeout'),
55
- phase: 'timeout',
59
+ transportFailure('Request timed out.', 'timeout', {
60
+ kind: 'invocation',
56
61
  delivery: 'unknown',
57
62
  }),
58
63
  true,
@@ -74,7 +79,7 @@ describe('formatKernelError', () => {
74
79
  expect(JSON.parse(writes[0]!)).toMatchObject({
75
80
  error: 'TIMEOUT',
76
81
  phase: 'timeout',
77
- delivery: 'unknown',
82
+ transport: { kind: 'invocation', delivery: 'unknown' },
78
83
  operation: '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
79
84
  retry:
80
85
  'astrale domain install https://crm.test --direct --operation 4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8',
@@ -229,7 +234,7 @@ describe('formatKernelError', () => {
229
234
  )
230
235
  })
231
236
 
232
- test('maps SDK class names to stable CLI error codes', async () => {
237
+ test('maps SDK error identities to stable CLI error codes', async () => {
233
238
  const writes: string[] = []
234
239
  const original = process.stderr.write
235
240
  process.stderr.write = ((chunk: string | Uint8Array) => {
@@ -237,15 +242,17 @@ describe('formatKernelError', () => {
237
242
  return true
238
243
  }) as typeof process.stderr.write
239
244
  try {
240
- const pathError = new Error('Path must have an Id or Domain anchor.')
241
- pathError.name = 'PathError'
242
- await formatKernelError(pathError, true)
245
+ try {
246
+ Path.parse('not-a-path')
247
+ } catch (error) {
248
+ await formatKernelError(error, true)
249
+ }
243
250
  } finally {
244
251
  process.stderr.write = original
245
252
  }
246
253
  expect(JSON.parse(writes[0]!)).toEqual({
247
254
  error: 'PATH_INVALID',
248
- message: 'Path must have an Id or Domain anchor.',
255
+ message: expect.any(String),
249
256
  })
250
257
  })
251
258
 
@@ -1,4 +1,4 @@
1
- import type { Fetch } from '@astrale-os/kernel-client'
1
+ import type { Fetch } from '@astrale-os/sdk/client'
2
2
 
3
3
  import { issuer } from '@astrale-os/sdk/auth'
4
4
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
@@ -14,13 +14,8 @@ const DOMAIN = issuer.accept('https://admin.example')
14
14
  const TARGET = { url: `${KERNEL}/api`, kernelIssuer: KERNEL, domainIssuer: DOMAIN }
15
15
  const INVOCATION = `${KERNEL}/invoke`
16
16
  const EXPIRES_AT = Math.floor(Date.now() / 1_000) + 500
17
- const SOURCE_EXPIRES_AT = Math.floor(Date.now() / 1_000) + 120
18
- const SOURCE_TOKEN = token(
19
- issuer.accept('https://workos.example'),
20
- KERNEL,
21
- 'user-1',
22
- SOURCE_EXPIRES_AT,
23
- )
17
+ const SOURCE_EXPIRES_AT = Math.floor(Date.now() / 1_000) + 600
18
+ const SOURCE_TOKEN = sourceToken('user-1')
24
19
  let directory: string
25
20
 
26
21
  beforeEach(async () => {
@@ -33,7 +28,7 @@ afterEach(async () => {
33
28
 
34
29
  describe('Domain token exchange', () => {
35
30
  /** @evidence TEST-CLI-EXCHANGE-WHOAMI-DELEGATE-EXCHANGE-CACHE */
36
- test('runs the exact User to Kernel to Domain journey and reuses the bound token', async () => {
31
+ test('runs the exact User to Kernel to Domain journey once and reuses it before whoami', async () => {
37
32
  const observed: Array<{
38
33
  url: string
39
34
  init: RequestInit | undefined
@@ -91,7 +86,7 @@ describe('Domain token exchange', () => {
91
86
  await expect(resolver.resolve(KERNEL, new AbortController().signal)).resolves.toBe(exchanged)
92
87
 
93
88
  const kernelRequests = observed.filter((entry) => entry.url === INVOCATION)
94
- expect(kernelRequests).toHaveLength(3)
89
+ expect(kernelRequests).toHaveLength(2)
95
90
  expect(kernelRequests[0]!.body).toMatchObject({
96
91
  credential: SOURCE_TOKEN,
97
92
  call: { input: {} },
@@ -107,13 +102,32 @@ describe('Domain token exchange', () => {
107
102
  })
108
103
  const delegatedTtl = kernelRequests[1]!.body!.call.input.ttlSeconds
109
104
  expect(delegatedTtl).toBeGreaterThan(0)
110
- expect(delegatedTtl).toBeLessThan(120)
105
+ expect(delegatedTtl).toBeGreaterThanOrEqual(295)
106
+ expect(delegatedTtl).toBeLessThanOrEqual(300)
111
107
  expect(sourceAudiences).toEqual([KERNEL, KERNEL])
112
108
  expect(
113
109
  observed.filter((entry) => entry.url.endsWith('/.well-known/astrale/token')),
114
110
  ).toHaveLength(1)
115
111
  })
116
112
 
113
+ test('rejects a source credential without a stable cache identity before network I/O', async () => {
114
+ let fetches = 0
115
+ const resolver = createExchangeCredentialResolver(
116
+ TARGET,
117
+ { resolve: async () => sourceToken(undefined) },
118
+ async () => {
119
+ fetches += 1
120
+ throw new Error('network must remain untouched')
121
+ },
122
+ 5_000,
123
+ new ExchangeCredentialCache(join(directory, 'credentials.json')),
124
+ )
125
+ await expect(resolver.resolve(KERNEL, new AbortController().signal)).rejects.toMatchObject({
126
+ code: 'TOKEN_EXCHANGE_SOURCE_INVALID',
127
+ })
128
+ expect(fetches).toBe(0)
129
+ })
130
+
117
131
  /** @evidence TEST-CLI-EXCHANGE-NO-LEGACY-FALLBACK */
118
132
  test('fails closed when issuer discovery does not advertise exchange', async () => {
119
133
  const fetch: Fetch = async (input, init) => {
@@ -190,6 +204,35 @@ describe('Domain token exchange', () => {
190
204
  message: 'Token exchange returned an invalid success response.',
191
205
  })
192
206
  })
207
+
208
+ test('normalizes native issuer Fetch failures without exposing their message', async () => {
209
+ const native = new AggregateError([new Error('private DNS state')], '')
210
+ const resolver = createExchangeCredentialResolver(
211
+ TARGET,
212
+ { resolve: async () => SOURCE_TOKEN },
213
+ async (input, init) => {
214
+ if (String(input) === INVOCATION) {
215
+ const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
216
+ return invocationResponse(
217
+ body.requestId,
218
+ body.call.input && Object.keys(body.call.input).length === 0
219
+ ? { id: 'user-1' }
220
+ : 'kernel-destination-envelope',
221
+ new Headers(init?.headers).get('accept')!,
222
+ )
223
+ }
224
+ throw native
225
+ },
226
+ 5_000,
227
+ new ExchangeCredentialCache(join(directory, 'native-failure.json')),
228
+ )
229
+
230
+ await expect(resolver.resolve(KERNEL, new AbortController().signal)).rejects.toMatchObject({
231
+ code: 'TOKEN_EXCHANGE_UNAVAILABLE',
232
+ message: 'Domain issuer discovery could not be reached.',
233
+ cause: native,
234
+ })
235
+ })
193
236
  })
194
237
 
195
238
  function exchangeFetch(
@@ -264,3 +307,13 @@ function token(iss: string, aud: string, user: string, exp: number): string {
264
307
  grant: { v: 1, expr: { kind: 'identity', credential: proof } },
265
308
  })}.signature`
266
309
  }
310
+
311
+ function sourceToken(subject: string | undefined): string {
312
+ const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString('base64url')
313
+ return `${encode({ alg: 'EdDSA', typ: 'JWT' })}.${encode({
314
+ iss: 'https://workos.example',
315
+ ...(subject === undefined ? {} : { sub: subject }),
316
+ aud: KERNEL,
317
+ exp: SOURCE_EXPIRES_AT,
318
+ })}.signature`
319
+ }
@@ -0,0 +1,39 @@
1
+ import { ClientError, TransportError } from '@astrale-os/sdk/client'
2
+
3
+ import type { TransportDiagnosticContext } from '../failure/model'
4
+
5
+ type CompatibleTransportPhase = TransportError['phase'] | 'unknown'
6
+
7
+ export function transportFailure(
8
+ message: string,
9
+ phase: CompatibleTransportPhase,
10
+ context: TransportDiagnosticContext,
11
+ ): TransportError {
12
+ const error = new Error(message) as TransportError & {
13
+ context: TransportDiagnosticContext
14
+ }
15
+ Object.setPrototypeOf(error, TransportError.prototype)
16
+ Object.assign(error, { name: 'TransportError', phase, context })
17
+ return error
18
+ }
19
+
20
+ export function legacyTransportFailure(
21
+ message: string,
22
+ phase: CompatibleTransportPhase,
23
+ delivery: 'not-sent' | 'unknown',
24
+ ): TransportError {
25
+ const error = new Error(message) as TransportError & { delivery: typeof delivery }
26
+ Object.setPrototypeOf(error, TransportError.prototype)
27
+ Object.assign(error, { name: 'TransportError', phase, delivery })
28
+ return error
29
+ }
30
+
31
+ export function sessionFailure(
32
+ message: string,
33
+ failure: 'cancelled' | 'closed' | 'timeout',
34
+ ): ClientError {
35
+ const error = new Error(message) as ClientError & { failure: typeof failure }
36
+ Object.setPrototypeOf(error, ClientError.prototype)
37
+ Object.assign(error, { name: 'SessionError', failure })
38
+ return error
39
+ }