@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,14 +1,20 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { chmod, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
2
+ import { chmod, mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
6
  import {
7
+ admitScriptInstall,
8
+ classifyUpdateExecution,
9
+ DEFAULT_UPDATE_CHANNEL,
10
+ InstallMetadataSchema,
11
+ readInstallMetadata,
7
12
  releaseBase,
8
13
  shouldUpdate,
9
14
  updateAstrale,
10
15
  writeInstallMetadata,
11
16
  type InstallMetadata,
17
+ type UpdateExecution,
12
18
  } from '../update'
13
19
 
14
20
  async function makeFakeRelease(
@@ -63,7 +69,7 @@ async function makeFakeRelease(
63
69
  async function makeInstall(
64
70
  root: string,
65
71
  version: string,
66
- ): Promise<{ meta: InstallMetadata; path: string }> {
72
+ ): Promise<{ meta: InstallMetadata; path: string; execution: UpdateExecution }> {
67
73
  const bin = join(root, 'bin', 'astrale')
68
74
  await mkdir(join(root, 'bin'), { recursive: true })
69
75
  await writeFile(
@@ -80,10 +86,43 @@ async function makeInstall(
80
86
  bin,
81
87
  }
82
88
  await writeInstallMetadata(meta, path)
83
- return { meta, path }
89
+ return { meta, path, execution: { kind: 'standalone', executable: bin } }
84
90
  }
85
91
 
86
92
  describe('update helpers', () => {
93
+ test('classifies only a Bun-compiled executable as standalone', () => {
94
+ expect(
95
+ classifyUpdateExecution({
96
+ bunVersion: '1.3.14',
97
+ executable: '/tmp/astrale',
98
+ entry: '/$bunfs/root/astrale',
99
+ }),
100
+ ).toEqual({ kind: 'standalone', executable: '/tmp/astrale' })
101
+ expect(
102
+ classifyUpdateExecution({
103
+ bunVersion: '1.3.14',
104
+ executable: '/opt/homebrew/bin/bun',
105
+ entry: '/tmp/astrale.ts',
106
+ }),
107
+ ).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/bun' })
108
+ expect(
109
+ classifyUpdateExecution({
110
+ executable: '/opt/homebrew/bin/node',
111
+ entry: '/tmp/astrale.js',
112
+ }),
113
+ ).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/node' })
114
+ })
115
+
116
+ test('defaults missing install metadata to the beta channel', () => {
117
+ expect(DEFAULT_UPDATE_CHANNEL).toBe('beta')
118
+ expect(
119
+ InstallMetadataSchema.parse({
120
+ method: 'script',
121
+ bin: '/tmp/astrale',
122
+ }).channel,
123
+ ).toBe('beta')
124
+ })
125
+
87
126
  test('releaseBase resolves exact versions and channels', () => {
88
127
  const meta: InstallMetadata = {
89
128
  method: 'script',
@@ -106,10 +145,70 @@ describe('update helpers', () => {
106
145
  })
107
146
  })
108
147
 
148
+ describe('script install admission', () => {
149
+ test('rejects malformed JSON with the stable metadata error', async () => {
150
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
151
+ const path = join(root, 'install.json')
152
+ await writeFile(path, '{')
153
+
154
+ await expect(readInstallMetadata(path)).rejects.toMatchObject({
155
+ code: 'UPDATE_BAD_INSTALL_METADATA',
156
+ })
157
+ })
158
+
159
+ test('admits a symlink only when it resolves to the recorded binary', async () => {
160
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
161
+ const { meta } = await makeInstall(root, '1.0.0')
162
+ const alias = join(root, 'astrale-alias')
163
+ await symlink(meta.bin, alias)
164
+
165
+ const admitted = await admitScriptInstall(meta, {
166
+ kind: 'standalone',
167
+ executable: alias,
168
+ })
169
+
170
+ expect(admitted.metadata).toEqual(meta)
171
+ })
172
+
173
+ test('rejects a standalone binary that does not own the recorded target', async () => {
174
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
175
+ const { meta } = await makeInstall(root, '1.0.0')
176
+ const other = join(root, 'other-astrale')
177
+ await writeFile(other, '#!/bin/sh\n')
178
+
179
+ await expect(
180
+ admitScriptInstall(meta, { kind: 'standalone', executable: other }),
181
+ ).rejects.toMatchObject({ code: 'UPDATE_INSTALL_MISMATCH' })
182
+ })
183
+ })
184
+
109
185
  describe('updateAstrale', () => {
186
+ test('a package-managed process never consults or mutates a coexisting script install', async () => {
187
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
188
+ const { path, meta } = await makeInstall(root, '1.0.0')
189
+ const before = await readFile(meta.bin, 'utf8')
190
+ process.env.ASTRALE_UPDATE_BASE = 'file:///definitely-not-a-release'
191
+ try {
192
+ const result = await updateAstrale({
193
+ currentVersion: '2.0.0',
194
+ installPath: path,
195
+ execution: { kind: 'package-managed', executable: '/opt/homebrew/bin/node' },
196
+ })
197
+
198
+ expect(result).toEqual({
199
+ status: 'managed',
200
+ currentVersion: '2.0.0',
201
+ executable: '/opt/homebrew/bin/node',
202
+ })
203
+ expect(await readFile(meta.bin, 'utf8')).toBe(before)
204
+ } finally {
205
+ delete process.env.ASTRALE_UPDATE_BASE
206
+ }
207
+ })
208
+
110
209
  test('check compares the installed release identity from metadata', async () => {
111
210
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
112
- const { path } = await makeInstall(root, 'main-abc123')
211
+ const { path, execution } = await makeInstall(root, 'main-abc123')
113
212
  const release = await makeFakeRelease(root, 'main-abc123', { binaryVersion: '1.0.0' })
114
213
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
115
214
  try {
@@ -118,6 +217,7 @@ describe('updateAstrale', () => {
118
217
  currentVersion: '1.0.0',
119
218
  platform: { os: 'darwin', arch: 'arm64' },
120
219
  installPath: path,
220
+ execution,
121
221
  })
122
222
 
123
223
  expect(result).toMatchObject({
@@ -132,7 +232,7 @@ describe('updateAstrale', () => {
132
232
 
133
233
  test('check reports available update without replacing the binary', async () => {
134
234
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
135
- const { path, meta } = await makeInstall(root, '1.0.0')
235
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
136
236
  const release = await makeFakeRelease(root, '1.1.0')
137
237
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
138
238
  try {
@@ -141,6 +241,7 @@ describe('updateAstrale', () => {
141
241
  currentVersion: '1.0.0',
142
242
  platform: { os: 'darwin', arch: 'arm64' },
143
243
  installPath: path,
244
+ execution,
144
245
  })
145
246
 
146
247
  expect(result).toMatchObject({
@@ -156,7 +257,7 @@ describe('updateAstrale', () => {
156
257
 
157
258
  test('supports legacy string asset manifests', async () => {
158
259
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
159
- const { path, meta } = await makeInstall(root, '1.0.0')
260
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
160
261
  const release = await makeFakeRelease(root, '1.1.0', { legacyManifest: true })
161
262
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
162
263
  try {
@@ -165,6 +266,7 @@ describe('updateAstrale', () => {
165
266
  currentVersion: '1.0.0',
166
267
  platform: { os: 'darwin', arch: 'arm64' },
167
268
  installPath: path,
269
+ execution,
168
270
  })
169
271
 
170
272
  expect(check).toMatchObject({
@@ -176,6 +278,7 @@ describe('updateAstrale', () => {
176
278
  currentVersion: '1.0.0',
177
279
  platform: { os: 'darwin', arch: 'arm64' },
178
280
  installPath: path,
281
+ execution,
179
282
  })
180
283
 
181
284
  expect(result).toMatchObject({
@@ -192,7 +295,7 @@ describe('updateAstrale', () => {
192
295
 
193
296
  test('updates the binary and install metadata', async () => {
194
297
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
195
- const { path, meta } = await makeInstall(root, '1.0.0')
298
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
196
299
  const release = await makeFakeRelease(root, '1.1.0')
197
300
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
198
301
  try {
@@ -200,6 +303,7 @@ describe('updateAstrale', () => {
200
303
  currentVersion: '1.0.0',
201
304
  platform: { os: 'darwin', arch: 'arm64' },
202
305
  installPath: path,
306
+ execution,
203
307
  })
204
308
 
205
309
  expect(result).toMatchObject({
@@ -219,7 +323,7 @@ describe('updateAstrale', () => {
219
323
 
220
324
  test('updates canary-style releases whose binary reports the package version', async () => {
221
325
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
222
- const { path, meta } = await makeInstall(root, 'main-old123')
326
+ const { path, meta, execution } = await makeInstall(root, 'main-old123')
223
327
  const release = await makeFakeRelease(root, 'main-new456', { binaryVersion: '1.0.0' })
224
328
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
225
329
  try {
@@ -227,6 +331,7 @@ describe('updateAstrale', () => {
227
331
  currentVersion: '1.0.0',
228
332
  platform: { os: 'darwin', arch: 'arm64' },
229
333
  installPath: path,
334
+ execution,
230
335
  })
231
336
 
232
337
  expect(result).toMatchObject({
@@ -23,7 +23,6 @@ const profile: ResolvedView = {
23
23
  kind: 'definition',
24
24
  definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
25
25
  },
26
- auth: 'required',
27
26
  },
28
27
  href: 'https://shell.test/profile',
29
28
  handshake: 'shell',
@@ -41,7 +40,6 @@ const card: ResolvedView = {
41
40
  kind: 'definition',
42
41
  definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
43
42
  },
44
- auth: 'public',
45
43
  },
46
44
  href: 'https://shell.test/card',
47
45
  handshake: 'none',
@@ -1,10 +1,12 @@
1
- import { describe, expect, test } from 'bun:test'
1
+ import type { AuthApi } from '@astrale-os/sdk/auth'
2
+
3
+ import { describe, expect, mock, test } from 'bun:test'
2
4
  import { once } from 'node:events'
3
5
 
4
6
  import type { ViewServeConfig } from '../view/session'
5
7
 
6
8
  import { findFreePort } from '../port'
7
- import { startViewServer } from '../view/server'
9
+ import { mintViewCredential, startViewServer } from '../view/server'
8
10
 
9
11
  const digest = (character: string) => `sha256:${character.repeat(64)}` as const
10
12
  const target = (value: string) => value as ViewServeConfig['session']['view']['target']
@@ -13,6 +15,19 @@ const revision = (character: string) =>
13
15
  digest(character) as ViewServeConfig['session']['view']['route']['revision']
14
16
 
15
17
  describe('view session server credentials', () => {
18
+ test('mints a proof-bounded credential for the Kernel audience', async () => {
19
+ const mint = mock(async () => 'minted-credential')
20
+ const auth = { mint } as unknown as Pick<AuthApi, 'mint'>
21
+
22
+ await expect(mintViewCredential(auth, issuer('https://kernel.test'))).resolves.toBe(
23
+ 'minted-credential',
24
+ )
25
+ expect(mint).toHaveBeenCalledWith({
26
+ audience: 'https://kernel.test',
27
+ ttlSeconds: 240,
28
+ })
29
+ })
30
+
16
31
  /** @evidence TEST-CLI-PLAIN-VIEW-RECEIVES-NO-CREDENTIAL */
17
32
  test('refuses to mint a token for a handshake-none View', async () => {
18
33
  const nonce = 'plain-view'
@@ -29,7 +44,7 @@ describe('view session server credentials', () => {
29
44
  target: target('/:example.test'),
30
45
  route: {
31
46
  key: 'example.test:view.public',
32
- declaration: { target: { kind: 'domain' }, auth: 'required' },
47
+ declaration: { target: { kind: 'domain' } },
33
48
  href: 'https://example.test/ui/public',
34
49
  handshake: 'none',
35
50
  issuer: issuer('https://example.test'),
@@ -58,7 +58,7 @@ describe('view session private state', () => {
58
58
  issuer: issuer('https://example.test'),
59
59
  etag: `sha256:${'a'.repeat(64)}`,
60
60
  revision: revision('b'),
61
- declaration: { target: { kind: 'domain' }, auth: 'required' },
61
+ declaration: { target: { kind: 'domain' } },
62
62
  },
63
63
  },
64
64
  createdAt: '2026-08-12T00:00:00.000Z',
@@ -6,7 +6,7 @@ import { withAdminClientSession } from '../connection'
6
6
  export {
7
7
  AdminInstanceNotFoundError,
8
8
  findOwnedInstance,
9
- formatInstanceLocation,
9
+ formatInstanceState,
10
10
  type InstanceInfo,
11
11
  type InstanceState,
12
12
  type OwnedInstanceInfo,
@@ -19,19 +19,25 @@ export function listOwnedInstances(options: AdminConnectionOptions) {
19
19
  )
20
20
  }
21
21
 
22
+ /** Read caller-visible inventory together with the exact local identity used for the Admin call. */
23
+ export function listOwnedInstancesWithIdentity(options: AdminConnectionOptions) {
24
+ return withAdminClientSession(options, async (context) =>
25
+ Object.freeze({
26
+ instances: await (await connectAdminInstances(context)).list(),
27
+ ...(context.identity === undefined ? {} : { identity: context.identity }),
28
+ }),
29
+ )
30
+ }
31
+
22
32
  /** Reuse an already-open Admin session for a caller-visible Instance inventory. */
23
33
  export async function listOwnedInstancesInContext(context: ConnectionContext) {
24
34
  return (await connectAdminInstances(context)).list()
25
35
  }
26
36
 
27
- /** Provision through Fleet placement or one explicitly resolved Host receiver. */
28
- export function createOwnedInstance(
29
- options: AdminConnectionOptions,
30
- slug: string,
31
- hostId?: string,
32
- ) {
37
+ /** Request one managed Instance; Admin owns infrastructure placement. */
38
+ export function createOwnedInstance(options: AdminConnectionOptions, slug: string) {
33
39
  return withAdminClientSession(options, async (context) =>
34
- (await connectAdminInstances(context)).create(slug, hostId),
40
+ (await connectAdminInstances(context)).create(slug),
35
41
  )
36
42
  }
37
43
 
@@ -5,11 +5,30 @@ import type { InstanceStore } from './instance'
5
5
 
6
6
  import { AstraleError } from '../errors'
7
7
  import { readInstances, resolveInstanceKey } from './instance'
8
+ import { DEFAULT_UPDATE_CHANNEL } from './update'
8
9
  import { isHttpUrl } from './validation'
9
10
 
10
11
  export const DEFAULT_ADMIN_TARGET_NAME = 'admin'
11
- export const DEFAULT_ADMIN_TARGET_URL = 'https://admin.eu.astrale.ai/api'
12
- export const DEFAULT_ADMIN_DOMAIN_ISSUER = 'https://admin.beta.astrale.ai'
12
+
13
+ export function defaultAdminTargetForChannel(channel: string): {
14
+ readonly url: string
15
+ readonly domainIssuer: string
16
+ } {
17
+ return channel === 'stable'
18
+ ? {
19
+ url: 'https://admin.eu.astrale.ai/api',
20
+ domainIssuer: 'https://admin.astrale.ai',
21
+ }
22
+ : {
23
+ url: 'https://admin.eu.beta.astrale.ai/api',
24
+ domainIssuer: 'https://admin.beta.astrale.ai',
25
+ }
26
+ }
27
+
28
+ const DEFAULT_ADMIN_TARGET = defaultAdminTargetForChannel(DEFAULT_UPDATE_CHANNEL)
29
+
30
+ export const DEFAULT_ADMIN_TARGET_URL = DEFAULT_ADMIN_TARGET.url
31
+ export const DEFAULT_ADMIN_DOMAIN_ISSUER = DEFAULT_ADMIN_TARGET.domainIssuer
13
32
 
14
33
  export const DEFAULT_ADMIN_TARGET_CONFIG = {
15
34
  name: DEFAULT_ADMIN_TARGET_NAME,
@@ -2,8 +2,6 @@ import type { Command, CommanderError } from 'commander'
2
2
 
3
3
  import chalk from 'chalk'
4
4
 
5
- import { isMachine } from './output'
6
-
7
5
  export type CommandCatalogEntry = {
8
6
  path: string[]
9
7
  usage: string
@@ -30,27 +28,27 @@ export function renderCommanderError(
30
28
  program: Command,
31
29
  error: CommanderError,
32
30
  argv = process.argv.slice(2),
31
+ machine = argv.some((token) => token === '--ci' || token === '--json' || token === '--raw') ||
32
+ !(process.stdout.isTTY ?? false),
33
33
  ): string {
34
34
  const tokens = stripOptions(argv)
35
35
  const catalog = collectCommandCatalog(program)
36
36
  const matched = matchRegisteredPrefix(program, tokens)
37
37
 
38
38
  if (matched.path.length === 0 && tokens.length > 0) {
39
- return maybeMachine(renderUnknownCommand(tokens, catalog))
39
+ return maybeMachine(renderUnknownCommand(tokens, catalog), machine)
40
40
  }
41
41
 
42
42
  if (error.code === 'commander.missingArgument') {
43
43
  const usage = usageFor(matched.path, matched.command)
44
- const argName = error.message.match(/'([^']+)'/)?.[1]
45
44
  return maybeMachine(
46
45
  [
47
- `Missing required argument${argName ? ` ${chalk.bold(`<${argName}>`)}` : ''} for ${chalk.bold(
48
- `astrale ${matched.path.join(' ')}`,
49
- )}`,
46
+ `Missing required argument for ${chalk.bold(`astrale ${matched.path.join(' ')}`)}`,
50
47
  '',
51
48
  'Usage:',
52
49
  ` astrale ${usage}`,
53
50
  ].join('\n'),
51
+ machine,
54
52
  )
55
53
  }
56
54
 
@@ -66,6 +64,7 @@ export function renderCommanderError(
66
64
  'Usage:',
67
65
  ` astrale ${usage}`,
68
66
  ].join('\n'),
67
+ machine,
69
68
  )
70
69
  }
71
70
 
@@ -77,6 +76,7 @@ export function renderCommanderError(
77
76
  ? ['', 'Did you mean:', ...suggestions.map((s) => ` astrale ${s}`)]
78
77
  : []),
79
78
  ].join('\n'),
79
+ machine,
80
80
  )
81
81
  }
82
82
 
@@ -143,8 +143,8 @@ function usageFor(path: string[], command: Command): string {
143
143
 
144
144
  const ANSI_RE = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g')
145
145
 
146
- function maybeMachine(text: string): string {
147
- if (!isMachine()) return text
146
+ function maybeMachine(text: string, machine: boolean): string {
147
+ if (!machine) return text
148
148
  const plain = text.replace(ANSI_RE, '')
149
149
  const first = plain.split('\n').find((line) => line.trim().length > 0) ?? plain
150
150
  return JSON.stringify({ error: 'USAGE_ERROR', message: first, detail: plain })
@@ -0,0 +1,104 @@
1
+ import { publication } from '@astrale-os/sdk/publication'
2
+
3
+ const MAXIMUM_PUBLICATION_BYTES = 1024 * 1024
4
+ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
5
+
6
+ /** Fetch and admit one canonical Domain Publication without following its bundle. */
7
+ export async function fetchDomainPublication(
8
+ input: string,
9
+ signal?: AbortSignal,
10
+ fetchImpl: FetchLike = globalThis.fetch,
11
+ ): Promise<publication.Publication> {
12
+ const url = domainPublicationUrl(input)
13
+ const response = await fetchImpl(url, {
14
+ redirect: 'error',
15
+ ...(signal === undefined ? {} : { signal }),
16
+ })
17
+ if (!response.ok) {
18
+ await cancel(response.body)
19
+ throw new Error(`GET ${url.href} → ${response.status}`)
20
+ }
21
+ const bytes = await readBounded(response, url)
22
+ try {
23
+ const source = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
24
+ return publication.accept(JSON.parse(source) as unknown)
25
+ } catch (cause) {
26
+ throw new Error(`GET ${url.href} returned an invalid Domain Publication`, { cause })
27
+ }
28
+ }
29
+
30
+ async function readBounded(response: Response, url: URL): Promise<Uint8Array> {
31
+ const declared = response.headers.get('content-length')
32
+ if (
33
+ declared !== null &&
34
+ (!/^\d+$/u.test(declared) ||
35
+ !Number.isSafeInteger(Number(declared)) ||
36
+ Number(declared) > MAXIMUM_PUBLICATION_BYTES)
37
+ ) {
38
+ await cancel(response.body)
39
+ throw sizeError(url)
40
+ }
41
+ if (response.body === null) return new Uint8Array()
42
+
43
+ const reader = response.body.getReader()
44
+ const chunks: Uint8Array[] = []
45
+ let size = 0
46
+ let complete = false
47
+ try {
48
+ for (;;) {
49
+ const next = await reader.read()
50
+ if (next.done) {
51
+ complete = true
52
+ break
53
+ }
54
+ if (next.value.byteLength === 0) continue
55
+ if (next.value.byteLength > MAXIMUM_PUBLICATION_BYTES - size) {
56
+ throw sizeError(url)
57
+ }
58
+ chunks.push(next.value)
59
+ size += next.value.byteLength
60
+ }
61
+ } finally {
62
+ if (!complete) await reader.cancel().catch(() => undefined)
63
+ reader.releaseLock()
64
+ }
65
+
66
+ const bytes = new Uint8Array(size)
67
+ let offset = 0
68
+ for (const chunk of chunks) {
69
+ bytes.set(chunk, offset)
70
+ offset += chunk.byteLength
71
+ }
72
+ return bytes
73
+ }
74
+
75
+ function sizeError(url: URL): Error {
76
+ return new Error(`GET ${url.href} exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`)
77
+ }
78
+
79
+ async function cancel(body: ReadableStream<Uint8Array> | null): Promise<void> {
80
+ if (body !== null) await body.cancel().catch(() => undefined)
81
+ }
82
+
83
+ /** Canonical discovery document for a Domain origin or exact Publication URL. */
84
+ export function domainPublicationUrl(input: string): URL {
85
+ let url: URL
86
+ try {
87
+ url = new URL(input)
88
+ } catch {
89
+ throw new TypeError(`Domain deployment URL is invalid: ${input}`)
90
+ }
91
+ if (
92
+ !['http:', 'https:'].includes(url.protocol) ||
93
+ url.username !== '' ||
94
+ url.password !== '' ||
95
+ url.search !== '' ||
96
+ url.hash !== '' ||
97
+ !['', '/', publication.PATH].includes(url.pathname)
98
+ ) {
99
+ throw new TypeError(
100
+ `Domain deployment URL must be an HTTP(S) origin or canonical Publication URL: ${input}`,
101
+ )
102
+ }
103
+ return new URL(publication.PATH, url.origin)
104
+ }
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk'
2
+
3
+ const MAXIMUM_DEBUG_FAILURES = 32
4
+
5
+ export function printFailureDebug(error: unknown, url: string): void {
6
+ process.stderr.write(`\n${chalk.dim('── debug ─────────────────')}\n`)
7
+ if (url) process.stderr.write(`${chalk.dim(`url: ${url}`)}\n`)
8
+ const seen = new Set<unknown>()
9
+ const pending: Array<{ value: unknown; relation: string }> = [{ value: error, relation: 'error' }]
10
+ while (pending.length > 0 && seen.size < MAXIMUM_DEBUG_FAILURES) {
11
+ const { value, relation } = pending.shift()!
12
+ if (seen.has(value)) continue
13
+ seen.add(value)
14
+ if (!(value instanceof Error)) {
15
+ process.stderr.write(`${chalk.dim(`${relation}: ${String(value)}`)}\n`)
16
+ continue
17
+ }
18
+ process.stderr.write(
19
+ `${chalk.dim(`${relation}: ${value.constructor.name}: ${value.message}`)}\n`,
20
+ )
21
+ if (value.stack) process.stderr.write(`${chalk.dim(value.stack)}\n`)
22
+ if (value.cause !== undefined) pending.push({ value: value.cause, relation: 'caused by' })
23
+ if (value instanceof AggregateError)
24
+ for (const child of value.errors) pending.push({ value: child, relation: 'aggregate' })
25
+ }
26
+ }
package/src/lib/idp.ts CHANGED
@@ -145,6 +145,8 @@ export const IdpSessionSchema = z
145
145
  idp: z.string(),
146
146
  issuer: z.string().url(),
147
147
  subject: z.string(),
148
+ /** OAuth client that issued this session's refresh token. */
149
+ clientId: z.string().optional(),
148
150
  audience: z.string().optional(),
149
151
  organizationId: z.string().optional(),
150
152
  access_token: z.string(),
@@ -212,6 +214,8 @@ export type IdpConfig = {
212
214
 
213
215
  export const BUILTIN_WORKOS_IDP_NAME = 'workos'
214
216
  const DEFAULT_WORKOS_API_HOST = 'https://api.workos.com'
217
+ const DEFAULT_WORKOS_CLIENT_ID = 'client_01KC29HET5F3QAQ8GNTPZ7F320'
218
+ const LEGACY_WORKOS_CLIENT_ID = 'client_01KC29HEGD7B40TV2C4QZ436BG'
215
219
  const WORKOS_CLIENT_ID_ENV_NAMES = ['WORKOS_CLIENT_ID', 'VITE_WORKOS_CLIENT_ID'] as const
216
220
 
217
221
  export function idpDir(name: string): string {
@@ -278,7 +282,17 @@ export async function readIdpConfigOrBuiltin(
278
282
  validateName(name, 'IdP')
279
283
  const store = await readIdpStore()
280
284
  if (store.idps[name]) {
281
- return await readIdpConfig(name)
285
+ const existing = await readIdpConfig(name)
286
+ const replacement = opts.persist
287
+ ? legacyBuiltinWorkosReplacement(existing, opts.clientId)
288
+ : undefined
289
+ if (!replacement) return existing
290
+ return upsertIdpConfig({
291
+ name: replacement.name,
292
+ metadata: replacement.metadata,
293
+ client: replacement.client,
294
+ builtIn: true,
295
+ })
282
296
  }
283
297
 
284
298
  const builtin = builtinIdpConfig(name, opts.clientId)
@@ -411,11 +425,30 @@ export function builtinIdpConfig(
411
425
  }
412
426
 
413
427
  export function workosClientIdFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
414
- return WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
415
- (value): value is string => typeof value === 'string' && value.length > 0,
428
+ return (
429
+ WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
430
+ (value): value is string => typeof value === 'string' && value.length > 0,
431
+ ) ?? DEFAULT_WORKOS_CLIENT_ID
416
432
  )
417
433
  }
418
434
 
435
+ export function legacyBuiltinWorkosReplacement(
436
+ existing: IdpConfig,
437
+ clientIdOverride?: string,
438
+ env: NodeJS.ProcessEnv = process.env,
439
+ ): IdpConfig | undefined {
440
+ if (
441
+ existing.name !== BUILTIN_WORKOS_IDP_NAME ||
442
+ !existing.entry.builtIn ||
443
+ existing.client.client_id !== LEGACY_WORKOS_CLIENT_ID
444
+ ) {
445
+ return undefined
446
+ }
447
+ const replacement = builtinIdpConfig(existing.name, clientIdOverride, env)
448
+ if (!replacement || replacement.client.client_id === LEGACY_WORKOS_CLIENT_ID) return undefined
449
+ return replacement
450
+ }
451
+
419
452
  async function fetchOAuthAuthorizationServerMetadata(
420
453
  issuer: string,
421
454
  ): Promise<Partial<OidcMetadata> | undefined> {
@@ -644,10 +677,11 @@ export async function refreshSession(
644
677
  if (!session.refresh_token)
645
678
  throw new Error(`IdP session for "${identityName}" has no refresh token`)
646
679
  const idp = await readIdpConfig(session.idp)
680
+ const clientId = requireClientId(idp, session.clientId)
647
681
  const params = new URLSearchParams({
648
682
  grant_type: 'refresh_token',
649
683
  refresh_token: session.refresh_token,
650
- client_id: requireClientId(idp),
684
+ client_id: clientId,
651
685
  })
652
686
  const audience = opts.audience ?? session.audience
653
687
  if (audience) params.set('audience', audience)
@@ -674,6 +708,7 @@ export async function refreshSession(
674
708
  const expiresAt = tokenExpiresAt(token)
675
709
  const next: IdpSession = {
676
710
  ...session,
711
+ clientId,
677
712
  access_token: token.access_token,
678
713
  id_token: token.id_token ?? session.id_token,
679
714
  refresh_token: token.refresh_token ?? session.refresh_token,
@@ -1,6 +1,6 @@
1
1
  import type { InstanceEntry, InstanceStore } from './instance'
2
2
 
3
- import { formatInstanceLocation, type OwnedInstanceInfo } from './admin-instance'
3
+ import { formatInstanceState, type OwnedInstanceInfo } from './admin-instance'
4
4
  import { normalizeInstanceKernelUrl, resolveInstanceKey } from './instance'
5
5
 
6
6
  export type InstanceCandidate =
@@ -44,6 +44,6 @@ export function describeInstanceCandidate(candidate: InstanceCandidate): string
44
44
  if (candidate.source === 'bookmark') {
45
45
  return `${candidate.key} (bookmark) ${candidate.url}`
46
46
  }
47
- const extra = formatInstanceLocation(candidate.info)
47
+ const extra = formatInstanceState(candidate.info)
48
48
  return `${candidate.key} (managed) ${candidate.url}${extra ? ` — ${extra}` : ''}`
49
49
  }