@astrale-os/cli 0.8.0-alpha.0 → 0.8.1-alpha.2

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 (317) hide show
  1. package/README.md +4 -1
  2. package/dist/astrale.js +62968 -41240
  3. package/dist/public/connect-core.js +55030 -0
  4. package/dist/public/keys/index.js +45631 -0
  5. package/dist/public/paths/index.js +43990 -0
  6. package/dist/types/admin/instance/model.d.ts +40 -0
  7. package/dist/types/connect-core.d.ts +22 -0
  8. package/dist/types/connection/auth.d.ts +33 -0
  9. package/dist/types/errors.d.ts +23 -0
  10. package/dist/types/identity/registry.d.ts +25 -0
  11. package/dist/types/keys/algorithm.d.ts +6 -0
  12. package/dist/types/keys/credential.d.ts +21 -0
  13. package/dist/types/keys/index.d.ts +4 -0
  14. package/dist/types/keys/pair.d.ts +34 -0
  15. package/dist/types/lib/admin-target.d.ts +74 -0
  16. package/dist/types/lib/ca-fetch.d.ts +2 -0
  17. package/dist/types/lib/config.d.ts +71 -0
  18. package/dist/types/lib/format.d.ts +1 -0
  19. package/dist/types/lib/idp-session.d.ts +33 -0
  20. package/dist/types/lib/idp.d.ts +397 -0
  21. package/dist/types/lib/instance-target.d.ts +36 -0
  22. package/dist/types/lib/instance.d.ts +186 -0
  23. package/dist/types/lib/log.d.ts +30 -0
  24. package/dist/types/lib/login-flow.d.ts +49 -0
  25. package/dist/types/lib/meta.d.ts +36 -0
  26. package/dist/types/lib/output.d.ts +83 -0
  27. package/dist/types/lib/table.d.ts +18 -0
  28. package/dist/types/lib/validation.d.ts +11 -0
  29. package/dist/types/paths/index.d.ts +2 -0
  30. package/dist/types/state/exchange-credentials.d.ts +25 -0
  31. package/dist/types/state/files.d.ts +9 -0
  32. package/dist/types/state/identities.d.ts +42 -0
  33. package/dist/types/state/index.d.ts +8 -0
  34. package/dist/types/state/paths.d.ts +32 -0
  35. package/package.json +30 -8
  36. package/src/admin/.spec/api.d.ts +3 -0
  37. package/src/admin/.spec/architecture.md +15 -0
  38. package/src/admin/.spec/icon.svg +3 -0
  39. package/src/admin/.spec/layout.ts +6 -0
  40. package/src/admin/catalog/.spec/api.d.ts +65 -0
  41. package/src/admin/catalog/.spec/architecture.md +6 -0
  42. package/src/admin/catalog/.spec/icon.svg +3 -0
  43. package/src/admin/catalog/.spec/layout.ts +6 -0
  44. package/src/admin/catalog/__tests__/client.test.ts +171 -0
  45. package/src/admin/catalog/client.ts +214 -0
  46. package/src/admin/catalog/index.ts +13 -0
  47. package/src/admin/catalog/model.ts +42 -0
  48. package/src/admin/catalog/tsconfig.json +8 -0
  49. package/src/admin/graph/.spec/api.d.ts +28 -0
  50. package/src/admin/graph/.spec/architecture.md +5 -0
  51. package/src/admin/graph/.spec/icon.svg +3 -0
  52. package/src/admin/graph/.spec/layout.ts +3 -0
  53. package/src/admin/graph/index.ts +6 -0
  54. package/src/admin/graph/nodes.ts +56 -0
  55. package/src/admin/index.ts +2 -0
  56. package/src/admin/instance/.spec/api.d.ts +66 -0
  57. package/src/admin/instance/.spec/architecture.md +10 -0
  58. package/src/admin/instance/.spec/icon.svg +3 -0
  59. package/src/admin/instance/.spec/layout.ts +6 -0
  60. package/src/admin/instance/__tests__/client.test.ts +289 -0
  61. package/src/admin/instance/client.ts +298 -0
  62. package/src/admin/instance/index.ts +16 -0
  63. package/src/admin/instance/model.ts +65 -0
  64. package/src/admin/instance/tsconfig.json +8 -0
  65. package/src/commands/__tests__/admin-instance.test.ts +3 -36
  66. package/src/commands/__tests__/call.test.ts +21 -68
  67. package/src/commands/__tests__/domain-install-owned.test.ts +26 -27
  68. package/src/commands/__tests__/domain-list.test.ts +2 -6
  69. package/src/commands/__tests__/logs.test.ts +50 -98
  70. package/src/commands/__tests__/ls.test.ts +21 -17
  71. package/src/commands/__tests__/read-commands.test.ts +171 -101
  72. package/src/commands/__tests__/view.test.ts +100 -51
  73. package/src/commands/admin/status.ts +3 -3
  74. package/src/commands/admin/use.ts +29 -6
  75. package/src/commands/auth/login.ts +1 -1
  76. package/src/commands/auth/logout.ts +4 -2
  77. package/src/commands/auth/status.ts +2 -2
  78. package/src/commands/auth/token.ts +2 -2
  79. package/src/commands/browser.ts +1 -1
  80. package/src/commands/call.ts +55 -91
  81. package/src/commands/describe.ts +52 -147
  82. package/src/commands/domain/install.ts +81 -50
  83. package/src/commands/domain/list.ts +4 -8
  84. package/src/commands/domain/publish.ts +9 -31
  85. package/src/commands/get.ts +32 -65
  86. package/src/commands/identity/__tests__/register.test.ts +93 -0
  87. package/src/commands/identity/create.ts +2 -2
  88. package/src/commands/identity/delete.ts +2 -2
  89. package/src/commands/identity/export.ts +7 -33
  90. package/src/commands/identity/import.ts +21 -70
  91. package/src/commands/identity/list.ts +2 -2
  92. package/src/commands/identity/register.ts +148 -117
  93. package/src/commands/identity/sync.ts +2 -2
  94. package/src/commands/identity/unsync.ts +2 -2
  95. package/src/commands/identity/use.ts +2 -2
  96. package/src/commands/identity/whoami.ts +2 -2
  97. package/src/commands/idp/add.ts +1 -1
  98. package/src/commands/idp/list.ts +1 -1
  99. package/src/commands/idp/refresh.ts +1 -1
  100. package/src/commands/idp/remove.ts +2 -2
  101. package/src/commands/idp/show.ts +1 -1
  102. package/src/commands/instance/active.ts +2 -2
  103. package/src/commands/instance/bookmark.ts +8 -2
  104. package/src/commands/instance/create.ts +1 -1
  105. package/src/commands/instance/delete.ts +4 -10
  106. package/src/commands/instance/forget.ts +1 -1
  107. package/src/commands/instance/list.ts +3 -3
  108. package/src/commands/instance/status.ts +5 -7
  109. package/src/commands/instance/use.ts +3 -3
  110. package/src/commands/logs.ts +152 -279
  111. package/src/commands/ls.ts +78 -204
  112. package/src/commands/mutate.ts +70 -139
  113. package/src/commands/query.ts +100 -278
  114. package/src/commands/session/analyze.ts +1 -1
  115. package/src/commands/session/list.ts +1 -1
  116. package/src/commands/setup.ts +1 -1
  117. package/src/commands/status.ts +1 -1
  118. package/src/commands/studio.ts +1 -1
  119. package/src/commands/token.ts +26 -21
  120. package/src/commands/update.ts +1 -1
  121. package/src/commands/use.ts +2 -3
  122. package/src/commands/view-serve.ts +1 -1
  123. package/src/commands/view.ts +67 -109
  124. package/src/connect-core.test.ts +38 -0
  125. package/src/connect-core.ts +42 -0
  126. package/src/connection/.spec/api.d.ts +111 -0
  127. package/src/connection/.spec/architecture.md +41 -0
  128. package/src/connection/.spec/capabilities/connection.ts +13 -0
  129. package/src/connection/.spec/examples/call.ts +7 -0
  130. package/src/connection/.spec/flows/command.ts +20 -0
  131. package/src/connection/.spec/flows/session.ts +61 -0
  132. package/src/connection/.spec/laws/connection.ts +181 -0
  133. package/src/connection/.spec/layout.ts +19 -0
  134. package/src/{kernel → connection}/__tests__/auth.test.ts +26 -2
  135. package/src/connection/__tests__/ca-fetch.test.ts +53 -0
  136. package/src/connection/__tests__/call.test.ts +17 -0
  137. package/src/connection/__tests__/credential.test.ts +109 -0
  138. package/src/connection/__tests__/errors.test.ts +96 -0
  139. package/src/connection/__tests__/exchange.test.ts +224 -0
  140. package/src/connection/__tests__/fixtures/localhost-cert.pem +19 -0
  141. package/src/connection/__tests__/fixtures/localhost-key.base64 +1 -0
  142. package/src/connection/__tests__/managed.test.ts +39 -0
  143. package/src/connection/__tests__/self.test.ts +51 -0
  144. package/src/connection/__tests__/session.test.ts +140 -0
  145. package/src/connection/__tests__/target.test.ts +93 -0
  146. package/src/{kernel → connection}/auth.ts +8 -8
  147. package/src/connection/call.ts +7 -0
  148. package/src/{kernel/run.ts → connection/command.ts} +22 -13
  149. package/src/connection/credential.ts +114 -0
  150. package/src/{kernel → connection}/errors.ts +32 -9
  151. package/src/connection/exchange.ts +274 -0
  152. package/src/connection/index.ts +8 -0
  153. package/src/connection/self.ts +120 -0
  154. package/src/connection/session.ts +176 -0
  155. package/src/connection/target.ts +111 -0
  156. package/src/connection/tsconfig.json +6 -0
  157. package/src/graph/.spec/api.d.ts +34 -0
  158. package/src/graph/.spec/laws/documents.ts +24 -0
  159. package/src/graph/.spec/laws/projection.ts +13 -0
  160. package/src/graph/.spec/layout.ts +6 -0
  161. package/src/graph/__tests__/mutation.test.ts +43 -0
  162. package/src/graph/__tests__/projection.test.ts +22 -0
  163. package/src/graph/__tests__/query.test.ts +154 -0
  164. package/src/graph/index.ts +3 -0
  165. package/src/graph/mutation.ts +18 -0
  166. package/src/graph/projection.ts +21 -0
  167. package/src/graph/query.ts +108 -0
  168. package/src/graph/tsconfig.json +6 -0
  169. package/src/identity/.history/ard/bootstrap-root-transfer.md +57 -0
  170. package/src/identity/.history/ard/domain-mediated-registration.md +36 -0
  171. package/src/identity/.spec/api.d.ts +124 -0
  172. package/src/identity/.spec/architecture.md +33 -0
  173. package/src/identity/.spec/capabilities/identity.ts +7 -0
  174. package/src/identity/.spec/flows/bootstrap-root.ts +79 -0
  175. package/src/identity/.spec/flows/domain-mediated-registration.ts +51 -0
  176. package/src/identity/.spec/flows/import.ts +29 -0
  177. package/src/identity/.spec/laws/identity.ts +56 -0
  178. package/src/identity/.spec/layout.ts +13 -0
  179. package/src/identity/.spec/schemas/identity-export-v1.schema.json +25 -0
  180. package/src/identity/__tests__/fixtures/registry-journey.ts +53 -0
  181. package/src/identity/__tests__/registration.test.ts +64 -0
  182. package/src/identity/__tests__/registry.test.ts +70 -0
  183. package/src/identity/__tests__/transfer.test.ts +198 -0
  184. package/src/identity/index.ts +23 -0
  185. package/src/identity/registration.ts +72 -0
  186. package/src/identity/registry.ts +172 -0
  187. package/src/identity/transfer.ts +187 -0
  188. package/src/identity/tsconfig.json +6 -0
  189. package/src/index.ts +1 -0
  190. package/src/keys/.spec/api.d.ts +74 -0
  191. package/src/keys/.spec/architecture.md +13 -0
  192. package/src/keys/.spec/laws/keys.ts +43 -0
  193. package/src/keys/.spec/layout.ts +6 -0
  194. package/src/keys/__tests__/algorithm.test.ts +39 -0
  195. package/src/keys/__tests__/keys.test.ts +168 -0
  196. package/src/keys/algorithm.ts +33 -0
  197. package/src/keys/credential.ts +112 -0
  198. package/src/keys/index.ts +14 -0
  199. package/src/keys/pair.ts +219 -0
  200. package/src/keys/tsconfig.json +6 -0
  201. package/src/lib/__tests__/admin-target.test.ts +67 -7
  202. package/src/lib/__tests__/binary.test.ts +2 -3
  203. package/src/lib/__tests__/command-dx.test.ts +2 -2
  204. package/src/lib/__tests__/config.test.ts +3 -2
  205. package/src/lib/__tests__/idp.test.ts +4 -6
  206. package/src/lib/__tests__/instance-target.test.ts +7 -4
  207. package/src/lib/__tests__/local-status.test.ts +1 -1
  208. package/src/lib/__tests__/log.test.ts +31 -0
  209. package/src/lib/__tests__/meta.test.ts +55 -0
  210. package/src/lib/__tests__/sdk-deps.test.ts +1 -1
  211. package/src/lib/__tests__/self.test.ts +8 -230
  212. package/src/lib/__tests__/use-target.test.ts +1 -1
  213. package/src/lib/__tests__/view-open-intent.test.ts +78 -104
  214. package/src/lib/__tests__/view-session.test.ts +99 -0
  215. package/src/lib/admin-domain.ts +39 -25
  216. package/src/lib/admin-instance.ts +38 -43
  217. package/src/lib/admin-target.ts +67 -13
  218. package/src/lib/binary.ts +13 -34
  219. package/src/lib/browser.ts +1 -1
  220. package/src/{kernel → lib}/ca-fetch.ts +35 -27
  221. package/src/lib/config.ts +1 -1
  222. package/src/lib/idp-session.ts +1 -1
  223. package/src/lib/idp.ts +3 -8
  224. package/src/lib/instance-target.ts +12 -8
  225. package/src/lib/instance.ts +34 -2
  226. package/src/lib/local-status.ts +2 -2
  227. package/src/lib/log.ts +3 -1
  228. package/src/lib/login-flow.ts +32 -5
  229. package/src/lib/meta.ts +15 -11
  230. package/src/lib/provision-instance.ts +4 -12
  231. package/src/lib/self.ts +5 -147
  232. package/src/lib/update.ts +1 -1
  233. package/src/lib/use-target.ts +1 -1
  234. package/src/lib/validation.ts +2 -2
  235. package/src/lib/view/open-intent.ts +7 -37
  236. package/src/lib/view/port-allocation.ts +1 -1
  237. package/src/lib/view/resolve.ts +52 -45
  238. package/src/lib/view/server.ts +16 -33
  239. package/src/lib/view/session.ts +51 -22
  240. package/src/paths/.spec/api.d.ts +13 -0
  241. package/src/paths/.spec/architecture.md +5 -0
  242. package/src/paths/.spec/layout.ts +3 -0
  243. package/src/paths/index.ts +13 -0
  244. package/src/program/.spec/api.d.ts +42 -0
  245. package/src/program/.spec/architecture.md +18 -0
  246. package/src/program/.spec/laws/program.ts +29 -0
  247. package/src/program/.spec/layout.ts +14 -0
  248. package/src/program/__tests__/program.test.ts +359 -0
  249. package/src/program/build.ts +190 -0
  250. package/src/program/command.ts +42 -0
  251. package/src/program/index.ts +2 -0
  252. package/src/program/options.ts +40 -0
  253. package/src/{registry.ts → program/registry.ts} +5 -1
  254. package/src/program/tsconfig.json +6 -0
  255. package/src/setup/steps/admin.ts +18 -3
  256. package/src/setup/steps/instance.ts +1 -1
  257. package/src/state/.spec/api.d.ts +133 -0
  258. package/src/state/.spec/architecture.md +31 -0
  259. package/src/state/.spec/capabilities/state.ts +7 -0
  260. package/src/state/.spec/flows/identity-update.ts +22 -0
  261. package/src/state/.spec/laws/state.ts +73 -0
  262. package/src/state/.spec/layout.ts +14 -0
  263. package/src/state/__tests__/exchange-credentials.test.ts +188 -0
  264. package/src/state/__tests__/files.test.ts +113 -0
  265. package/src/state/__tests__/fixtures/identity-transition.ts +37 -0
  266. package/src/state/__tests__/identities.test.ts +142 -0
  267. package/src/state/__tests__/paths.test.ts +27 -0
  268. package/src/state/exchange-credentials.ts +219 -0
  269. package/src/state/files.ts +129 -0
  270. package/src/state/identities.ts +236 -0
  271. package/src/state/index.ts +29 -0
  272. package/src/state/paths.ts +61 -0
  273. package/src/state/tsconfig.json +6 -0
  274. package/src/telemetry/__tests__/redact.test.ts +3 -3
  275. package/src/telemetry/__tests__/trigger.test.ts +1 -1
  276. package/src/telemetry/settings.ts +1 -1
  277. package/src/telemetry/store.ts +1 -1
  278. package/src/test-utils.ts +1 -1
  279. package/studio/client/dist/assets/elk-api-D0cBetPW.js +1 -0
  280. package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  281. package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
  282. package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
  283. package/studio/client/dist/assets/index-Dspir4w7.js +81 -0
  284. package/studio/client/dist/assets/index-bVD2KJgz.js +8 -0
  285. package/studio/client/dist/index.html +2 -2
  286. package/studio/package.json +10 -1
  287. package/studio/server/introspect/extractor.ts +1 -1
  288. package/studio/server/state/views.test.ts +3 -7
  289. package/studio/server/state/views.ts +23 -30
  290. package/studio/tsconfig.json +23 -0
  291. package/tsconfig.json +1 -1
  292. package/viewer/dist/main.js +65 -39
  293. package/viewer/tsconfig.json +13 -0
  294. package/src/command.ts +0 -42
  295. package/src/commands/__tests__/help-contract.test.ts +0 -164
  296. package/src/kernel/__tests__/client-owned-lookup.test.ts +0 -51
  297. package/src/kernel/__tests__/errors.test.ts +0 -43
  298. package/src/kernel/__tests__/expand.test.ts +0 -123
  299. package/src/kernel/client.ts +0 -214
  300. package/src/kernel/expand.ts +0 -192
  301. package/src/kernel/graph.ts +0 -96
  302. package/src/kernel/index.ts +0 -29
  303. package/src/kernel/options.ts +0 -22
  304. package/src/kernel/types.ts +0 -14
  305. package/src/lib/__tests__/domain-identity.test.ts +0 -60
  306. package/src/lib/__tests__/fs-atomic.test.ts +0 -104
  307. package/src/lib/__tests__/identity.test.ts +0 -79
  308. package/src/lib/__tests__/keys.test.ts +0 -129
  309. package/src/lib/domain-identity.ts +0 -49
  310. package/src/lib/env.ts +0 -49
  311. package/src/lib/fs-atomic.ts +0 -126
  312. package/src/lib/identity.ts +0 -256
  313. package/src/lib/keys.ts +0 -294
  314. package/src/lib/paths.ts +0 -11
  315. package/src/program.ts +0 -213
  316. package/studio/client/dist/assets/index-BaqEuIQJ.js +0 -109
  317. package/studio/client/dist/assets/index-jRdExahh.css +0 -1
@@ -1,18 +1,19 @@
1
+ import type { ResolvedView } from '@astrale-os/shell'
2
+
1
3
  import chalk from 'chalk'
2
4
  import { randomBytes } from 'node:crypto'
3
- import { closeSync, existsSync, openSync, statSync } from 'node:fs'
4
- import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
5
+ import { closeSync, existsSync, statSync } from 'node:fs'
6
+ import { readdir, readFile } from 'node:fs/promises'
5
7
  import { dirname, join } from 'node:path'
6
8
 
7
- import type { CommandDefinition } from '../command'
8
- import type { KernelCommandOpts } from '../kernel'
9
+ import type { KernelCommandOpts } from '../connection'
9
10
  import type { ViewServeConfig, ViewSessionRecord } from '../lib/view/session'
11
+ import type { CommandDefinition } from '../program/index'
10
12
 
13
+ import { expandSelfInPath, withClientSession } from '../connection'
11
14
  import { AstraleError } from '../errors'
12
- import { bindGraph, expandSelfInPath, resolveKernelTarget, withKernelClient } from '../kernel'
15
+ import { readIdentities } from '../identity/index'
13
16
  import { ab, AGENT_BROWSER_REPO, BROWSER_DIR, findAgentBrowser } from '../lib/browser'
14
- import { readConfig } from '../lib/config'
15
- import { readIdentities } from '../lib/identity'
16
17
  import { readInstances } from '../lib/instance'
17
18
  import { fatal, log } from '../lib/log'
18
19
  import { isMachine, output, type RawOutputOpts } from '../lib/output'
@@ -20,13 +21,13 @@ import { findFreePort } from '../lib/port'
20
21
  import { run, spawnHandle } from '../lib/proc'
21
22
  import { withViewPortAllocationLock } from '../lib/view/port-allocation'
22
23
  import {
23
- applyViewUrlOverride,
24
24
  candidateSlug,
25
25
  parseViewSpec,
26
26
  pickCandidate,
27
27
  resolveViewCandidates,
28
- rewriteLocalViewUrl,
28
+ selectedView,
29
29
  type ViewCandidate,
30
+ viewOwnerTarget,
30
31
  } from '../lib/view/resolve'
31
32
  import { ensureViewerAssets } from '../lib/view/server'
32
33
  import {
@@ -34,8 +35,9 @@ import {
34
35
  configPath,
35
36
  listSessions,
36
37
  logPath,
38
+ openSessionLog,
37
39
  saveRecord,
38
- VIEW_DIR,
40
+ saveServeConfig,
39
41
  } from '../lib/view/session'
40
42
  import { snapshotText, waitForSettledSnapshot } from '../lib/view/snapshot'
41
43
 
@@ -73,84 +75,57 @@ const VIEW_PROFILE = `${BROWSER_DIR}/_view`
73
75
 
74
76
  const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
75
77
 
76
- type ResolvedTarget = { id: string; path: string }
77
- type ResolvedView = {
78
- url: string
79
- functionId: string
80
- handshake: 'shell' | 'none'
81
- path?: string
82
- name?: string
83
- }
84
-
85
78
  export async function resolveSession(
86
- spec: string | undefined,
79
+ spec: string,
87
80
  opts: ViewOpts,
88
- ): Promise<{ view?: ResolvedView; target?: ResolvedTarget; candidates: ViewCandidate[] }> {
89
- const parsed = spec ? parseViewSpec(spec) : undefined
90
- if (parsed?.kind === 'target' && opts.target) {
81
+ ): Promise<{ view?: ResolvedView; candidates: ViewCandidate[] }> {
82
+ rejectUnrepresentableOverrides(opts)
83
+ const parsed = parseViewSpec(spec)
84
+ if (parsed.kind === 'target' && opts.target) {
91
85
  fatal(new Error('Pass the target either as the positional or as --target, not both'))
92
86
  }
93
- const targetInput = parsed?.kind === 'target' ? parsed.path : opts.target
94
-
95
- const resolved = await withKernelClient(opts, async (ctx) => {
96
- let target: ResolvedTarget | undefined
97
- if (targetInput) {
98
- const { path } = await expandSelfInPath(targetInput, opts)
99
- const node = (await bindGraph(ctx).get(path)) as { id?: string } | null
100
- if (!node?.id) {
101
- throw new AstraleError('NOT_FOUND', `target ${path} not found or not visible`)
102
- }
103
- target = { id: node.id, path }
104
- }
105
- // A bare --view-url already identifies the frontend. Its target is only
106
- // shell context, so asking View:resolve for installed candidates is both
107
- // unnecessary and incorrect for classes without a class-owned self view.
108
- const anchor = parsed ? (parsed.kind === 'view' ? parsed.path : target?.path) : undefined
109
- const candidates = anchor ? await resolveViewCandidates(ctx, anchor) : []
110
- return { target, candidates }
111
- })
112
-
113
- // Bare --view-url: mount an arbitrary URL as a view (nothing installed yet).
114
- if (!parsed) {
115
- return {
116
- view: {
117
- url: rewriteLocalViewUrl(opts.viewUrl!),
118
- functionId: 'dev-view',
119
- handshake: opts.handshake ?? 'shell',
120
- name: 'dev',
121
- },
122
- target: resolved.target,
123
- candidates: [],
124
- }
87
+ if (parsed.kind === 'view' && opts.view) {
88
+ fatal(new Error('An explicit ViewPath cannot be combined with --view <slug>'))
125
89
  }
90
+ const targetInput =
91
+ 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))
126
95
 
127
- const anchor = parsed.kind === 'view' ? parsed.path : resolved.target!.path
128
96
  if (opts.list) {
129
- return { target: resolved.target, candidates: resolved.candidates }
97
+ return { candidates }
130
98
  }
131
- const picked = await chooseCandidate(resolved.candidates, anchor, opts)
132
- let url = picked.url
133
- if (opts.viewUrl) url = applyViewUrlOverride(url, opts.viewUrl)
134
- url = rewriteLocalViewUrl(url)
99
+ const selector = parsed.kind === 'view' ? parsed.path : opts.view
100
+ const picked = await chooseCandidate(candidates, target, selector, opts)
135
101
  return {
136
- view: {
137
- url,
138
- functionId: picked.id,
139
- handshake: opts.handshake ?? picked.handshake ?? 'shell',
140
- path: picked.path,
141
- name: candidateSlug(picked),
142
- },
143
- target: resolved.target,
144
- candidates: resolved.candidates,
102
+ view: selectedView(picked),
103
+ candidates,
145
104
  }
146
105
  }
147
106
 
107
+ /**
108
+ * Retain the frozen command flags while failing closed: neither legacy flag
109
+ * has a truthful representation in V2's verified View placement contract.
110
+ */
111
+ export function rejectUnrepresentableOverrides(
112
+ opts: Pick<ViewOpts, 'handshake' | 'viewUrl'>,
113
+ ): void {
114
+ if (opts.viewUrl === undefined && opts.handshake === undefined) return
115
+ throw new AstraleError(
116
+ 'UNSUPPORTED_VIEW_OVERRIDE',
117
+ '--view-url and --handshake cannot override a V2 View: the Shell mounts one verified View placement with complete provenance.',
118
+ 'Pass a ViewPath or target and use the placement published by its Domain.',
119
+ )
120
+ }
121
+
148
122
  async function chooseCandidate(
149
123
  candidates: ViewCandidate[],
150
124
  anchor: string,
125
+ selector: string | undefined,
151
126
  opts: ViewOpts,
152
127
  ): Promise<ViewCandidate> {
153
- const picked = pickCandidate(candidates, anchor, opts.view)
128
+ const picked = pickCandidate(candidates, anchor, selector)
154
129
  if (picked !== 'ambiguous') return picked
155
130
  if (process.stdin.isTTY && !isMachine(opts)) {
156
131
  const { select } = await import('@inquirer/prompts')
@@ -225,29 +200,19 @@ async function newerThan(dir: string, mtimeMs: number): Promise<boolean> {
225
200
  }
226
201
 
227
202
  /** Spawn the detached session server (the CLI re-invoking itself) and wait for it. */
228
- async function startSession(
229
- view: ResolvedView,
230
- target: ResolvedTarget | undefined,
231
- opts: ViewOpts,
232
- ): Promise<ViewSessionRecord> {
203
+ async function startSession(view: ResolvedView, opts: ViewOpts): Promise<ViewSessionRecord> {
233
204
  await ensureViewerAssets()
234
- const config = await readConfig()
235
- const kernelTarget = await resolveKernelTarget(opts, config)
205
+ const kernelTarget = await withClientSession(
206
+ opts,
207
+ async ({ target: connectionTarget }) => connectionTarget,
208
+ )
236
209
  const [instances, identities, runtime] = await Promise.all([
237
210
  readInstances(),
238
211
  readIdentities(),
239
212
  resolveServeRuntime(),
240
213
  ])
241
214
  return withViewPortAllocationLock(() =>
242
- startSessionLocked(
243
- view,
244
- target,
245
- opts,
246
- kernelTarget,
247
- instances.active,
248
- identities.default,
249
- runtime,
250
- ),
215
+ startSessionLocked(view, opts, kernelTarget, instances.active, identities.default, runtime),
251
216
  )
252
217
  }
253
218
 
@@ -258,9 +223,8 @@ async function startSession(
258
223
  */
259
224
  async function startSessionLocked(
260
225
  view: ResolvedView,
261
- target: ResolvedTarget | undefined,
262
226
  opts: ViewOpts,
263
- kernelTarget: { url: string; caFile?: string },
227
+ kernelTarget: { url: string; kernelIssuer: string; caFile?: string },
264
228
  activeInstance: string | undefined,
265
229
  defaultIdentity: string | undefined,
266
230
  runtime: { file: string; args: string[] },
@@ -281,7 +245,6 @@ async function startSessionLocked(
281
245
  nonce,
282
246
  pageUrl: `http://127.0.0.1:${port}/s/${nonce}/`,
283
247
  view,
284
- target,
285
248
  instance: opts.instance ?? (opts.url ? opts.url : activeInstance),
286
249
  identity: opts.creds ? '(pre-signed creds)' : (opts.as ?? defaultIdentity),
287
250
  createdAt: new Date().toISOString(),
@@ -297,15 +260,15 @@ async function startSessionLocked(
297
260
  },
298
261
  proxy: {
299
262
  kernelUrl: kernelTarget.url,
263
+ issuer: kernelTarget.kernelIssuer,
300
264
  caFile: kernelTarget.caFile,
301
265
  direct: isPublicHttps(kernelTarget.url) && !kernelTarget.caFile,
302
266
  },
303
267
  idleMs: IDLE_MS,
304
268
  }
305
269
 
306
- await mkdir(VIEW_DIR, { recursive: true })
307
- await writeFile(configPath(id), JSON.stringify(serveConfig, null, 2))
308
- const logFd = openSync(logPath(id), 'a')
270
+ await saveServeConfig(serveConfig)
271
+ const logFd = await openSessionLog(id)
309
272
  const child = spawnHandle(
310
273
  runtime.file,
311
274
  [...runtime.args, '__view-serve', '--config', configPath(id)],
@@ -422,9 +385,9 @@ async function reportOpened(
422
385
  )
423
386
  return
424
387
  }
425
- const label = record.view.path ?? record.view.url
388
+ const label = `/:${record.view.route.key}`
426
389
  log.success(`View session ${chalk.bold(record.id)} — ${chalk.bold(label)}`)
427
- if (record.target) log.dim(` target ${record.target.path} (${record.target.id})`)
390
+ log.dim(` target ${record.view.target}`)
428
391
  log.dim(
429
392
  ` identity ${record.identity ?? '(default)'} instance ${record.instance ?? '(active)'}`,
430
393
  )
@@ -479,7 +442,7 @@ async function closeCommand(opts: ViewOpts): Promise<void> {
479
442
  return fatal(
480
443
  new Error(
481
444
  `${sessions.length} sessions open — pass --close <id> or --close --all:\n${sessions
482
- .map((s) => ` ${s.id} ${s.view.path ?? s.view.url}`)
445
+ .map((s) => ` ${s.id} /:${s.view.route.key}`)
483
446
  .join('\n')}`,
484
447
  ),
485
448
  )
@@ -502,7 +465,7 @@ async function sessionsCommand(opts: ViewOpts): Promise<void> {
502
465
  }
503
466
  for (const s of sessions) {
504
467
  console.log(
505
- `${chalk.bold(s.id)} ${s.view.path ?? s.view.url}${s.target ? ` target ${s.target.path}` : ''} ${chalk.dim(s.pageUrl)}`,
468
+ `${chalk.bold(s.id)} /:${s.view.route.key} target ${s.view.target} ${chalk.dim(s.pageUrl)}`,
506
469
  )
507
470
  }
508
471
  }
@@ -555,27 +518,22 @@ What it does:
555
518
  agent-browser's job; auth follows --as/--creds/-i like any kernel command.
556
519
 
557
520
  A session stays up ~30 min idle (heartbeat while the page is open). The view
558
- gets exactly what the GUI would hand it: a delegation token, a kernel URL,
559
- and your target node id.
521
+ gets exactly what the GUI would hand it: one target-bound resolved placement,
522
+ an audience-bound credential for shell mounts, and the kernel endpoint.
560
523
 
561
524
  Examples:
562
525
  $ astrale view /crm/customers/ada # views on a node
563
526
  $ astrale view /:crm.acme.dev:view.dashboard # explicit ViewPath
564
527
  $ astrale view /:agents.astrale.ai:view.agent --target @f00d1234 --as alice
565
528
  $ astrale view /crm/customers/ada --snapshot # open + show it
566
- $ astrale view /:d:view.x --view-url http://localhost:8787 # local frontend, live data
567
- $ astrale view --view-url http://localhost:8787/ui/x --handshake shell --target /a/b
568
529
  $ astrale view --sessions ; astrale view --close --all
569
530
  `,
570
531
  action: async (spec: string | undefined, opts: ViewOpts) => {
571
532
  if (opts.close !== undefined) return closeCommand(opts)
572
533
  if (opts.sessions) return sessionsCommand(opts)
573
534
 
574
- if (!spec && !opts.viewUrl) {
575
- return fatal(
576
- new Error('Nothing to open — pass a ViewPath or target node, or --view-url <url>.'),
577
- )
578
- }
535
+ rejectUnrepresentableOverrides(opts)
536
+ if (!spec) return fatal(new Error('Nothing to open — pass a ViewPath or target node.'))
579
537
  const wantsAgentBrowser = !opts.browser && opts.open !== false
580
538
  if ((opts.snapshot || opts.screenshot) && !wantsAgentBrowser) {
581
539
  return fatal(
@@ -590,14 +548,14 @@ Examples:
590
548
  process.exit(1)
591
549
  }
592
550
 
593
- const { view, target, candidates } = await resolveSession(spec, opts)
551
+ const { view, candidates } = await resolveSession(spec, opts)
594
552
  if (opts.list) {
595
553
  if (isMachine(opts)) output(candidates, opts)
596
554
  else if (candidates.length === 0) log.dim('No views resolve here.')
597
555
  else {
598
556
  for (const c of candidates) {
599
557
  console.log(
600
- `${chalk.bold(candidateSlug(c))} ${c.handshake ?? 'shell'} ${c.origin} ${chalk.dim(c.url)} ${c.path}`,
558
+ `${chalk.bold(candidateSlug(c))} ${c.handshake} ${c.origin} ${chalk.dim(c.url)} ${c.path}`,
601
559
  )
602
560
  }
603
561
  }
@@ -605,7 +563,7 @@ Examples:
605
563
  }
606
564
  if (!view) throw new Error('View resolution completed without a selected view')
607
565
 
608
- const record = await startSession(view, target, opts)
566
+ const record = await startSession(view, opts)
609
567
 
610
568
  let mode: 'agent' | 'system' | 'none' = 'none'
611
569
  if (wantsAgentBrowser) {
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import * as compatibility from './connect-core'
4
+
5
+ describe('connect-core compatibility barrel', () => {
6
+ test('retains the functions borrowed by connect-host', () => {
7
+ for (const value of [
8
+ compatibility.readIdentities,
9
+ compatibility.getDefault,
10
+ compatibility.getIdentity,
11
+ compatibility.signAs,
12
+ compatibility.listIdentityKeys,
13
+ compatibility.readInstances,
14
+ compatibility.resetInstancesMemo,
15
+ compatibility.resolveInstance,
16
+ compatibility.normalizeInstanceKernelUrl,
17
+ compatibility.orgIdForAudience,
18
+ compatibility.resolveInstanceTarget,
19
+ compatibility.readConfig,
20
+ compatibility.resolveCredential,
21
+ compatibility.fetchWithCaFile,
22
+ compatibility.createPaths,
23
+ compatibility.loginViaIdp,
24
+ compatibility.resolveIdpName,
25
+ compatibility.readIdpSession,
26
+ compatibility.isSessionExpired,
27
+ ]) {
28
+ expect(value).toBeTypeOf('function')
29
+ }
30
+ })
31
+
32
+ test('retains typed errors and the captured paths singleton', () => {
33
+ expect(compatibility.AuthError).toBeTypeOf('function')
34
+ expect(compatibility.IdpRefreshTransientError).toBeTypeOf('function')
35
+ expect(compatibility.IdpOrgMembershipError).toBeTypeOf('function')
36
+ expect(compatibility.paths.home).toBeTypeOf('string')
37
+ })
38
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Temporary compatibility surface for the observed `@astrale-os/connect-host`
3
+ * consumer. Keep this file as re-exports only: CLI state and connection owners
4
+ * remain authoritative, and no Kernel Client implementation type is exposed.
5
+ */
6
+
7
+ export { getDefault, getIdentity, readIdentities } from './identity/registry'
8
+ export type { Identity, IdentityStore } from './identity/registry'
9
+
10
+ export { listIdentityKeys, signAs } from './keys/index'
11
+
12
+ export {
13
+ normalizeInstanceKernelUrl,
14
+ orgIdForAudience,
15
+ readInstances,
16
+ resetInstancesMemo,
17
+ resolveInstance,
18
+ } from './lib/instance'
19
+ export type { InstanceEntry, InstanceStore, ResolvedInstance } from './lib/instance'
20
+
21
+ export { resolveInstanceTarget } from './lib/instance-target'
22
+ export type { ResolvedInstanceTarget } from './lib/instance-target'
23
+
24
+ export { readConfig } from './lib/config'
25
+ export type { AstraleConfig } from './lib/config'
26
+
27
+ export {
28
+ IdpOrgMembershipError,
29
+ IdpRefreshTransientError,
30
+ resolveCredential,
31
+ } from './connection/auth'
32
+
33
+ export { loginViaIdp, resolveIdpName } from './lib/login-flow'
34
+ export type { DeviceVerification, LoginFlowOpts, LoginResult } from './lib/login-flow'
35
+
36
+ export { isSessionExpired, readIdpSession } from './lib/idp'
37
+ export type { IdpSession } from './lib/idp'
38
+
39
+ export { AuthError } from './errors'
40
+ export { fetchWithCaFile } from './lib/ca-fetch'
41
+
42
+ export { createPaths, paths } from './state/index'
@@ -0,0 +1,111 @@
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
+ import type { IssuerId } from '@astrale-os/sdk/auth'
6
+
7
+ /** Existing CLI connection flags accepted by Kernel-touching commands. */
8
+ export interface ConnectionOptions {
9
+ readonly url?: string
10
+ readonly instance?: string
11
+ readonly timeout?: string
12
+ readonly as?: string
13
+ readonly creds?: string
14
+ /** Omit caller credentials even when local or bookmark defaults exist. */
15
+ readonly anonymous?: boolean
16
+ }
17
+
18
+ /** Existing Admin-target overrides accepted only by Admin Domain operations. */
19
+ export interface AdminConnectionOptions extends ConnectionOptions {
20
+ readonly admin?: string
21
+ readonly adminUrl?: string
22
+ readonly domainIssuer?: string
23
+ }
24
+
25
+ /** Exact source Kernel selected from flags and local CLI state. */
26
+ export interface ConnectionTarget {
27
+ readonly url: string
28
+ readonly issuer: IssuerId
29
+ /** Installed Domain issuer; presence activates standard token exchange. */
30
+ readonly domainIssuer?: IssuerId
31
+ readonly slug?: string
32
+ readonly defaultIdentity?: string
33
+ readonly caFile?: string
34
+ }
35
+
36
+ /** Stable local identity-registration key for the exact selected source Kernel. */
37
+ export function registrationKeyForTarget(target: ConnectionTarget): string
38
+
39
+ /** Narrow capabilities available during one scoped CLI connection. */
40
+ export interface ConnectionContext {
41
+ readonly session: ClientSession
42
+ readonly graph: GraphApi
43
+ readonly auth: AuthApi
44
+ readonly target: ConnectionTarget
45
+ }
46
+
47
+ /** Existing output and diagnostic flags shared by commands that open a Kernel connection. */
48
+ export interface KernelCommandOpts extends ConnectionOptions {
49
+ readonly raw?: boolean
50
+ readonly json?: boolean
51
+ readonly format?: 'yaml' | 'json'
52
+ readonly debug?: boolean
53
+ }
54
+
55
+ /** Detached metadata used only to improve an error after a caller-authored @self expansion. */
56
+ export interface SelfExpansionMeta {
57
+ readonly original: string
58
+ readonly expanded: string
59
+ readonly selfId: string
60
+ readonly slug?: string
61
+ }
62
+
63
+ /** Parse caller-authored path text and retain one portable input in the public Call shape. */
64
+ export function createPathCall(path: string, input: unknown): Call
65
+
66
+ /** Resolve one ordinary CLI target, run an action, and close every owned Client resource. */
67
+ export function withClientSession<Value>(
68
+ options: ConnectionOptions,
69
+ action: (context: ConnectionContext) => Promise<Value>,
70
+ ): Promise<Value>
71
+
72
+ /** Resolve the configured Admin Domain target under the same scoped lifecycle. */
73
+ export function withAdminClientSession<Value>(
74
+ options: AdminConnectionOptions,
75
+ action: (context: ConnectionContext) => Promise<Value>,
76
+ ): Promise<Value>
77
+
78
+ /** Expand @self through the effective principal returned by authenticated Identity.whoami. */
79
+ export function expandSelfInPath(
80
+ path: string,
81
+ options: KernelCommandOpts,
82
+ ): Promise<{ readonly path: string; readonly meta?: SelfExpansionMeta }>
83
+
84
+ /** Expand @self once across one Call path and its CLI-authored string parameters. */
85
+ export function expandSelfInCall(
86
+ path: string,
87
+ parameters: readonly string[],
88
+ options: KernelCommandOpts,
89
+ ): Promise<{
90
+ readonly path: string
91
+ readonly parameters: readonly string[]
92
+ readonly meta?: SelfExpansionMeta
93
+ }>
94
+
95
+ /** Preserve stale-registration evidence while an expanded request is executed. */
96
+ export function withSelfHint<Value>(
97
+ action: () => Promise<Value>,
98
+ meta: SelfExpansionMeta | undefined,
99
+ ): Promise<Value>
100
+
101
+ /** Run one command through the canonical progress, connection, presentation, and error boundary. */
102
+ export function runKernelCommand<Value>(input: {
103
+ readonly opts: KernelCommandOpts
104
+ readonly label: string
105
+ readonly fn: (context: ConnectionContext) => Promise<Value>
106
+ readonly format?: (
107
+ result: Value,
108
+ options: KernelCommandOpts,
109
+ machine: boolean,
110
+ ) => void | Promise<void>
111
+ }): Promise<void>
@@ -0,0 +1,41 @@
1
+ # CLI connection architecture
2
+
3
+ The private `connection` module owns one scoped bridge from existing CLI flags and local identity
4
+ state to the public Kernel Client. It selects the exact source Kernel URL and issuer, either omits
5
+ credentials for an explicit anonymous selection or resolves fresh source-Kernel authority for each
6
+ Call, binds Graph and Auth helpers, and closes every owned Client resource when the command action
7
+ terminates.
8
+
9
+ When a target also names an exact Domain issuer, the connection owner performs the client-mediated
10
+ `whoami -> delegate(attenuation) -> issuer exchange` journey. The resulting Domain token crosses
11
+ into ClientSession as an opaque credential; connection does not authorize Domain operations.
12
+
13
+ ```mermaid
14
+ flowchart LR
15
+ F[CLI flags and local state] --> T[Connection target]
16
+ T --> H[ClientSession]
17
+ C[CLI credential sources] --> R[SessionAuth]
18
+ R --> H
19
+ H --> G[GraphApi]
20
+ H --> A[AuthApi]
21
+ H --> X[Command action]
22
+ G --> X
23
+ A --> X
24
+ ```
25
+
26
+ ClientSession owns source admission, Publication discovery, redirect witnessing, route caching, transport
27
+ selection, and the single safe stale-route recovery. The CLI does not reproduce those rules or
28
+ discover destination identity. `SessionAuth.resolve(call, signal)` returns source-Kernel authority and
29
+ ClientSession supplies an audience-free Delegate with omitted attenuation, preserving the exact
30
+ current Grant for routing. Connection itself persists no credential or route; the separate state
31
+ owner persists only exchanged source credentials.
32
+
33
+ `--anonymous` deliberately suppresses ambient and bookmark-default identities by omitting the
34
+ `SessionAuth` capability. It cannot be combined with `--as` or `--creds`; contradictory selections fail
35
+ before local identity state or a connection is opened. Public and optional callables can then
36
+ observe a genuinely anonymous caller, while required callables reject the request at the Kernel.
37
+
38
+ The target, timeout, and optional CA file are resolved before constructing the session. The CA file
39
+ customizes only the Fetch capability passed to Client. `withClientSession` and
40
+ `withAdminClientSession` are terminal lifecycle boundaries: success, failure, and cancellation all
41
+ close both the Client Session and its direct source-Auth client.
@@ -0,0 +1,13 @@
1
+ import { defineCapability } from '@astrale-os/spec/authoring'
2
+
3
+ export const CLI_CONNECTION = defineCapability({
4
+ id: 'CLI-CONNECTION',
5
+ statement:
6
+ 'Selects one exact source Kernel and exposes scoped public Host, Graph, and Auth capabilities with terminal cleanup.',
7
+ })
8
+
9
+ export const CLI_AUTH = defineCapability({
10
+ id: 'CLI-AUTH',
11
+ statement:
12
+ 'Pins the selected source issuer, explicitly admits either credential-less or target-bound authenticated sessions, and delegates remote destinations without forwarding a prior bearer.',
13
+ })
@@ -0,0 +1,7 @@
1
+ import { createPathCall, withClientSession } from '../api.js'
2
+
3
+ const result = await withClientSession({ instance: 'staging', as: 'alice' }, ({ session }) =>
4
+ session.call(createPathCall('/:notes.example.dev:function.search', { text: 'astrale' })),
5
+ )
6
+
7
+ console.log(result)
@@ -0,0 +1,20 @@
1
+ import type { ConnectionContext, KernelCommandOpts } from '../api.js'
2
+
3
+ declare const open: <Value>(
4
+ options: KernelCommandOpts,
5
+ action: (context: ConnectionContext) => Promise<Value>,
6
+ ) => Promise<Value>
7
+ declare const present: <Value>(value: Value) => void | Promise<void>
8
+ declare const mapExpectedFailure: (error: unknown) => Promise<void>
9
+
10
+ /** One command owns progress and error presentation around one terminal ClientSession lifecycle. */
11
+ export async function runKernelCommand<Value>(
12
+ options: KernelCommandOpts,
13
+ action: (context: ConnectionContext) => Promise<Value>,
14
+ ): Promise<void> {
15
+ try {
16
+ await present(await open(options, action))
17
+ } catch (error) {
18
+ await mapExpectedFailure(error)
19
+ }
20
+ }
@@ -0,0 +1,61 @@
1
+ import type { SessionAuth } from '@astrale-os/kernel-client/session'
2
+ import type { Call } from '@astrale-os/kernel-protocol/invocation'
3
+
4
+ import type { ConnectionContext, ConnectionOptions, ConnectionTarget } from '../api.js'
5
+
6
+ interface OwnedConnection {
7
+ readonly context: ConnectionContext
8
+ close(): void
9
+ }
10
+
11
+ declare const resolveTarget: (options: ConnectionOptions) => Promise<ConnectionTarget>
12
+ declare const resolveTimeoutMs: (input: string | undefined) => number
13
+ declare const createClientConnection: (
14
+ target: ConnectionTarget,
15
+ timeoutMs: number,
16
+ auth: SessionAuth | undefined,
17
+ ) => OwnedConnection
18
+ declare const validateCredentialSelection: (options: ConnectionOptions) => void
19
+ declare const resolveSourceCredential: (
20
+ target: ConnectionTarget,
21
+ options: ConnectionOptions,
22
+ audience: string,
23
+ signal: AbortSignal,
24
+ ) => Promise<string>
25
+ /** Bind source authority without learning or minting destination credentials. */
26
+ function createConnectionAuth(target: ConnectionTarget, options: ConnectionOptions): SessionAuth {
27
+ return {
28
+ ttlSeconds: 3_600,
29
+ async resolve(_call: Call, signal: AbortSignal) {
30
+ return {
31
+ credential: await resolveSourceCredential(target, options, target.issuer, signal),
32
+ }
33
+ },
34
+ }
35
+ }
36
+
37
+ /** Open one Client Session with a call-scoped source-authority resolver bound once. */
38
+ function openConnection(
39
+ target: ConnectionTarget,
40
+ timeoutMs: number,
41
+ options: ConnectionOptions,
42
+ ): OwnedConnection {
43
+ const auth = options.anonymous === true ? undefined : createConnectionAuth(target, options)
44
+ return createClientConnection(target, timeoutMs, auth)
45
+ }
46
+
47
+ /** One terminal command-scoped connection lifecycle. */
48
+ export async function withClientSession<Value>(
49
+ options: ConnectionOptions,
50
+ action: (context: ConnectionContext) => Promise<Value>,
51
+ ): Promise<Value> {
52
+ validateCredentialSelection(options)
53
+ const timeoutMs = resolveTimeoutMs(options.timeout)
54
+ const target = await resolveTarget(options)
55
+ const connection = openConnection(target, timeoutMs, options)
56
+ try {
57
+ return await action(connection.context)
58
+ } finally {
59
+ connection.close()
60
+ }
61
+ }