@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
@@ -6,6 +6,7 @@ import {
6
6
  DEFAULT_ADMIN_TARGET_NAME,
7
7
  DEFAULT_ADMIN_DOMAIN_ISSUER,
8
8
  DEFAULT_ADMIN_TARGET_URL,
9
+ defaultAdminTargetForChannel,
9
10
  resolveAdminTargetFromStore,
10
11
  } from '../admin-target'
11
12
  import { DEFAULT_CONFIG, type AstraleConfig } from '../config'
@@ -32,6 +33,17 @@ const instances: InstanceStore = {
32
33
  }
33
34
 
34
35
  describe('resolveAdminTargetFromStore', () => {
36
+ test('derives hosted Admin defaults from the release channel', () => {
37
+ expect(defaultAdminTargetForChannel('beta')).toEqual({
38
+ url: 'https://admin.eu.beta.astrale.ai/api',
39
+ domainIssuer: 'https://admin.beta.astrale.ai',
40
+ })
41
+ expect(defaultAdminTargetForChannel('stable')).toEqual({
42
+ url: 'https://admin.eu.astrale.ai/api',
43
+ domainIssuer: 'https://admin.astrale.ai',
44
+ })
45
+ })
46
+
35
47
  test('uses hosted admin default without active instance coupling', () => {
36
48
  expect(resolveAdminTargetFromStore({}, DEFAULT_CONFIG, instances)).toEqual({
37
49
  name: DEFAULT_ADMIN_TARGET_NAME,
@@ -16,7 +16,7 @@ function dx(
16
16
  error: CommanderError,
17
17
  argv: string[],
18
18
  ) {
19
- const rendered = JSON.parse(stripAnsi(renderCommanderError(program, error, argv))) as {
19
+ const rendered = JSON.parse(stripAnsi(renderCommanderError(program, error, argv, true))) as {
20
20
  error: string
21
21
  message: string
22
22
  detail: string
@@ -0,0 +1,138 @@
1
+ import { publication } from '@astrale-os/sdk/publication'
2
+ import { defineSchema } from '@astrale-os/sdk/schema'
3
+ import { describe, expect, test } from 'bun:test'
4
+
5
+ import { releaseFor } from '../../__tests__/fixtures/publication'
6
+ import { domainPublicationUrl, fetchDomainPublication } from '../domain-publication'
7
+
8
+ const ROOT = 'https://publication-reader.example.dev'
9
+ const PUBLICATION_URL = `${ROOT}/.well-known/astrale/domain.json`
10
+ const MAXIMUM_PUBLICATION_BYTES = 1024 * 1024
11
+ const schema = defineSchema('publication-reader.example.dev', {})
12
+ const deployed = releaseFor(schema, ROOT).publication
13
+
14
+ describe('Domain Publication reader', () => {
15
+ test('fetches only the canonical path with redirects disabled', async () => {
16
+ const requests: Array<{ readonly url: string; readonly redirect?: RequestRedirect }> = []
17
+ const fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
18
+ requests.push({ url: String(input), redirect: init?.redirect })
19
+ return Response.json(deployed)
20
+ }
21
+
22
+ await expect(fetchDomainPublication(`${ROOT}/`, undefined, fetch)).resolves.toEqual(deployed)
23
+ expect(requests).toEqual([{ url: PUBLICATION_URL, redirect: 'error' }])
24
+ })
25
+
26
+ test('normalizes an origin and admits the exact canonical Publication URL', () => {
27
+ expect(domainPublicationUrl(ROOT).href).toBe(PUBLICATION_URL)
28
+ expect(domainPublicationUrl(PUBLICATION_URL).href).toBe(PUBLICATION_URL)
29
+ })
30
+
31
+ test('propagates redirect rejection instead of accepting another target', async () => {
32
+ const fetch = async (_input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
33
+ expect(init?.redirect).toBe('error')
34
+ throw new TypeError('redirect rejected')
35
+ }
36
+
37
+ await expect(fetchDomainPublication(ROOT, undefined, fetch)).rejects.toThrow(
38
+ 'redirect rejected',
39
+ )
40
+ })
41
+
42
+ test('stops at one MiB and cancels an over-limit response stream', async () => {
43
+ let pulls = 0
44
+ let cancelled = false
45
+ const body = new ReadableStream<Uint8Array>(
46
+ {
47
+ pull(controller) {
48
+ pulls += 1
49
+ if (pulls === 1) {
50
+ controller.enqueue(new Uint8Array(MAXIMUM_PUBLICATION_BYTES))
51
+ return
52
+ }
53
+ if (pulls === 2) {
54
+ controller.enqueue(Uint8Array.of(0))
55
+ return
56
+ }
57
+ controller.error(new Error('reader consumed beyond its byte limit'))
58
+ },
59
+ cancel() {
60
+ cancelled = true
61
+ },
62
+ },
63
+ { highWaterMark: 0 },
64
+ )
65
+ const fetch = async (): Promise<Response> => new Response(body)
66
+
67
+ await expect(fetchDomainPublication(ROOT, undefined, fetch)).rejects.toThrow(
68
+ `exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`,
69
+ )
70
+ expect(pulls).toBe(2)
71
+ expect(cancelled).toBe(true)
72
+ })
73
+
74
+ test('rejects an oversized Content-Length before pulling and cancels the body', async () => {
75
+ let pulls = 0
76
+ let cancelled = false
77
+ const body = new ReadableStream<Uint8Array>(
78
+ {
79
+ pull() {
80
+ pulls += 1
81
+ },
82
+ cancel() {
83
+ cancelled = true
84
+ },
85
+ },
86
+ { highWaterMark: 0 },
87
+ )
88
+ const fetch = async (): Promise<Response> =>
89
+ new Response(body, {
90
+ headers: { 'content-length': String(MAXIMUM_PUBLICATION_BYTES + 1) },
91
+ })
92
+
93
+ await expect(fetchDomainPublication(ROOT, undefined, fetch)).rejects.toThrow(
94
+ `exceeded ${MAXIMUM_PUBLICATION_BYTES} bytes`,
95
+ )
96
+ expect(pulls).toBe(0)
97
+ expect(cancelled).toBe(true)
98
+ })
99
+
100
+ test.each([
101
+ ['invalid UTF-8', Uint8Array.of(0xc3, 0x28), TypeError],
102
+ ['invalid JSON', new TextEncoder().encode('{'), SyntaxError],
103
+ ['invalid Publication', new TextEncoder().encode('{}'), publication.PublicationError],
104
+ ] as const)('rejects %s before returning a Publication', async (_label, bytes, Cause) => {
105
+ const fetch = async (): Promise<Response> => new Response(bytes)
106
+ const error = await rejection(fetchDomainPublication(ROOT, undefined, fetch))
107
+
108
+ expect(error.message).toBe(`GET ${PUBLICATION_URL} returned an invalid Domain Publication`)
109
+ expect(error.cause).toBeInstanceOf(Cause)
110
+ })
111
+
112
+ test.each([
113
+ `${ROOT}/nested`,
114
+ `${ROOT}?deployment=current`,
115
+ `${ROOT}#fragment`,
116
+ 'https://user:secret@publication-reader.example.dev',
117
+ 'ftp://publication-reader.example.dev',
118
+ ])('rejects a non-origin deployment URL before fetch: %s', async (input) => {
119
+ let called = false
120
+ const fetch = async (): Promise<Response> => {
121
+ called = true
122
+ return Response.json(deployed)
123
+ }
124
+
125
+ await expect(fetchDomainPublication(input, undefined, fetch)).rejects.toBeInstanceOf(TypeError)
126
+ expect(called).toBe(false)
127
+ })
128
+ })
129
+
130
+ async function rejection(input: Promise<unknown>): Promise<Error> {
131
+ try {
132
+ await input
133
+ } catch (cause) {
134
+ if (cause instanceof Error) return cause
135
+ throw new Error('Expected an Error rejection.', { cause })
136
+ }
137
+ throw new Error('Expected the promise to reject.')
138
+ }
@@ -68,11 +68,29 @@ describe('ensureFreshSession', () => {
68
68
  expect(result.orgHintCalls).toBe(1)
69
69
  expect(server.refreshCount()).toBe(1)
70
70
  const session = await readSession()
71
+ expect(session.clientId).toBe('c_1')
71
72
  expect(session.refresh_token).toBe('rt-1')
72
73
  expect((session.tokens as Record<string, { access_token: string }>)[AUD].access_token).toBe(
73
74
  result.token as string,
74
75
  )
75
76
  expect(server.lastOrganizationId()).toBe('org_1')
77
+ expect(server.lastClientId()).toBe('c_1')
78
+ } finally {
79
+ await server.stop()
80
+ }
81
+ })
82
+
83
+ test('refreshes with the client that issued the session instead of the IdP default', async () => {
84
+ const server = rotationServer()
85
+ try {
86
+ await writeIdpConfig(server.url)
87
+ await writeSession({ clientId: 'c_session', expires_at: past() })
88
+
89
+ const result = await runDriver('ensure', { DRIVER_AUDIENCE: AUD })
90
+
91
+ expect(result.ok).toBe(true)
92
+ expect(server.lastClientId()).toBe('c_session')
93
+ expect((await readSession()).clientId).toBe('c_session')
76
94
  } finally {
77
95
  await server.stop()
78
96
  }
@@ -273,6 +291,7 @@ function rotationServer(opts?: {
273
291
  }): {
274
292
  url: string
275
293
  refreshCount: () => number
294
+ lastClientId: () => string | undefined
276
295
  lastOrganizationId: () => string | undefined
277
296
  stop: () => Promise<void>
278
297
  } {
@@ -280,6 +299,7 @@ function rotationServer(opts?: {
280
299
  // refresh token is invalid_grant.
281
300
  let current = 'rt-0'
282
301
  let count = 0
302
+ let lastClientId: string | undefined
283
303
  let lastOrgId: string | undefined
284
304
  const server = Bun.serve({
285
305
  port: 0,
@@ -300,6 +320,7 @@ function rotationServer(opts?: {
300
320
  if (opts?.delayMs) await new Promise((r) => setTimeout(r, opts.delayMs))
301
321
  count += 1
302
322
  current = `rt-${count}`
323
+ lastClientId = form.get('client_id') ?? undefined
303
324
  lastOrgId = form.get('organization_id') ?? undefined
304
325
  return Response.json({
305
326
  access_token: unsignedJwt({ aud: AUD, exp: futureEpoch() }),
@@ -312,6 +333,7 @@ function rotationServer(opts?: {
312
333
  return {
313
334
  url: `http://${server.hostname}:${server.port}`,
314
335
  refreshCount: () => count,
336
+ lastClientId: () => lastClientId,
315
337
  lastOrganizationId: () => lastOrgId,
316
338
  stop: () => server.stop(true),
317
339
  }
@@ -9,6 +9,7 @@ import {
9
9
  IdpSessionSchema,
10
10
  isSessionExpired,
11
11
  issuerFromToken,
12
+ legacyBuiltinWorkosReplacement,
12
13
  normalizeTokenResponse,
13
14
  OAuthTokenError,
14
15
  OidcMetadataSchema,
@@ -63,8 +64,11 @@ describe('IdP schemas and token helpers', () => {
63
64
  expect(config?.client.token_request_format).toBe('json')
64
65
  })
65
66
 
66
- test('does not invent a WorkOS environment when no client id is configured', () => {
67
- expect(builtinIdpConfig('workos', undefined, {})).toBeNull()
67
+ test('builds the built-in WorkOS IdP with the public client id by default', () => {
68
+ const config = builtinIdpConfig('workos', undefined, {})
69
+
70
+ expect(config?.entry.builtIn).toBe(true)
71
+ expect(config?.client.client_id).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
68
72
  })
69
73
 
70
74
  test('reads WorkOS client id from WORKOS_CLIENT_ID before VITE_WORKOS_CLIENT_ID', () => {
@@ -75,8 +79,34 @@ describe('IdP schemas and token helpers', () => {
75
79
  }),
76
80
  ).toBe('client_primary')
77
81
  expect(workosClientIdFromEnv({ VITE_WORKOS_CLIENT_ID: 'client_vite' })).toBe('client_vite')
78
- expect(workosClientIdFromEnv({})).toBeUndefined()
79
- expect(workosClientIdFromEnv({ WORKOS_CLIENT_ID: ' ' })).toBeUndefined()
82
+ expect(workosClientIdFromEnv({})).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
83
+ expect(workosClientIdFromEnv({ WORKOS_CLIENT_ID: ' ' })).toBe(
84
+ 'client_01KC29HET5F3QAQ8GNTPZ7F320',
85
+ )
86
+ })
87
+
88
+ test('replaces only the legacy persisted built-in WorkOS client', () => {
89
+ const legacy = builtinIdpConfig('workos', 'client_01KC29HEGD7B40TV2C4QZ436BG', {})!
90
+ const replacement = legacyBuiltinWorkosReplacement(legacy, undefined, {})
91
+
92
+ expect(replacement?.client.client_id).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
93
+ expect(replacement?.metadata.jwks_uri).toBe(
94
+ 'https://api.workos.com/sso/jwks/client_01KC29HET5F3QAQ8GNTPZ7F320',
95
+ )
96
+ expect(
97
+ legacyBuiltinWorkosReplacement(
98
+ { ...legacy, entry: { ...legacy.entry, builtIn: false } },
99
+ undefined,
100
+ {},
101
+ ),
102
+ ).toBeUndefined()
103
+ expect(
104
+ legacyBuiltinWorkosReplacement(
105
+ { ...legacy, client: { ...legacy.client, client_id: 'client_custom' } },
106
+ undefined,
107
+ {},
108
+ ),
109
+ ).toBeUndefined()
80
110
  })
81
111
 
82
112
  test('stores client secret env names, not secret material', () => {
@@ -100,6 +130,7 @@ describe('IdP schemas and token helpers', () => {
100
130
  idp: 'workos',
101
131
  issuer: 'https://example.authkit.app',
102
132
  subject: 'user_123',
133
+ clientId: 'client_123',
103
134
  access_token: 'opaque-token',
104
135
  refresh_token: 'refresh-token',
105
136
  token_type: 'Bearer',
@@ -110,6 +141,7 @@ describe('IdP schemas and token helpers', () => {
110
141
  })
111
142
 
112
143
  expect(session.claims?.email).toBe('alice@example.com')
144
+ expect(session.clientId).toBe('client_123')
113
145
  expect(session.refresh_token).toBe('refresh-token')
114
146
  })
115
147
 
@@ -18,7 +18,6 @@ const managedDemo: OwnedInstanceInfo = {
18
18
  slug: 'demo',
19
19
  url: 'https://demo.eu.astrale.ai',
20
20
  state: 'ready',
21
- region: 'eu',
22
21
  }
23
22
 
24
23
  describe('collectInstanceCandidates', () => {
@@ -138,11 +137,10 @@ describe('describeInstanceCandidate', () => {
138
137
  slug: 'twin',
139
138
  url: 'https://twin.eu.astrale.ai',
140
139
  state: 'ready',
141
- region: 'eu',
142
140
  },
143
141
  ])
144
142
  expect(describeInstanceCandidate(managed!)).toBe(
145
- 'twin (managed) https://twin.eu.astrale.ai/api — eu',
143
+ 'twin (managed) https://twin.eu.astrale.ai/api',
146
144
  )
147
145
  })
148
146
  })
@@ -3,6 +3,7 @@ import { describe, expect, test } from 'bun:test'
3
3
  import type { AstraleConfig } from '../config'
4
4
  import type { InstanceStore } from '../instance'
5
5
 
6
+ import { AdminInstanceNotFoundError } from '../../admin/instance/model'
6
7
  import { AstraleError } from '../../errors'
7
8
  import { DEFAULT_CONFIG } from '../config'
8
9
  import {
@@ -112,6 +113,7 @@ describe('resolveInstanceTarget', () => {
112
113
  id: 'node-1',
113
114
  slug,
114
115
  url: `https://${slug}.eu.astrale.ai`,
116
+ state: 'ready',
115
117
  }),
116
118
  },
117
119
  ),
@@ -124,9 +126,6 @@ describe('resolveInstanceTarget', () => {
124
126
  })
125
127
 
126
128
  test('unknown slugs surface the original instance-not-found error', async () => {
127
- const notFound = new Error('Path not found: "/admin/instances/missing"')
128
- notFound.name = 'NotFoundError'
129
-
130
129
  await expect(
131
130
  resolveInstanceTarget(
132
131
  { source: 'name', name: 'missing' },
@@ -134,7 +133,7 @@ describe('resolveInstanceTarget', () => {
134
133
  config: DEFAULT_CONFIG,
135
134
  instances: emptyStore,
136
135
  managed: async () => {
137
- throw notFound
136
+ throw new AdminInstanceNotFoundError('missing')
138
137
  },
139
138
  },
140
139
  ),
@@ -168,14 +167,8 @@ describe('instance target helpers', () => {
168
167
  expect(couldBeConfiguredAdminInstance('bryan', directAdminConfig)).toBe(false)
169
168
  })
170
169
 
171
- test('treats wrapped remote Instance.info misses as managed lookup not-found', () => {
172
- const wrapped = new Error('Path not found: "/admin/instances/admin"') as Error & {
173
- data?: unknown
174
- }
175
- wrapped.name = 'KernelError'
176
- wrapped.data = { type: 'NotFoundError' }
177
-
178
- expect(isManagedInstanceNotFound(wrapped)).toBe(true)
170
+ test('treats the owner-scoped Admin error as managed lookup not-found', () => {
171
+ expect(isManagedInstanceNotFound(new AdminInstanceNotFoundError('admin'))).toBe(true)
179
172
  })
180
173
 
181
174
  test('does not hide non-not-found managed lookup failures', () => {
@@ -194,37 +187,12 @@ describe('instance target helpers', () => {
194
187
  })
195
188
  })
196
189
 
197
- describe('isManagedInstanceNotFound: kernel InternalKernelError wrap', () => {
198
- test('NOT_FOUND-prefixed InternalKernelError is an instance miss', () => {
199
- const e = new Error('NOT_FOUND: no instance node for "knowledge"')
200
- e.name = 'InternalKernelError'
201
- expect(isManagedInstanceNotFound(e)).toBe(true)
202
- })
203
- test('other InternalKernelError messages are not swallowed', () => {
190
+ describe('isManagedInstanceNotFound: unknown errors', () => {
191
+ test('does not infer a miss from an InternalKernelError message', () => {
204
192
  const e = new Error('DISPATCH_FAILED: handler crashed')
205
193
  e.name = 'InternalKernelError'
206
194
  expect(isManagedInstanceNotFound(e)).toBe(false)
207
195
  })
208
- test('resolveNamedInstanceTarget maps the wrap to typed INSTANCE_NOT_FOUND', async () => {
209
- const managed = async () => {
210
- const e = new Error('NOT_FOUND: no instance node for "ghost"')
211
- e.name = 'InternalKernelError'
212
- throw e
213
- }
214
- const opts = {
215
- config: DEFAULT_CONFIG,
216
- instances: { instances: {} },
217
- managed,
218
- } as unknown as Parameters<typeof resolveInstanceTarget>[1]
219
- let caught: unknown
220
- try {
221
- await resolveInstanceTarget({ source: 'name', name: 'ghost' }, opts)
222
- } catch (e) {
223
- caught = e
224
- }
225
- expect(caught).toBeInstanceOf(AstraleError)
226
- expect((caught as AstraleError).code).toBe('INSTANCE_NOT_FOUND')
227
- })
228
196
 
229
197
  test('maps Admin token-exchange failure to INSTANCE_NOT_FOUND', async () => {
230
198
  const managed = async () => {
@@ -74,6 +74,12 @@ describe('InstanceStoreSchema', () => {
74
74
  expect(normalizeInstanceKernelUrl('https://testmarc.eu.astrale.ai/')).toBe(
75
75
  'https://testmarc.eu.astrale.ai/api',
76
76
  )
77
+ expect(normalizeInstanceKernelUrl('https://testmarc.eu.beta.astrale.ai')).toBe(
78
+ 'https://testmarc.eu.beta.astrale.ai/api',
79
+ )
80
+ expect(normalizeInstanceKernelUrl('https://shell.beta.astrale.ai')).toBe(
81
+ 'https://shell.beta.astrale.ai',
82
+ )
77
83
  })
78
84
 
79
85
  test('does not rewrite explicit kernel or non-managed URLs', () => {
@@ -0,0 +1,64 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+
3
+ import { AstraleError } from '../../errors'
4
+ import { failInput, fatal } from '../log'
5
+
6
+ class ExitError extends Error {}
7
+
8
+ let stderr = ''
9
+ let exit: typeof process.exit
10
+ let write: typeof process.stderr.write
11
+
12
+ beforeEach(() => {
13
+ stderr = ''
14
+ exit = process.exit
15
+ write = process.stderr.write
16
+ process.exit = (() => {
17
+ throw new ExitError()
18
+ }) as typeof process.exit
19
+ process.stderr.write = ((chunk: string | Uint8Array) => {
20
+ stderr += String(chunk)
21
+ return true
22
+ }) as typeof process.stderr.write
23
+ })
24
+
25
+ afterEach(() => {
26
+ process.exit = exit
27
+ process.stderr.write = write
28
+ })
29
+
30
+ describe('terminal error safety', () => {
31
+ test('renders an empty aggregate as an honest non-blank unexpected failure', () => {
32
+ expect(() => fatal(new AggregateError([], ''), { json: true })).toThrow(ExitError)
33
+ expect(JSON.parse(stderr)).toEqual({
34
+ error: 'UNEXPECTED_ERROR',
35
+ message: 'The CLI encountered an unexpected internal failure.',
36
+ })
37
+ })
38
+
39
+ test('retains unknown failure evidence only under explicit debug output', () => {
40
+ const failure = new AggregateError([new Error('provider rejected organization scope')], '')
41
+
42
+ expect(() => fatal(failure, { json: true, debug: true })).toThrow(ExitError)
43
+
44
+ expect(stderr).toContain(
45
+ '{"error":"UNEXPECTED_ERROR","message":"The CLI encountered an unexpected internal failure."}',
46
+ )
47
+ expect(stderr).toContain('aggregate: Error: provider rejected organization scope')
48
+ })
49
+
50
+ test('rejects blank admitted CLI diagnostics', () => {
51
+ expect(() => new AstraleError('INVALID_INPUT', ' ')).toThrow(TypeError)
52
+ })
53
+
54
+ test('labels only expected admission failures as invalid input', () => {
55
+ expect(() => failInput(new TypeError('Authored value is invalid.'), { json: true })).toThrow(
56
+ ExitError,
57
+ )
58
+ expect(JSON.parse(stderr)).toMatchObject({ error: 'INVALID_INPUT' })
59
+
60
+ stderr = ''
61
+ expect(() => failInput(new AggregateError([], ''), { json: true })).toThrow(ExitError)
62
+ expect(JSON.parse(stderr)).toMatchObject({ error: 'UNEXPECTED_ERROR' })
63
+ })
64
+ })
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { readFileSync } from 'node:fs'
3
- import { join } from 'node:path'
2
+ import { readFileSync, readdirSync, statSync } from 'node:fs'
3
+ import { join, relative } from 'node:path'
4
4
  import { Project } from 'ts-morph'
5
5
 
6
6
  /**
@@ -19,6 +19,18 @@ import { Project } from 'ts-morph'
19
19
 
20
20
  const CLI_ROOT = new URL('../../../', import.meta.url).pathname
21
21
 
22
+ interface PackageManifest {
23
+ dependencies?: Record<string, string>
24
+ scripts?: Record<string, string>
25
+ }
26
+
27
+ const cliPackage = JSON.parse(
28
+ readFileSync(join(CLI_ROOT, 'package.json'), 'utf8'),
29
+ ) as PackageManifest
30
+ const studioPackage = JSON.parse(
31
+ readFileSync(join(CLI_ROOT, 'studio/package.json'), 'utf8'),
32
+ ) as PackageManifest
33
+
22
34
  /** Base package of a bare specifier: `@scope/pkg/sub` → `@scope/pkg`, `pkg/sub` → `pkg`. */
23
35
  function basePackage(spec: string): string {
24
36
  const parts = spec.split('/')
@@ -29,26 +41,33 @@ function isBuiltin(spec: string): boolean {
29
41
  return spec.startsWith('node:') || spec === 'bun' || spec.startsWith('bun:')
30
42
  }
31
43
 
44
+ function runtimeFiles(directory: string): string[] {
45
+ return readdirSync(directory).flatMap((entry) => {
46
+ const path = join(directory, entry)
47
+ if (statSync(path).isDirectory()) return runtimeFiles(path)
48
+ return /\.(?:ts|tsx)$/.test(path) && !/\.test\.(?:ts|tsx)$/.test(path) ? [path] : []
49
+ })
50
+ }
51
+
52
+ const studioPath = (path: string) => relative(CLI_ROOT, path).replaceAll('\\', '/')
53
+
32
54
  describe('studio server runtime deps', () => {
33
- const deps = new Set(
34
- Object.keys(
35
- (
36
- JSON.parse(readFileSync(join(CLI_ROOT, 'package.json'), 'utf8')) as {
37
- dependencies?: Record<string, string>
38
- }
39
- ).dependencies ?? {},
40
- ),
41
- )
55
+ const deps = new Set(Object.keys(cliPackage.dependencies ?? {}))
42
56
 
43
57
  const project = new Project({ skipAddingFilesFromTsConfig: true })
44
58
  project.addSourceFilesAtPaths([
45
59
  join(CLI_ROOT, 'studio/server/**/*.ts'),
60
+ join(CLI_ROOT, 'studio/server/**/*.tsx'),
46
61
  join(CLI_ROOT, 'studio/shared/**/*.ts'),
62
+ join(CLI_ROOT, 'studio/shared/**/*.tsx'),
47
63
  ])
64
+ const runtimeSourceFiles = project
65
+ .getSourceFiles()
66
+ .filter((sourceFile) => !/\.test\.(?:ts|tsx)$/.test(sourceFile.getBaseName()))
48
67
 
49
68
  // getImportStringLiterals covers static imports, re-exports, and dynamic import().
50
69
  const offenders: Record<string, string[]> = {}
51
- for (const sf of project.getSourceFiles()) {
70
+ for (const sf of runtimeSourceFiles) {
52
71
  const specs = sf.getImportStringLiterals().map((l) => l.getLiteralValue())
53
72
  for (const spec of specs) {
54
73
  if (spec.startsWith('.') || spec.startsWith('/') || isBuiltin(spec)) continue
@@ -66,9 +85,27 @@ describe('studio server runtime deps', () => {
66
85
  ).toEqual({})
67
86
  })
68
87
 
69
- test('parsed at least the known studio server files', () => {
70
- // Guard against the glob silently matching nothing (which would make the
71
- // invariant vacuously pass).
72
- expect(project.getSourceFiles().length).toBeGreaterThan(10)
88
+ test('parses every shipped Studio server and shared runtime source', () => {
89
+ const expected = [
90
+ ...runtimeFiles(join(CLI_ROOT, 'studio/server')),
91
+ ...runtimeFiles(join(CLI_ROOT, 'studio/shared')),
92
+ ]
93
+ .map(studioPath)
94
+ .sort()
95
+ const parsed = runtimeSourceFiles
96
+ .map((sourceFile) => studioPath(sourceFile.getFilePath()))
97
+ .sort()
98
+
99
+ expect(parsed).toEqual(expected)
100
+ })
101
+ })
102
+
103
+ describe('studio test gate', () => {
104
+ test('the root and Studio suites include all client, server, and shared tests', () => {
105
+ expect(cliPackage.scripts?.test).toBe(
106
+ 'node scripts/qualification/source-boundary.mjs --target && bun test src studio/client/src studio/server studio/shared && node --test scripts/*.test.mjs',
107
+ )
108
+ expect(cliPackage.scripts?.['test:studio']).toBe('pnpm --dir studio test')
109
+ expect(studioPackage.scripts?.test).toBe('bun test client/src server shared')
73
110
  })
74
111
  })