@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.11

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 (329) hide show
  1. package/README.md +23 -12
  2. package/dist/astrale.js +48048 -56798
  3. package/dist/public/connect-core.js +2011 -30765
  4. package/dist/public/keys/index.js +884 -29483
  5. package/dist/public/paths/index.js +13101 -41459
  6. package/dist/types/admin/binding.d.ts +19 -0
  7. package/dist/types/admin/instance/client.d.ts +22 -0
  8. package/dist/types/admin/instance/index.d.ts +2 -0
  9. package/dist/types/admin/instance/model.d.ts +5 -12
  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 +7 -59
  29. package/dist/types/lib/failure-debug.d.ts +1 -0
  30. package/dist/types/lib/idp.d.ts +26 -168
  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/meta.d.ts +2 -2
  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 +91 -0
  38. package/dist/types/lib/validation.d.ts +4 -1
  39. package/package.json +16 -19
  40. package/src/__tests__/fixtures/publication.ts +20 -0
  41. package/src/admin/__tests__/binding.test.ts +31 -0
  42. package/src/admin/__tests__/fixture.ts +101 -0
  43. package/src/admin/binding.ts +98 -0
  44. package/src/admin/catalog/.spec/api.d.ts +2 -2
  45. package/src/admin/catalog/__tests__/client.test.ts +20 -49
  46. package/src/admin/catalog/client.ts +36 -32
  47. package/src/admin/catalog/model.ts +3 -4
  48. package/src/admin/graph/.spec/api.d.ts +4 -10
  49. package/src/admin/graph/nodes.ts +1 -1
  50. package/src/admin/instance/.spec/api.d.ts +2 -2
  51. package/src/admin/instance/__tests__/client.test.ts +39 -164
  52. package/src/admin/instance/client.ts +35 -124
  53. package/src/admin/instance/index.ts +1 -2
  54. package/src/admin/instance/model.ts +11 -20
  55. package/src/commands/__tests__/admin-instance.test.ts +11 -8
  56. package/src/commands/__tests__/auth-login.test.ts +54 -1
  57. package/src/commands/__tests__/call-describe.test.ts +46 -33
  58. package/src/commands/__tests__/call.test.ts +5 -1
  59. package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
  60. package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
  61. package/src/commands/__tests__/domain-list.test.ts +63 -4
  62. package/src/commands/__tests__/install-identity-override.test.ts +19 -19
  63. package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
  64. package/src/commands/__tests__/instance-status.test.ts +139 -0
  65. package/src/commands/__tests__/read-commands.test.ts +35 -10
  66. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  67. package/src/commands/__tests__/update.test.ts +48 -19
  68. package/src/commands/call-describe.ts +60 -68
  69. package/src/commands/call.ts +43 -56
  70. package/src/commands/domain/install.ts +13 -17
  71. package/src/commands/domain/list.ts +18 -13
  72. package/src/commands/domain/publish.ts +14 -11
  73. package/src/commands/domain/uninstall.ts +2 -2
  74. package/src/commands/get.ts +14 -15
  75. package/src/commands/identity/__tests__/register.test.ts +3 -3
  76. package/src/commands/identity/register.ts +5 -4
  77. package/src/commands/instance/create.ts +8 -16
  78. package/src/commands/instance/delete.ts +12 -9
  79. package/src/commands/instance/list.ts +2 -2
  80. package/src/commands/instance/status.ts +66 -20
  81. package/src/commands/instance/use.ts +16 -12
  82. package/src/commands/introspect.ts +9 -16
  83. package/src/commands/logs.ts +24 -25
  84. package/src/commands/mutate.ts +6 -5
  85. package/src/commands/query.ts +27 -30
  86. package/src/commands/studio.ts +20 -0
  87. package/src/commands/token.ts +8 -7
  88. package/src/commands/update.ts +38 -14
  89. package/src/commands/view.ts +7 -6
  90. package/src/connection/.spec/api.d.ts +7 -8
  91. package/src/connection/.spec/architecture.md +5 -4
  92. package/src/connection/.spec/flows/session.ts +2 -2
  93. package/src/connection/.spec/laws/connection.ts +6 -2
  94. package/src/connection/.spec/layout.ts +1 -0
  95. package/src/connection/__tests__/credential.test.ts +20 -1
  96. package/src/connection/__tests__/errors.test.ts +43 -32
  97. package/src/connection/__tests__/exchange.test.ts +36 -6
  98. package/src/connection/__tests__/failure-fixtures.ts +39 -0
  99. package/src/connection/__tests__/failure-safety.test.ts +75 -0
  100. package/src/connection/__tests__/self.test.ts +31 -11
  101. package/src/connection/__tests__/session.test.ts +3 -3
  102. package/src/connection/__tests__/target.test.ts +1 -0
  103. package/src/connection/auth.ts +13 -0
  104. package/src/connection/call.ts +1 -1
  105. package/src/connection/credential.ts +2 -2
  106. package/src/connection/errors.ts +1 -355
  107. package/src/connection/exchange.ts +52 -27
  108. package/src/connection/failure/classify.ts +110 -0
  109. package/src/connection/failure/index.ts +18 -0
  110. package/src/connection/failure/model.ts +28 -0
  111. package/src/connection/failure/render.ts +116 -0
  112. package/src/connection/reasons.ts +16 -0
  113. package/src/connection/self.ts +39 -21
  114. package/src/connection/session.ts +19 -8
  115. package/src/errors.ts +3 -2
  116. package/src/graph/.spec/api.d.ts +12 -5
  117. package/src/graph/.spec/layout.ts +9 -1
  118. package/src/graph/__tests__/mutation.test.ts +2 -2
  119. package/src/graph/__tests__/query.test.ts +25 -6
  120. package/src/graph/class.ts +30 -0
  121. package/src/graph/index.ts +1 -0
  122. package/src/graph/query.ts +21 -32
  123. package/src/identity/.spec/api.d.ts +2 -2
  124. package/src/identity/__tests__/registration.test.ts +1 -1
  125. package/src/identity/registration.ts +3 -3
  126. package/src/keys/__tests__/keys.test.ts +4 -0
  127. package/src/keys/credential.ts +1 -0
  128. package/src/lib/__tests__/admin-target.test.ts +12 -0
  129. package/src/lib/__tests__/command-dx.test.ts +1 -1
  130. package/src/lib/__tests__/domain-publication.test.ts +138 -0
  131. package/src/lib/__tests__/idp-session.test.ts +22 -0
  132. package/src/lib/__tests__/idp.test.ts +36 -4
  133. package/src/lib/__tests__/instance-candidates.test.ts +1 -3
  134. package/src/lib/__tests__/instance-target.test.ts +7 -39
  135. package/src/lib/__tests__/instance.test.ts +6 -0
  136. package/src/lib/__tests__/log-errors.test.ts +64 -0
  137. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  138. package/src/lib/__tests__/update.test.ts +113 -8
  139. package/src/lib/__tests__/view-open-intent.test.ts +0 -2
  140. package/src/lib/__tests__/view-server.test.ts +18 -3
  141. package/src/lib/__tests__/view-session.test.ts +1 -1
  142. package/src/lib/admin-instance.ts +14 -8
  143. package/src/lib/admin-target.ts +21 -2
  144. package/src/lib/command-dx.ts +9 -9
  145. package/src/lib/domain-publication.ts +104 -0
  146. package/src/lib/failure-debug.ts +26 -0
  147. package/src/lib/idp.ts +39 -4
  148. package/src/lib/instance-candidates.ts +2 -2
  149. package/src/lib/instance-target.ts +3 -14
  150. package/src/lib/instance.ts +22 -12
  151. package/src/lib/log.ts +16 -9
  152. package/src/lib/login-flow.ts +13 -3
  153. package/src/lib/meta.ts +2 -2
  154. package/src/lib/provision-instance.ts +34 -57
  155. package/src/lib/update.ts +124 -30
  156. package/src/lib/validation.ts +2 -2
  157. package/src/lib/view/resolve.ts +1 -1
  158. package/src/lib/view/server.ts +17 -3
  159. package/src/program/__tests__/program.test.ts +20 -4
  160. package/src/program/build.ts +2 -2
  161. package/src/setup/__tests__/instance-step.test.ts +27 -10
  162. package/src/setup/steps/instance.ts +22 -15
  163. package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
  164. package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
  165. package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
  166. package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
  167. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
  168. package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
  169. package/studio/client/dist/index.html +3 -2
  170. package/studio/package.json +10 -14
  171. package/studio/server/agent/ask.test.ts +1 -1
  172. package/studio/server/agent/bridge/grant.test.ts +1 -1
  173. package/studio/server/agent/bridge/routes.test.ts +1 -1
  174. package/studio/server/agent/bridge/stdio.ts +8 -1
  175. package/studio/server/agent/conversation.test.ts +10 -5
  176. package/studio/server/agent/conversation.ts +56 -8
  177. package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
  178. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  179. package/studio/server/agent/harness/gateway/config.ts +20 -18
  180. package/studio/server/agent/harness/selection.ts +9 -1
  181. package/studio/server/agent/prompts/anchors.test.ts +32 -36
  182. package/studio/server/agent/prompts/anchors.ts +20 -49
  183. package/studio/server/agent/prompts/system.test.ts +3 -2
  184. package/studio/server/agent/prompts/system.ts +6 -5
  185. package/studio/server/agent/prompts/turn.ts +13 -4
  186. package/studio/server/agent/routes.test.ts +1 -1
  187. package/studio/server/agent/run/completion.ts +2 -2
  188. package/studio/server/agent/run/coordinator.test.ts +1 -1
  189. package/studio/server/agent/run/preparation.ts +6 -5
  190. package/studio/server/agent/run/transcript.test.ts +35 -1
  191. package/studio/server/agent/run/transcript.ts +169 -3
  192. package/studio/server/agent/run/usage.test.ts +24 -0
  193. package/studio/server/agent/run/usage.ts +26 -1
  194. package/studio/server/api/canvas.ts +43 -0
  195. package/studio/server/api/comments.ts +85 -0
  196. package/studio/server/api/context.ts +61 -0
  197. package/studio/server/api/deployment.ts +39 -0
  198. package/studio/server/api/documents.ts +55 -0
  199. package/studio/server/api/domain.ts +59 -0
  200. package/studio/server/api/http.ts +45 -0
  201. package/studio/server/api/project.ts +57 -0
  202. package/studio/server/api/schema.ts +12 -0
  203. package/studio/server/api/views.ts +57 -0
  204. package/studio/server/api/workspace.ts +60 -0
  205. package/studio/server/api-agent-loadout.test.ts +1 -1
  206. package/studio/server/api.test.ts +147 -0
  207. package/studio/server/api.ts +20 -422
  208. package/studio/server/cache.test.ts +122 -0
  209. package/studio/server/cache.ts +162 -11
  210. package/studio/server/cli-consumers.test.ts +494 -0
  211. package/studio/server/cli.test.ts +88 -0
  212. package/studio/server/cli.ts +240 -0
  213. package/studio/server/client-package.ts +25 -1
  214. package/studio/server/domain.test.ts +50 -40
  215. package/studio/server/domain.ts +83 -52
  216. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  217. package/studio/server/environment/dotenv-preview.ts +27 -0
  218. package/studio/server/environment/files.test.ts +53 -0
  219. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  220. package/studio/server/{state → handoff}/copy.ts +21 -6
  221. package/studio/server/handoff/service.test.ts +22 -0
  222. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  223. package/studio/server/index.ts +1 -1
  224. package/studio/server/instances/active.ts +72 -0
  225. package/studio/server/instances/deploy-record.test.ts +31 -0
  226. package/studio/server/instances/deploy-record.ts +37 -0
  227. package/studio/server/instances/deploy.ts +56 -0
  228. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  229. package/studio/server/instances/probe.ts +67 -0
  230. package/studio/server/instances/status.test.ts +12 -0
  231. package/studio/server/instances/status.ts +49 -0
  232. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  233. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  234. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  235. package/studio/server/introspect/anatomy/source.ts +181 -0
  236. package/studio/server/introspect/anatomy/views/routes.ts +114 -0
  237. package/studio/server/introspect/anatomy/views.ts +53 -0
  238. package/studio/server/introspect/anatomy-extras.test.ts +106 -35
  239. package/studio/server/introspect/anatomy-extras.ts +7 -726
  240. package/studio/server/introspect/anatomy.test.ts +22 -14
  241. package/studio/server/introspect/anatomy.ts +52 -28
  242. package/studio/server/introspect/bundle.ts +15 -9
  243. package/studio/server/introspect/canonical-schema.test.ts +93 -309
  244. package/studio/server/introspect/canonical-schema.ts +257 -425
  245. package/studio/server/introspect/config-preview.test.ts +32 -0
  246. package/studio/server/introspect/config-preview.ts +119 -0
  247. package/studio/server/introspect/core-extractor.ts +18 -127
  248. package/studio/server/introspect/core.ts +1 -2
  249. package/studio/server/introspect/diff.test.ts +18 -18
  250. package/studio/server/introspect/diff.ts +44 -90
  251. package/studio/server/introspect/extractor.ts +23 -116
  252. package/studio/server/introspect/hash.ts +5 -2
  253. package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
  254. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  255. package/studio/server/introspect/overlay.test.ts +26 -54
  256. package/studio/server/introspect/overlay.ts +10 -37
  257. package/studio/server/introspect/revision.test.ts +54 -0
  258. package/studio/server/introspect/revision.ts +49 -0
  259. package/studio/server/introspect/runtime.test.ts +83 -143
  260. package/studio/server/introspect/runtime.ts +40 -11
  261. package/studio/server/introspect/schema-ir-json.test.ts +70 -0
  262. package/studio/server/introspect/schema-ir-json.ts +151 -0
  263. package/studio/server/introspect/schema-refs.test.ts +43 -88
  264. package/studio/server/introspect/schema-refs.ts +14 -49
  265. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  266. package/studio/server/introspect/source-overlay/handlers.ts +161 -0
  267. package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
  268. package/studio/server/introspect/source-overlay/project.ts +248 -0
  269. package/studio/server/introspect/source-overlay/spans.ts +330 -0
  270. package/studio/server/json.ts +46 -0
  271. package/studio/server/lifecycle.ts +5 -1
  272. package/studio/server/sse.test.ts +26 -0
  273. package/studio/server/sse.ts +4 -1
  274. package/studio/server/state/baseline.test.ts +223 -7
  275. package/studio/server/state/baseline.ts +148 -34
  276. package/studio/server/state/comments.test.ts +66 -1
  277. package/studio/server/state/comments.ts +173 -10
  278. package/studio/server/state/context.ts +34 -2
  279. package/studio/server/state/documents.ts +42 -1
  280. package/studio/server/state/integrations.ts +27 -1
  281. package/studio/server/state/layout.test.ts +34 -0
  282. package/studio/server/state/layout.ts +44 -17
  283. package/studio/server/state/settings.ts +29 -22
  284. package/studio/server/state/store.test.ts +122 -0
  285. package/studio/server/state/store.ts +66 -17
  286. package/studio/server/state/visibility.ts +22 -11
  287. package/studio/server/views/model.test.ts +57 -0
  288. package/studio/server/views/model.ts +66 -0
  289. package/studio/server/views/runtime.ts +40 -0
  290. package/studio/server/views/selection-repository.test.ts +31 -0
  291. package/studio/server/views/selection-repository.ts +60 -0
  292. package/studio/server/views/session.test.ts +73 -0
  293. package/studio/server/views/session.ts +166 -0
  294. package/studio/server/views/target.test.ts +78 -0
  295. package/studio/server/views/target.ts +222 -0
  296. package/studio/server/watch.test.ts +11 -11
  297. package/studio/server/watch.ts +9 -5
  298. package/studio/server/workspace/catalog.test.ts +17 -0
  299. package/studio/server/workspace/catalog.ts +43 -0
  300. package/studio/server/{state → workspace}/create.test.ts +1 -1
  301. package/studio/server/{state → workspace}/create.ts +1 -1
  302. package/studio/server/{state → workspace}/git.ts +1 -1
  303. package/studio/server/workspace/updates.ts +83 -0
  304. package/studio/server/workspace-state.ts +1 -1
  305. package/studio/server/workspace-watch.ts +2 -2
  306. package/studio/shared/contracts/README.md +16 -0
  307. package/studio/shared/contracts/agent.ts +270 -0
  308. package/studio/shared/contracts/runtime.ts +50 -0
  309. package/studio/shared/contracts/schema.ts +393 -0
  310. package/studio/shared/contracts/surface.test.ts +317 -0
  311. package/studio/shared/contracts/workspace.ts +265 -0
  312. package/studio/shared/layout.test.ts +116 -0
  313. package/studio/shared/schema/identity.test.ts +34 -0
  314. package/studio/shared/schema/identity.ts +68 -0
  315. package/studio/shared/types.ts +11 -1099
  316. package/studio/tsconfig.json +1 -1
  317. package/viewer/dist/main.js +40 -61
  318. package/.check-workspace.cjs +0 -40
  319. package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
  320. package/studio/client/dist/assets/elk-api-D2xgMJvi.js +0 -1
  321. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  322. package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
  323. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  324. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  325. package/studio/server/state/catalog.ts +0 -117
  326. package/studio/server/state/instance.ts +0 -280
  327. package/studio/server/state/updates.ts +0 -63
  328. package/studio/server/state/views.test.ts +0 -286
  329. package/studio/server/state/views.ts +0 -535
@@ -1,6 +1,6 @@
1
- import type { InstanceInfo } from '../admin/instance/model'
2
1
  import type { AstraleConfig } from './config'
3
2
 
3
+ import { AdminInstanceNotFoundError, type InstanceInfo } from '../admin/instance/model'
4
4
  import { AstraleError } from '../errors'
5
5
  import {
6
6
  DEFAULT_ADMIN_TARGET_NAME,
@@ -190,19 +190,8 @@ export function isAdminDiscoveryFailure(error: unknown): boolean {
190
190
  }
191
191
 
192
192
  export function isManagedInstanceNotFound(error: unknown): boolean {
193
- if (error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND') return true
194
- if (!(error instanceof Error)) return false
195
- if (error.name === 'NotFoundError') return true
196
- // The admin kernel reports a missing instance node as InternalKernelError
197
- // with a NOT_FOUND-prefixed message. In this lookup that's an instance
198
- // miss (config problem), not a kernel fault — map it so callers get the
199
- // typed INSTANCE_NOT_FOUND with remediation instead of a raw kernel error.
200
- if (error.name === 'InternalKernelError' && /^NOT_FOUND\b/.test(error.message)) return true
201
- const data = (error as Error & { data?: unknown }).data
202
193
  return (
203
- error.name === 'KernelError' &&
204
- !!data &&
205
- typeof data === 'object' &&
206
- (data as { type?: unknown }).type === 'NotFoundError'
194
+ error instanceof AdminInstanceNotFoundError ||
195
+ (error instanceof AstraleError && error.code === 'INSTANCE_NOT_FOUND')
207
196
  )
208
197
  }
@@ -297,22 +297,26 @@ export async function upsertInstance(
297
297
  * bookmark was repointed to a different kernel so callers can warn.
298
298
  */
299
299
  export async function upsertManagedBookmark(
300
- key: string,
301
- slug: string,
302
- rawUrl: string,
303
- organizationId?: string,
300
+ input: Readonly<{
301
+ key: string
302
+ slug: string
303
+ url: string
304
+ organizationId?: string
305
+ defaultIdentity?: string
306
+ }>,
304
307
  ): Promise<{ entry: InstanceEntry; repointedFrom?: string }> {
305
308
  const store = await readInstances()
306
- const previousUrl = store.instances[key]?.url
307
- const url = normalizeInstanceKernelUrl(rawUrl)
308
- const { entry } = await upsertInstance(key, {
309
+ const previousUrl = store.instances[input.key]?.url
310
+ const url = normalizeInstanceKernelUrl(input.url)
311
+ const { entry } = await upsertInstance(input.key, {
309
312
  url,
310
313
  issuer: url,
311
- slug,
312
- name: slug,
314
+ slug: input.slug,
315
+ name: input.slug,
313
316
  kind: 'bookmark',
314
317
  mode: 'remote',
315
- ...(organizationId ? { organizationId } : {}),
318
+ ...(input.organizationId ? { organizationId: input.organizationId } : {}),
319
+ ...(input.defaultIdentity ? { defaultIdentity: input.defaultIdentity } : {}),
316
320
  })
317
321
  return {
318
322
  entry,
@@ -362,8 +366,9 @@ export async function getActive(config?: AstraleConfig): Promise<InstanceEntry &
362
366
  export async function resolveInstance(
363
367
  identifier: string,
364
368
  config?: AstraleConfig,
369
+ opts: { persist?: boolean } = {},
365
370
  ): Promise<ResolvedInstance> {
366
- const store = await readInstances(config)
371
+ const store = await readInstances(config, opts)
367
372
  const key = resolveInstanceKey(store, identifier)
368
373
  const entry = key ? store.instances[key] : undefined
369
374
  if (!key || !entry?.url) {
@@ -448,7 +453,12 @@ function isRegionRoutedInstanceRoot(url: URL): boolean {
448
453
  if (url.search || url.hash) return false
449
454
 
450
455
  const labels = url.hostname.toLowerCase().split('.')
451
- return labels.length === 4 && labels[2] === 'astrale' && labels[3] === 'ai'
456
+ return (
457
+ (labels.length === 4 || labels.length === 5) &&
458
+ /^[a-z]{2}(?:-[a-z0-9]+)*$/u.test(labels[1] ?? '') &&
459
+ labels.at(-2) === 'astrale' &&
460
+ labels.at(-1) === 'ai'
461
+ )
452
462
  }
453
463
 
454
464
  function definedEntry(entry: AddInstanceOpts): AddInstanceOpts {
package/src/lib/log.ts CHANGED
@@ -2,6 +2,7 @@ import chalk from 'chalk'
2
2
  import ora, { type Ora } from 'ora'
3
3
 
4
4
  import { AstraleError, NotImplementedError } from '../errors'
5
+ import { printFailureDebug } from './failure-debug'
5
6
  import { formatElapsed } from './format'
6
7
  import { isMachine, type MachineOpts } from './output'
7
8
 
@@ -18,28 +19,34 @@ export const log = {
18
19
 
19
20
  /** Report an error with hint (when present) and exit. `--json` / `--ci` / a
20
21
  * non-TTY stdout always get one structured JSON line on stderr. */
21
- export function fatal(e: unknown, opts?: MachineOpts): never {
22
+ export function fatal(e: unknown, opts?: MachineOpts & { readonly debug?: boolean }): never {
22
23
  // Ctrl-C at an interactive (@inquirer/prompts) prompt — exit quietly with the
23
24
  // SIGINT convention, not a red error line.
24
25
  if (e instanceof Error && e.name === 'ExitPromptError') process.exit(130)
25
- const msg = e instanceof Error ? e.message : String(e)
26
- const code = e instanceof AstraleError ? e.code : e instanceof Error ? e.name : 'Error'
26
+ const msg =
27
+ e instanceof AstraleError ? e.message : 'The CLI encountered an unexpected internal failure.'
28
+ const code = e instanceof AstraleError ? e.code : 'UNEXPECTED_ERROR'
27
29
  if (isMachine(opts)) {
28
30
  const payload: Record<string, unknown> = { error: code, message: msg }
29
31
  if (e instanceof AstraleError && e.hint) payload.hint = e.hint
30
32
  process.stderr.write(JSON.stringify(payload) + '\n')
31
- process.exit(1)
33
+ } else {
34
+ log.error(`${code}: ${msg}`)
35
+ if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
32
36
  }
33
- log.error(e instanceof AstraleError ? `${code}: ${msg}` : msg)
34
- if (e instanceof AstraleError && e.hint) log.dim(` hint: ${e.hint}`)
37
+ if (opts?.debug) printFailureDebug(e, '')
35
38
  process.exit(1)
36
39
  }
37
40
 
38
- /** Admit expected invalid input and exit through {@link fatal}. */
39
- export function failClosed(error: unknown, opts?: MachineOpts): never {
41
+ /** Project only a caller-authored admission failure into the CLI input family. */
42
+ export function failInput(error: unknown, opts?: MachineOpts): never {
40
43
  if (error instanceof AstraleError) fatal(error, opts)
44
+ if (!(error instanceof TypeError)) fatal(error, opts)
41
45
  fatal(
42
- new AstraleError('INVALID_INPUT', error instanceof Error ? error.message : String(error)),
46
+ new AstraleError(
47
+ 'INVALID_INPUT',
48
+ error.message.trim() ? error.message : 'CLI input is invalid.',
49
+ ),
43
50
  opts,
44
51
  )
45
52
  }
@@ -7,7 +7,9 @@ import {
7
7
  normalizeTokenResponse,
8
8
  pollDeviceToken,
9
9
  readIdpConfigOrBuiltin,
10
+ readIdpSession,
10
11
  readIdpStore,
12
+ requireClientId,
11
13
  requestClientCredentials,
12
14
  requestDeviceAuthorization,
13
15
  saveIdpSession,
@@ -71,10 +73,17 @@ export type LoginResult = {
71
73
  */
72
74
  export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
73
75
  const idpName = await resolveIdpName(opts.idp)
74
- const idp = await readIdpConfigOrBuiltin(idpName, { clientId: opts.clientId, persist: true })
76
+ const previous = opts.name && !opts.clientId ? await readIdpSession(opts.name) : null
77
+ const savedClientId = previous?.idp === idpName ? previous.clientId : undefined
78
+ const requestedClientId = opts.clientId ?? savedClientId
79
+ const idp = await readIdpConfigOrBuiltin(idpName, {
80
+ clientId: requestedClientId,
81
+ persist: true,
82
+ })
83
+ const clientId = requireClientId(idp, requestedClientId)
75
84
  const scope = opts.scope ?? idp.client.scope ?? 'openid profile email offline_access'
76
85
 
77
- const token = normalizeTokenResponse(await obtainToken(idp, opts, scope))
86
+ const token = normalizeTokenResponse(await obtainToken(idp, { ...opts, clientId }, scope))
78
87
  if (!token.access_token) throw new Error('IdP response did not include access_token')
79
88
  if (opts.audience && !tokenAudienceMatches(token.access_token, opts.audience)) {
80
89
  throw new Error(
@@ -83,7 +92,7 @@ export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
83
92
  }
84
93
 
85
94
  const claims = decodeTokenClaims(token.id_token ?? token.access_token)
86
- const subject = subjectFromToken(token, opts.clientId ?? idp.client.client_id ?? idpName)
95
+ const subject = subjectFromToken(token, clientId)
87
96
  const issuer = issuerFromToken(token, idp.metadata.issuer)
88
97
  const identityName = opts.name ?? identityNameFromClaims(claims, idpName)
89
98
  const session: IdpSession = {
@@ -91,6 +100,7 @@ export async function loginViaIdp(opts: LoginFlowOpts): Promise<LoginResult> {
91
100
  idp: idpName,
92
101
  issuer,
93
102
  subject,
103
+ clientId,
94
104
  audience: opts.audience,
95
105
  access_token: token.access_token,
96
106
  id_token: token.id_token,
package/src/lib/meta.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * `jwks_uri`) and JWKS at `/.well-known/jwks.json`. These helpers probe
7
7
  * both to verify the issuer is alive and publishes at least one key.
8
8
  *
9
- * Domain workers may expose `/meta` for deployment drift detection; that is
10
- * outside this connect-only CLI surface.
9
+ * Domain deployment readiness uses the canonical Domain Publication; that is
10
+ * outside this issuer-connectivity helper.
11
11
  */
12
12
 
13
13
  import { IssuerUnreachableError } from '../errors'
@@ -9,12 +9,10 @@ import { createOwnedInstance } from './admin-instance'
9
9
  import { setActive, upsertManagedBookmark } from './instance'
10
10
  import { withSpinner } from './log'
11
11
  import { isMachine } from './output'
12
- import { promptSelect } from './prompt'
13
12
  import { validateSlug } from './validation'
14
13
 
15
14
  export type ProvisionOpts = KernelCommandOpts &
16
15
  AdminTargetCommandOpts & {
17
- hostId?: string
18
16
  // Global flags (program.ts) that force non-interactive — mirrors `instance use`.
19
17
  ci?: boolean
20
18
  noPrompt?: boolean
@@ -35,10 +33,10 @@ export type ProvisionResult = {
35
33
  const SAGA_TIMEOUT_MS = '240000'
36
34
 
37
35
  /**
38
- * Provision an alpha instance through the admin kernel, then bookmark it and
36
+ * Provision an instance through the admin kernel, then bookmark it and
39
37
  * make it the active target. Extracted from `instance create` so `astrale
40
- * setup` provisions through the exact same saga (auth assertion → alphaCreate
41
- * bookmark → set-active), including the interactive multi-host picker.
38
+ * setup` provisions through the exact same saga (auth assertion → create
39
+ * bookmark → set-active).
42
40
  *
43
41
  * Presentation is deliberately minimal here (a spinner + a one-line success);
44
42
  * the caller renders anything richer — `setup` follows this with a hero panel.
@@ -48,15 +46,10 @@ export async function provisionInstance(
48
46
  opts: ProvisionOpts,
49
47
  ): Promise<ProvisionResult> {
50
48
  validateSlug(slug)
51
- await assertAlphaCreateAuth(opts)
52
- // The created instance must belong to the LOGGED-IN identity — never to an
53
- // identity pinned on the admin bookmark (that mismatch silently made a fresh
54
- // user's instance unusable). `--as` still wins.
55
- if (!opts.as) opts = { ...opts, as: (await readIdentities()).default }
49
+ const authentication = await instanceCreateAuthentication(opts)
50
+ opts = authentication.opts
56
51
 
57
52
  const machine = isMachine(opts)
58
- const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI)
59
-
60
53
  let repointedFrom: string | undefined
61
54
  let selectionError: unknown = null
62
55
  // The global 30s default doesn't just fail the CLIENT: the disconnect kills
@@ -65,21 +58,24 @@ export async function provisionInstance(
65
58
  // timeout; an explicit --timeout still wins.
66
59
  const createOpts = { ...opts, timeout: opts.timeout ?? SAGA_TIMEOUT_MS }
67
60
 
68
- const runProvision = (hostId: string | undefined) =>
61
+ const runProvision = () =>
69
62
  withSpinner(
70
63
  `Provisioning instance ${slug}`,
71
64
  !machine,
72
65
  async () => {
73
- const created = await createOwnedInstance(createOpts, slug, hostId)
66
+ const created = await createOwnedInstance(createOpts, slug)
74
67
  try {
75
68
  // Org id from the create response — authoritative for token scoping
76
69
  // (the router's /auth/org is eventually consistent).
77
- const bookmarked = await upsertManagedBookmark(
78
- slug,
70
+ const bookmarked = await upsertManagedBookmark({
71
+ key: slug,
79
72
  slug,
80
- created.url,
81
- created.organizationId,
82
- )
73
+ url: created.url,
74
+ ...(created.organizationId ? { organizationId: created.organizationId } : {}),
75
+ ...(authentication.defaultIdentity
76
+ ? { defaultIdentity: authentication.defaultIdentity }
77
+ : {}),
78
+ })
83
79
  repointedFrom = bookmarked.repointedFrom
84
80
  await setActive(slug)
85
81
  } catch (e) {
@@ -93,23 +89,7 @@ export async function provisionInstance(
93
89
  },
94
90
  )
95
91
 
96
- // Host placement is chosen SERVER-side (the caller's ready + USE-granted
97
- // hosts). We recover ONLY from its multi-host ambiguity: pop a picker and
98
- // retry with the chosen host_id — the ambiguity error throws before any
99
- // provisioning side effect, so the retry is clean. No host / permission /
100
- // capacity / a non-interactive run all reject through to the caller's `fatal`,
101
- // surfacing the server's own message plus the listed ids. An explicit
102
- // `--host-id` skips all of this.
103
- const created = await runProvision(opts.hostId).catch(async (e: unknown) => {
104
- const hostIds = !opts.hostId && interactive ? parseEligibleHostIds(e) : null
105
- if (!hostIds) throw e
106
- const chosen = await promptSelect(
107
- `${hostIds.length} hosts available — pick one to provision on`,
108
- hostIds.map((hid) => ({ name: hid, value: hid })),
109
- )
110
- if (!chosen) throw e
111
- return runProvision(chosen)
112
- })
92
+ const created = await runProvision()
113
93
 
114
94
  // Warnings go to stderr so machine-readable stdout stays clean.
115
95
  const warn = (msg: string) => console.error(chalk.yellow('⚠'), msg)
@@ -124,31 +104,28 @@ export async function provisionInstance(
124
104
  return { created, slug, repointedFrom, ...(selectionError ? { selectionError } : {}) }
125
105
  }
126
106
 
127
- /**
128
- * Recover the eligible host ids from alphaCreate's multi-host error
129
- * ("N ready hosts are assigned (id1, id2). Specify host_id…") — only when
130
- * there's a real choice (>1). Returns null for any other error (no host,
131
- * permission, capacity). Deliberately coupled to that message wording (Option
132
- * B: no admin-side change); if it ever drifts we simply stop offering the
133
- * picker and the raw error is shown instead — no worse than before.
134
- */
135
- export function parseEligibleHostIds(error: unknown): string[] | null {
136
- const text = error instanceof Error ? error.message : String(error)
137
- const match = /ready hosts are assigned \(([^)]+)\)/.exec(text)
138
- if (!match) return null
139
- const ids = match[1]!
140
- .split(',')
141
- .map((s) => s.trim())
142
- .filter(Boolean)
143
- return ids.length > 1 ? ids : null
107
+ async function instanceCreateAuthentication(opts: ProvisionOpts): Promise<{
108
+ readonly opts: ProvisionOpts
109
+ readonly defaultIdentity?: string
110
+ }> {
111
+ if (opts.creds) return { opts }
112
+ const store = await readIdentities()
113
+ const defaultIdentity = selectInstanceCreateIdentity(store, opts)
114
+ return {
115
+ opts: opts.as ? opts : { ...opts, as: defaultIdentity },
116
+ defaultIdentity,
117
+ }
144
118
  }
145
119
 
146
- async function assertAlphaCreateAuth(opts: Pick<ProvisionOpts, 'as' | 'creds'>): Promise<void> {
147
- if (opts.creds) return
148
- assertAlphaCreateIdentity(await readIdentities(), opts)
120
+ export function selectInstanceCreateIdentity(
121
+ store: IdentityStore,
122
+ opts: Pick<ProvisionOpts, 'as'> = {},
123
+ ): string {
124
+ assertInstanceCreateIdentity(store, opts)
125
+ return opts.as ?? store.default
149
126
  }
150
127
 
151
- export function assertAlphaCreateIdentity(
128
+ export function assertInstanceCreateIdentity(
152
129
  store: IdentityStore,
153
130
  opts: Pick<ProvisionOpts, 'as'> = {},
154
131
  ): void {
package/src/lib/update.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { createHash } from 'node:crypto'
2
- import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
2
+ import {
3
+ chmod,
4
+ copyFile,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ realpath,
9
+ rename,
10
+ rm,
11
+ writeFile,
12
+ } from 'node:fs/promises'
3
13
  import { tmpdir } from 'node:os'
4
14
  import { dirname, join } from 'node:path'
5
15
  import { z } from 'zod'
@@ -9,11 +19,11 @@ import { INSTALL_PATH } from '../state/index'
9
19
  import { run } from './proc'
10
20
 
11
21
  const DEFAULT_REPO = 'astrale-os/cli'
12
- const DEFAULT_CHANNEL = 'alpha'
22
+ export const DEFAULT_UPDATE_CHANNEL = 'beta'
13
23
 
14
24
  export const InstallMetadataSchema = z.object({
15
25
  method: z.literal('script'),
16
- channel: z.string().min(1).default(DEFAULT_CHANNEL),
26
+ channel: z.string().min(1).default(DEFAULT_UPDATE_CHANNEL),
17
27
  version: z.string().min(1).optional(),
18
28
  repo: z.string().min(1).default(DEFAULT_REPO),
19
29
  bin: z.string().min(1),
@@ -61,9 +71,26 @@ export type UpdateRequest = {
61
71
  currentVersion: string
62
72
  platform?: Platform
63
73
  installPath?: string
74
+ execution?: UpdateExecution
64
75
  }
65
76
 
77
+ export type UpdateExecution =
78
+ | { kind: 'standalone'; executable: string }
79
+ | { kind: 'package-managed'; executable: string }
80
+
81
+ const admittedScriptInstall = Symbol('admittedScriptInstall')
82
+ export type AdmittedScriptInstall = Readonly<{
83
+ metadata: InstallMetadata
84
+ executable: string
85
+ [admittedScriptInstall]: true
86
+ }>
87
+
66
88
  export type UpdateResult =
89
+ | {
90
+ status: 'managed'
91
+ currentVersion: string
92
+ executable: string
93
+ }
67
94
  | {
68
95
  status: 'up-to-date'
69
96
  currentVersion: string
@@ -106,42 +133,99 @@ export function platformKey(platform: Platform): string {
106
133
  return `${platform.os}-${platform.arch}`
107
134
  }
108
135
 
109
- /**
110
- * True when running as the Bun-compiled standalone binary (Linux/macOS), which
111
- * self-updates by swapping its own file. The Node/npm build does not expose
112
- * `process.versions.bun`; it is managed by the user's package manager instead.
113
- */
114
- function isStandaloneBinary(): boolean {
115
- return Boolean((process.versions as { bun?: string }).bun)
136
+ export function classifyUpdateExecution(input: {
137
+ bunVersion?: string
138
+ executable: string
139
+ entry?: string
140
+ }): UpdateExecution {
141
+ return input.bunVersion && input.entry?.startsWith('/$bunfs/')
142
+ ? { kind: 'standalone', executable: input.executable }
143
+ : { kind: 'package-managed', executable: input.executable }
144
+ }
145
+
146
+ export function detectUpdateExecution(): UpdateExecution {
147
+ return classifyUpdateExecution({
148
+ bunVersion: (process.versions as { bun?: string }).bun,
149
+ executable: process.execPath,
150
+ entry: process.argv[1],
151
+ })
152
+ }
153
+
154
+ export function packageManagedUpdateError(executable: string): AstraleError {
155
+ return new AstraleError(
156
+ 'UPDATE_PACKAGE_MANAGED',
157
+ 'This Astrale build is managed by your package manager.',
158
+ `Active binary: ${executable}. Update it with npm, pnpm, or bun; or put the official script installation first on PATH.`,
159
+ )
116
160
  }
117
161
 
118
162
  export async function readInstallMetadata(path = INSTALL_PATH): Promise<InstallMetadata> {
119
163
  let raw: string
120
164
  try {
121
165
  raw = await readFile(path, 'utf8')
122
- } catch {
123
- throw isStandaloneBinary()
124
- ? new AstraleError(
125
- 'UPDATE_NOT_SCRIPT_INSTALLED',
126
- 'Astrale was not installed by the official install script.',
127
- 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
128
- )
129
- : new AstraleError(
130
- 'UPDATE_PACKAGE_MANAGED',
131
- 'This Astrale build is managed by your package manager.',
132
- 'Update with: npm install -g @astrale-os/cli@latest (or pnpm/bun)',
133
- )
166
+ } catch (error) {
167
+ if (!isMissingFile(error)) throw error
168
+ throw new AstraleError(
169
+ 'UPDATE_NOT_SCRIPT_INSTALLED',
170
+ 'Astrale was not installed by the official install script.',
171
+ 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
172
+ )
134
173
  }
135
174
 
136
- const parsed = InstallMetadataSchema.safeParse(JSON.parse(raw))
175
+ let decoded: unknown
176
+ try {
177
+ decoded = JSON.parse(raw)
178
+ } catch {
179
+ throw badInstallMetadata(path)
180
+ }
181
+ const parsed = InstallMetadataSchema.safeParse(decoded)
137
182
  if (!parsed.success) {
183
+ throw badInstallMetadata(path)
184
+ }
185
+ return parsed.data
186
+ }
187
+
188
+ function isMissingFile(error: unknown): error is NodeJS.ErrnoException {
189
+ return error instanceof Error && 'code' in error && error.code === 'ENOENT'
190
+ }
191
+
192
+ function badInstallMetadata(path: string): AstraleError {
193
+ return new AstraleError(
194
+ 'UPDATE_BAD_INSTALL_METADATA',
195
+ `Invalid install metadata at ${path}.`,
196
+ 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
197
+ )
198
+ }
199
+
200
+ export async function admitScriptInstall(
201
+ meta: InstallMetadata,
202
+ execution: Extract<UpdateExecution, { kind: 'standalone' }>,
203
+ ): Promise<AdmittedScriptInstall> {
204
+ const [running, recorded] = await Promise.all([
205
+ realpathIfExists(execution.executable),
206
+ realpathIfExists(meta.bin),
207
+ ])
208
+ if (running === undefined || recorded === undefined || running !== recorded) {
138
209
  throw new AstraleError(
139
- 'UPDATE_BAD_INSTALL_METADATA',
140
- `Invalid install metadata at ${path}.`,
141
- 'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
210
+ 'UPDATE_INSTALL_MISMATCH',
211
+ 'The running Astrale binary does not own the recorded script installation.',
212
+ `Running binary: ${execution.executable}; recorded binary: ${meta.bin}. Refusing to replace a different installation.`,
142
213
  )
143
214
  }
144
- return parsed.data
215
+ return Object.freeze({
216
+ metadata: meta,
217
+ executable: running,
218
+ [admittedScriptInstall]: true as const,
219
+ })
220
+ }
221
+
222
+ async function realpathIfExists(path: string): Promise<string | undefined> {
223
+ try {
224
+ return await realpath(path)
225
+ } catch (error) {
226
+ if (isMissingFile(error)) return undefined
227
+ throw error
228
+ }
145
229
  }
146
230
 
147
231
  export async function writeInstallMetadata(
@@ -162,7 +246,7 @@ export function releaseBase(
162
246
  const version = req.version.replace(/^cli\/v/, '').replace(/^v/, '')
163
247
  return `https://github.com/${repo}/releases/download/cli/v${version}`
164
248
  }
165
- return `https://github.com/${repo}/releases/download/${req.channel ?? meta.channel ?? DEFAULT_CHANNEL}`
249
+ return `https://github.com/${repo}/releases/download/${req.channel ?? meta.channel ?? DEFAULT_UPDATE_CHANNEL}`
166
250
  }
167
251
 
168
252
  export async function fetchManifest(base: string): Promise<UpdateManifest> {
@@ -175,9 +259,19 @@ export function shouldUpdate(currentVersion: string, manifestVersion: string): b
175
259
  }
176
260
 
177
261
  export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
178
- const meta = await readInstallMetadata(req.installPath)
262
+ const execution = req.execution ?? detectUpdateExecution()
263
+ if (execution.kind === 'package-managed') {
264
+ return {
265
+ status: 'managed',
266
+ currentVersion: req.currentVersion,
267
+ executable: execution.executable,
268
+ }
269
+ }
270
+
271
+ const install = await admitScriptInstall(await readInstallMetadata(req.installPath), execution)
272
+ const meta = install.metadata
179
273
  const currentVersion = meta.version ?? req.currentVersion
180
- const channel = req.channel ?? meta.channel ?? DEFAULT_CHANNEL
274
+ const channel = req.channel ?? meta.channel ?? DEFAULT_UPDATE_CHANNEL
181
275
  const platform = req.platform ?? detectPlatform()
182
276
  const key = platformKey(platform)
183
277
  const base = releaseBase(meta, { channel, version: req.version })
@@ -1,4 +1,4 @@
1
- import { isDnsLabel } from '@astrale-os/sdk/domain'
1
+ import { isDnsLabel } from '@astrale-os/sdk/value'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { ReservedSlugError } from '../errors'
@@ -21,7 +21,7 @@ export function validateName(name: string, entity: string): void {
21
21
  }
22
22
 
23
23
  export function validateSlug(slug: string): void {
24
- // Canonical DNS-label rule (`@astrale-os/sdk/domain`): a slug becomes a
24
+ // Canonical Core DNS-label rule: a slug becomes a
25
25
  // hostname label, so reject anything not DNS-safe (§4.7).
26
26
  if (!slug || !isDnsLabel(slug)) {
27
27
  throw new Error(
@@ -1,4 +1,4 @@
1
- import type { ResolvedView as SessionResolvedView } from '@astrale-os/kernel-client/session'
1
+ import type { ResolvedView as SessionResolvedView } from '@astrale-os/sdk/client/session'
2
2
  import type { ResolvedView } from '@astrale-os/shell'
3
3
 
4
4
  import { Path } from '@astrale-os/sdk/graph/path'
@@ -1,3 +1,5 @@
1
+ import type { AuthApi } from '@astrale-os/sdk/auth'
2
+ import type { IssuerId } from '@astrale-os/sdk/auth'
1
3
  import type { ReadableStream as WebReadableStream } from 'node:stream/web'
2
4
 
3
5
  import { readFile } from 'node:fs/promises'
@@ -23,8 +25,9 @@ export { ensureViewerAssets, viewerDistDir } from './assets'
23
25
  * heartbeat keeps a live session alive.
24
26
  */
25
27
 
26
- const TOKEN_REFRESH_MARGIN_MS = 5 * 60_000
27
- const FALLBACK_TOKEN_TTL_MS = 3600_000
28
+ const VIEW_TOKEN_TTL_SECONDS = 4 * 60
29
+ const TOKEN_REFRESH_MARGIN_MS = 60_000
30
+ const FALLBACK_TOKEN_TTL_MS = VIEW_TOKEN_TTL_SECONDS * 1_000
28
31
  const IDLE_SWEEP_MS = 60_000
29
32
 
30
33
  export type PageStatus = { state: string; error?: string; at: string }
@@ -44,7 +47,7 @@ export function startViewServer(config: ViewServeConfig): Server {
44
47
  async function freshGrant(): Promise<TokenGrant> {
45
48
  if (grant && grant.expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS) return grant
46
49
  grant = await withClientSession(config.kernel, async ({ auth, target }) => {
47
- const token = await auth.mint({ audience: target.kernelIssuer, ttlSeconds: 3_600 })
50
+ const token = await mintViewCredential(auth, target.kernelIssuer)
48
51
  return {
49
52
  token,
50
53
  expiresAt: jwtExpiry(token) ?? Date.now() + FALLBACK_TOKEN_TTL_MS,
@@ -182,6 +185,17 @@ export function startViewServer(config: ViewServeConfig): Server {
182
185
  return server
183
186
  }
184
187
 
188
+ /** Mint a short-lived credential from the selected CLI identity proof. */
189
+ export async function mintViewCredential(
190
+ auth: Pick<AuthApi, 'mint'>,
191
+ audience: IssuerId,
192
+ ): Promise<string> {
193
+ return auth.mint({
194
+ audience,
195
+ ttlSeconds: VIEW_TOKEN_TTL_SECONDS,
196
+ })
197
+ }
198
+
185
199
  function jwtExpiry(token: string): number | null {
186
200
  try {
187
201
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {