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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/README.md +4 -1
  2. package/dist/astrale.js +62968 -41240
  3. package/dist/public/connect-core.js +55030 -0
  4. package/dist/public/keys/index.js +45631 -0
  5. package/dist/public/paths/index.js +43990 -0
  6. package/dist/types/admin/instance/model.d.ts +40 -0
  7. package/dist/types/connect-core.d.ts +22 -0
  8. package/dist/types/connection/auth.d.ts +33 -0
  9. package/dist/types/errors.d.ts +23 -0
  10. package/dist/types/identity/registry.d.ts +25 -0
  11. package/dist/types/keys/algorithm.d.ts +6 -0
  12. package/dist/types/keys/credential.d.ts +21 -0
  13. package/dist/types/keys/index.d.ts +4 -0
  14. package/dist/types/keys/pair.d.ts +34 -0
  15. package/dist/types/lib/admin-target.d.ts +74 -0
  16. package/dist/types/lib/ca-fetch.d.ts +2 -0
  17. package/dist/types/lib/config.d.ts +71 -0
  18. package/dist/types/lib/format.d.ts +1 -0
  19. package/dist/types/lib/idp-session.d.ts +33 -0
  20. package/dist/types/lib/idp.d.ts +397 -0
  21. package/dist/types/lib/instance-target.d.ts +36 -0
  22. package/dist/types/lib/instance.d.ts +186 -0
  23. package/dist/types/lib/log.d.ts +30 -0
  24. package/dist/types/lib/login-flow.d.ts +49 -0
  25. package/dist/types/lib/meta.d.ts +36 -0
  26. package/dist/types/lib/output.d.ts +83 -0
  27. package/dist/types/lib/table.d.ts +18 -0
  28. package/dist/types/lib/validation.d.ts +11 -0
  29. package/dist/types/paths/index.d.ts +2 -0
  30. package/dist/types/state/exchange-credentials.d.ts +25 -0
  31. package/dist/types/state/files.d.ts +9 -0
  32. package/dist/types/state/identities.d.ts +42 -0
  33. package/dist/types/state/index.d.ts +8 -0
  34. package/dist/types/state/paths.d.ts +32 -0
  35. package/package.json +30 -8
  36. package/src/admin/.spec/api.d.ts +3 -0
  37. package/src/admin/.spec/architecture.md +15 -0
  38. package/src/admin/.spec/icon.svg +3 -0
  39. package/src/admin/.spec/layout.ts +6 -0
  40. package/src/admin/catalog/.spec/api.d.ts +65 -0
  41. package/src/admin/catalog/.spec/architecture.md +6 -0
  42. package/src/admin/catalog/.spec/icon.svg +3 -0
  43. package/src/admin/catalog/.spec/layout.ts +6 -0
  44. package/src/admin/catalog/__tests__/client.test.ts +171 -0
  45. package/src/admin/catalog/client.ts +214 -0
  46. package/src/admin/catalog/index.ts +13 -0
  47. package/src/admin/catalog/model.ts +42 -0
  48. package/src/admin/catalog/tsconfig.json +8 -0
  49. package/src/admin/graph/.spec/api.d.ts +28 -0
  50. package/src/admin/graph/.spec/architecture.md +5 -0
  51. package/src/admin/graph/.spec/icon.svg +3 -0
  52. package/src/admin/graph/.spec/layout.ts +3 -0
  53. package/src/admin/graph/index.ts +6 -0
  54. package/src/admin/graph/nodes.ts +56 -0
  55. package/src/admin/index.ts +2 -0
  56. package/src/admin/instance/.spec/api.d.ts +66 -0
  57. package/src/admin/instance/.spec/architecture.md +10 -0
  58. package/src/admin/instance/.spec/icon.svg +3 -0
  59. package/src/admin/instance/.spec/layout.ts +6 -0
  60. package/src/admin/instance/__tests__/client.test.ts +289 -0
  61. package/src/admin/instance/client.ts +298 -0
  62. package/src/admin/instance/index.ts +16 -0
  63. package/src/admin/instance/model.ts +65 -0
  64. package/src/admin/instance/tsconfig.json +8 -0
  65. package/src/commands/__tests__/admin-instance.test.ts +3 -36
  66. package/src/commands/__tests__/call.test.ts +21 -68
  67. package/src/commands/__tests__/domain-install-owned.test.ts +26 -27
  68. package/src/commands/__tests__/domain-list.test.ts +2 -6
  69. package/src/commands/__tests__/logs.test.ts +50 -98
  70. package/src/commands/__tests__/ls.test.ts +21 -17
  71. package/src/commands/__tests__/read-commands.test.ts +171 -101
  72. package/src/commands/__tests__/view.test.ts +100 -51
  73. package/src/commands/admin/status.ts +3 -3
  74. package/src/commands/admin/use.ts +29 -6
  75. package/src/commands/auth/login.ts +1 -1
  76. package/src/commands/auth/logout.ts +4 -2
  77. package/src/commands/auth/status.ts +2 -2
  78. package/src/commands/auth/token.ts +2 -2
  79. package/src/commands/browser.ts +1 -1
  80. package/src/commands/call.ts +55 -91
  81. package/src/commands/describe.ts +52 -147
  82. package/src/commands/domain/install.ts +81 -50
  83. package/src/commands/domain/list.ts +4 -8
  84. package/src/commands/domain/publish.ts +9 -31
  85. package/src/commands/get.ts +32 -65
  86. package/src/commands/identity/__tests__/register.test.ts +93 -0
  87. package/src/commands/identity/create.ts +2 -2
  88. package/src/commands/identity/delete.ts +2 -2
  89. package/src/commands/identity/export.ts +7 -33
  90. package/src/commands/identity/import.ts +21 -70
  91. package/src/commands/identity/list.ts +2 -2
  92. package/src/commands/identity/register.ts +148 -117
  93. package/src/commands/identity/sync.ts +2 -2
  94. package/src/commands/identity/unsync.ts +2 -2
  95. package/src/commands/identity/use.ts +2 -2
  96. package/src/commands/identity/whoami.ts +2 -2
  97. package/src/commands/idp/add.ts +1 -1
  98. package/src/commands/idp/list.ts +1 -1
  99. package/src/commands/idp/refresh.ts +1 -1
  100. package/src/commands/idp/remove.ts +2 -2
  101. package/src/commands/idp/show.ts +1 -1
  102. package/src/commands/instance/active.ts +2 -2
  103. package/src/commands/instance/bookmark.ts +8 -2
  104. package/src/commands/instance/create.ts +1 -1
  105. package/src/commands/instance/delete.ts +4 -10
  106. package/src/commands/instance/forget.ts +1 -1
  107. package/src/commands/instance/list.ts +3 -3
  108. package/src/commands/instance/status.ts +5 -7
  109. package/src/commands/instance/use.ts +3 -3
  110. package/src/commands/logs.ts +152 -279
  111. package/src/commands/ls.ts +78 -204
  112. package/src/commands/mutate.ts +70 -139
  113. package/src/commands/query.ts +100 -278
  114. package/src/commands/session/analyze.ts +1 -1
  115. package/src/commands/session/list.ts +1 -1
  116. package/src/commands/setup.ts +1 -1
  117. package/src/commands/status.ts +1 -1
  118. package/src/commands/studio.ts +1 -1
  119. package/src/commands/token.ts +26 -21
  120. package/src/commands/update.ts +1 -1
  121. package/src/commands/use.ts +2 -3
  122. package/src/commands/view-serve.ts +1 -1
  123. package/src/commands/view.ts +67 -109
  124. package/src/connect-core.test.ts +38 -0
  125. package/src/connect-core.ts +42 -0
  126. package/src/connection/.spec/api.d.ts +111 -0
  127. package/src/connection/.spec/architecture.md +41 -0
  128. package/src/connection/.spec/capabilities/connection.ts +13 -0
  129. package/src/connection/.spec/examples/call.ts +7 -0
  130. package/src/connection/.spec/flows/command.ts +20 -0
  131. package/src/connection/.spec/flows/session.ts +61 -0
  132. package/src/connection/.spec/laws/connection.ts +181 -0
  133. package/src/connection/.spec/layout.ts +19 -0
  134. package/src/{kernel → connection}/__tests__/auth.test.ts +26 -2
  135. package/src/connection/__tests__/ca-fetch.test.ts +53 -0
  136. package/src/connection/__tests__/call.test.ts +17 -0
  137. package/src/connection/__tests__/credential.test.ts +109 -0
  138. package/src/connection/__tests__/errors.test.ts +96 -0
  139. package/src/connection/__tests__/exchange.test.ts +224 -0
  140. package/src/connection/__tests__/fixtures/localhost-cert.pem +19 -0
  141. package/src/connection/__tests__/fixtures/localhost-key.base64 +1 -0
  142. package/src/connection/__tests__/managed.test.ts +39 -0
  143. package/src/connection/__tests__/self.test.ts +51 -0
  144. package/src/connection/__tests__/session.test.ts +140 -0
  145. package/src/connection/__tests__/target.test.ts +93 -0
  146. package/src/{kernel → connection}/auth.ts +8 -8
  147. package/src/connection/call.ts +7 -0
  148. package/src/{kernel/run.ts → connection/command.ts} +22 -13
  149. package/src/connection/credential.ts +114 -0
  150. package/src/{kernel → connection}/errors.ts +32 -9
  151. package/src/connection/exchange.ts +274 -0
  152. package/src/connection/index.ts +8 -0
  153. package/src/connection/self.ts +120 -0
  154. package/src/connection/session.ts +176 -0
  155. package/src/connection/target.ts +111 -0
  156. package/src/connection/tsconfig.json +6 -0
  157. package/src/graph/.spec/api.d.ts +34 -0
  158. package/src/graph/.spec/laws/documents.ts +24 -0
  159. package/src/graph/.spec/laws/projection.ts +13 -0
  160. package/src/graph/.spec/layout.ts +6 -0
  161. package/src/graph/__tests__/mutation.test.ts +43 -0
  162. package/src/graph/__tests__/projection.test.ts +22 -0
  163. package/src/graph/__tests__/query.test.ts +154 -0
  164. package/src/graph/index.ts +3 -0
  165. package/src/graph/mutation.ts +18 -0
  166. package/src/graph/projection.ts +21 -0
  167. package/src/graph/query.ts +108 -0
  168. package/src/graph/tsconfig.json +6 -0
  169. package/src/identity/.history/ard/bootstrap-root-transfer.md +57 -0
  170. package/src/identity/.history/ard/domain-mediated-registration.md +36 -0
  171. package/src/identity/.spec/api.d.ts +124 -0
  172. package/src/identity/.spec/architecture.md +33 -0
  173. package/src/identity/.spec/capabilities/identity.ts +7 -0
  174. package/src/identity/.spec/flows/bootstrap-root.ts +79 -0
  175. package/src/identity/.spec/flows/domain-mediated-registration.ts +51 -0
  176. package/src/identity/.spec/flows/import.ts +29 -0
  177. package/src/identity/.spec/laws/identity.ts +56 -0
  178. package/src/identity/.spec/layout.ts +13 -0
  179. package/src/identity/.spec/schemas/identity-export-v1.schema.json +25 -0
  180. package/src/identity/__tests__/fixtures/registry-journey.ts +53 -0
  181. package/src/identity/__tests__/registration.test.ts +64 -0
  182. package/src/identity/__tests__/registry.test.ts +70 -0
  183. package/src/identity/__tests__/transfer.test.ts +198 -0
  184. package/src/identity/index.ts +23 -0
  185. package/src/identity/registration.ts +72 -0
  186. package/src/identity/registry.ts +172 -0
  187. package/src/identity/transfer.ts +187 -0
  188. package/src/identity/tsconfig.json +6 -0
  189. package/src/index.ts +1 -0
  190. package/src/keys/.spec/api.d.ts +74 -0
  191. package/src/keys/.spec/architecture.md +13 -0
  192. package/src/keys/.spec/laws/keys.ts +43 -0
  193. package/src/keys/.spec/layout.ts +6 -0
  194. package/src/keys/__tests__/algorithm.test.ts +39 -0
  195. package/src/keys/__tests__/keys.test.ts +168 -0
  196. package/src/keys/algorithm.ts +33 -0
  197. package/src/keys/credential.ts +112 -0
  198. package/src/keys/index.ts +14 -0
  199. package/src/keys/pair.ts +219 -0
  200. package/src/keys/tsconfig.json +6 -0
  201. package/src/lib/__tests__/admin-target.test.ts +67 -7
  202. package/src/lib/__tests__/binary.test.ts +2 -3
  203. package/src/lib/__tests__/command-dx.test.ts +2 -2
  204. package/src/lib/__tests__/config.test.ts +3 -2
  205. package/src/lib/__tests__/idp.test.ts +4 -6
  206. package/src/lib/__tests__/instance-target.test.ts +7 -4
  207. package/src/lib/__tests__/local-status.test.ts +1 -1
  208. package/src/lib/__tests__/log.test.ts +31 -0
  209. package/src/lib/__tests__/meta.test.ts +55 -0
  210. package/src/lib/__tests__/sdk-deps.test.ts +1 -1
  211. package/src/lib/__tests__/self.test.ts +8 -230
  212. package/src/lib/__tests__/use-target.test.ts +1 -1
  213. package/src/lib/__tests__/view-open-intent.test.ts +78 -104
  214. package/src/lib/__tests__/view-session.test.ts +99 -0
  215. package/src/lib/admin-domain.ts +39 -25
  216. package/src/lib/admin-instance.ts +38 -43
  217. package/src/lib/admin-target.ts +67 -13
  218. package/src/lib/binary.ts +13 -34
  219. package/src/lib/browser.ts +1 -1
  220. package/src/{kernel → lib}/ca-fetch.ts +35 -27
  221. package/src/lib/config.ts +1 -1
  222. package/src/lib/idp-session.ts +1 -1
  223. package/src/lib/idp.ts +3 -8
  224. package/src/lib/instance-target.ts +12 -8
  225. package/src/lib/instance.ts +34 -2
  226. package/src/lib/local-status.ts +2 -2
  227. package/src/lib/log.ts +3 -1
  228. package/src/lib/login-flow.ts +32 -5
  229. package/src/lib/meta.ts +15 -11
  230. package/src/lib/provision-instance.ts +4 -12
  231. package/src/lib/self.ts +5 -147
  232. package/src/lib/update.ts +1 -1
  233. package/src/lib/use-target.ts +1 -1
  234. package/src/lib/validation.ts +2 -2
  235. package/src/lib/view/open-intent.ts +7 -37
  236. package/src/lib/view/port-allocation.ts +1 -1
  237. package/src/lib/view/resolve.ts +52 -45
  238. package/src/lib/view/server.ts +16 -33
  239. package/src/lib/view/session.ts +51 -22
  240. package/src/paths/.spec/api.d.ts +13 -0
  241. package/src/paths/.spec/architecture.md +5 -0
  242. package/src/paths/.spec/layout.ts +3 -0
  243. package/src/paths/index.ts +13 -0
  244. package/src/program/.spec/api.d.ts +42 -0
  245. package/src/program/.spec/architecture.md +18 -0
  246. package/src/program/.spec/laws/program.ts +29 -0
  247. package/src/program/.spec/layout.ts +14 -0
  248. package/src/program/__tests__/program.test.ts +359 -0
  249. package/src/program/build.ts +190 -0
  250. package/src/program/command.ts +42 -0
  251. package/src/program/index.ts +2 -0
  252. package/src/program/options.ts +40 -0
  253. package/src/{registry.ts → program/registry.ts} +5 -1
  254. package/src/program/tsconfig.json +6 -0
  255. package/src/setup/steps/admin.ts +18 -3
  256. package/src/setup/steps/instance.ts +1 -1
  257. package/src/state/.spec/api.d.ts +133 -0
  258. package/src/state/.spec/architecture.md +31 -0
  259. package/src/state/.spec/capabilities/state.ts +7 -0
  260. package/src/state/.spec/flows/identity-update.ts +22 -0
  261. package/src/state/.spec/laws/state.ts +73 -0
  262. package/src/state/.spec/layout.ts +14 -0
  263. package/src/state/__tests__/exchange-credentials.test.ts +188 -0
  264. package/src/state/__tests__/files.test.ts +113 -0
  265. package/src/state/__tests__/fixtures/identity-transition.ts +37 -0
  266. package/src/state/__tests__/identities.test.ts +142 -0
  267. package/src/state/__tests__/paths.test.ts +27 -0
  268. package/src/state/exchange-credentials.ts +219 -0
  269. package/src/state/files.ts +129 -0
  270. package/src/state/identities.ts +236 -0
  271. package/src/state/index.ts +29 -0
  272. package/src/state/paths.ts +61 -0
  273. package/src/state/tsconfig.json +6 -0
  274. package/src/telemetry/__tests__/redact.test.ts +3 -3
  275. package/src/telemetry/__tests__/trigger.test.ts +1 -1
  276. package/src/telemetry/settings.ts +1 -1
  277. package/src/telemetry/store.ts +1 -1
  278. package/src/test-utils.ts +1 -1
  279. package/studio/client/dist/assets/elk-api-D0cBetPW.js +1 -0
  280. package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  281. package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
  282. package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
  283. package/studio/client/dist/assets/index-Dspir4w7.js +81 -0
  284. package/studio/client/dist/assets/index-bVD2KJgz.js +8 -0
  285. package/studio/client/dist/index.html +2 -2
  286. package/studio/package.json +10 -1
  287. package/studio/server/introspect/extractor.ts +1 -1
  288. package/studio/server/state/views.test.ts +3 -7
  289. package/studio/server/state/views.ts +23 -30
  290. package/studio/tsconfig.json +23 -0
  291. package/tsconfig.json +1 -1
  292. package/viewer/dist/main.js +65 -39
  293. package/viewer/tsconfig.json +13 -0
  294. package/src/command.ts +0 -42
  295. package/src/commands/__tests__/help-contract.test.ts +0 -164
  296. package/src/kernel/__tests__/client-owned-lookup.test.ts +0 -51
  297. package/src/kernel/__tests__/errors.test.ts +0 -43
  298. package/src/kernel/__tests__/expand.test.ts +0 -123
  299. package/src/kernel/client.ts +0 -214
  300. package/src/kernel/expand.ts +0 -192
  301. package/src/kernel/graph.ts +0 -96
  302. package/src/kernel/index.ts +0 -29
  303. package/src/kernel/options.ts +0 -22
  304. package/src/kernel/types.ts +0 -14
  305. package/src/lib/__tests__/domain-identity.test.ts +0 -60
  306. package/src/lib/__tests__/fs-atomic.test.ts +0 -104
  307. package/src/lib/__tests__/identity.test.ts +0 -79
  308. package/src/lib/__tests__/keys.test.ts +0 -129
  309. package/src/lib/domain-identity.ts +0 -49
  310. package/src/lib/env.ts +0 -49
  311. package/src/lib/fs-atomic.ts +0 -126
  312. package/src/lib/identity.ts +0 -256
  313. package/src/lib/keys.ts +0 -294
  314. package/src/lib/paths.ts +0 -11
  315. package/src/program.ts +0 -213
  316. package/studio/client/dist/assets/index-BaqEuIQJ.js +0 -109
  317. package/studio/client/dist/assets/index-jRdExahh.css +0 -1
@@ -0,0 +1,224 @@
1
+ import type { Fetch } from '@astrale-os/kernel-client'
2
+
3
+ import { issuer } from '@astrale-os/sdk/auth'
4
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
5
+ import { mkdtemp, rm } from 'node:fs/promises'
6
+ import { tmpdir } from 'node:os'
7
+ import { join } from 'node:path'
8
+
9
+ import { ExchangeCredentialCache } from '../../state/exchange-credentials'
10
+ import { createExchangeCredentialResolver } from '../exchange'
11
+
12
+ const KERNEL = issuer.accept('https://kernel.example')
13
+ const DOMAIN = issuer.accept('https://admin.example')
14
+ const TARGET = { url: `${KERNEL}/api`, kernelIssuer: KERNEL, domainIssuer: DOMAIN }
15
+ const INVOCATION = `${KERNEL}/invoke`
16
+ const EXPIRES_AT = Math.floor(Date.now() / 1_000) + 500
17
+ const SOURCE_EXPIRES_AT = Math.floor(Date.now() / 1_000) + 120
18
+ const SOURCE_TOKEN = token(
19
+ issuer.accept('https://workos.example'),
20
+ KERNEL,
21
+ 'user-1',
22
+ SOURCE_EXPIRES_AT,
23
+ )
24
+ let directory: string
25
+
26
+ beforeEach(async () => {
27
+ directory = await mkdtemp(join(tmpdir(), 'astrale-exchange-'))
28
+ })
29
+
30
+ afterEach(async () => {
31
+ await rm(directory, { recursive: true, force: true })
32
+ })
33
+
34
+ describe('Domain token exchange', () => {
35
+ /** @evidence TEST-CLI-EXCHANGE-WHOAMI-DELEGATE-EXCHANGE-CACHE */
36
+ test('runs the exact User to Kernel to Domain journey and reuses the bound token', async () => {
37
+ const observed: Array<{
38
+ url: string
39
+ init: RequestInit | undefined
40
+ body?: Record<string, any>
41
+ }> = []
42
+ const exchanged = token(DOMAIN, KERNEL, 'user-1', EXPIRES_AT)
43
+ const fetch: Fetch = async (input, init) => {
44
+ const url = String(input)
45
+ if (url === INVOCATION) {
46
+ const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
47
+ observed.push({ url, init, body })
48
+ const index = observed.filter((entry) => entry.url === INVOCATION).length
49
+ return invocationResponse(
50
+ body.id,
51
+ index === 1
52
+ ? { id: 'user-1' }
53
+ : index === 2
54
+ ? 'kernel-destination-envelope'
55
+ : { id: 'user-1' },
56
+ new Headers(init?.headers).get('accept')!,
57
+ )
58
+ }
59
+ observed.push({ url, init })
60
+ if (url.endsWith('/.well-known/openid-configuration')) {
61
+ return jsonResponse(configuration(true))
62
+ }
63
+ if (url.endsWith('/.well-known/astrale/token')) {
64
+ expect(init?.body).toBeUndefined()
65
+ expect(new Headers(init?.headers).get('authorization')).toBe(
66
+ 'Bearer kernel-destination-envelope',
67
+ )
68
+ return jsonResponse(
69
+ { token: exchanged, expiresAt: EXPIRES_AT },
70
+ 200,
71
+ 'application/vnd.astrale+json',
72
+ )
73
+ }
74
+ throw new Error(`unexpected URL ${url}`)
75
+ }
76
+ const sourceAudiences: string[] = []
77
+ const resolver = createExchangeCredentialResolver(
78
+ TARGET,
79
+ {
80
+ async resolve(audience) {
81
+ sourceAudiences.push(audience)
82
+ return SOURCE_TOKEN
83
+ },
84
+ },
85
+ fetch,
86
+ 5_000,
87
+ new ExchangeCredentialCache(join(directory, 'credentials.json')),
88
+ )
89
+
90
+ expect(await resolver.resolve(KERNEL, new AbortController().signal)).toBe(exchanged)
91
+ await expect(resolver.resolve(KERNEL, new AbortController().signal)).resolves.toBe(exchanged)
92
+
93
+ const kernelRequests = observed.filter((entry) => entry.url === INVOCATION)
94
+ expect(kernelRequests).toHaveLength(3)
95
+ expect(kernelRequests[0]!.body).toMatchObject({
96
+ credential: SOURCE_TOKEN,
97
+ call: { input: {} },
98
+ })
99
+ expect(kernelRequests[1]!.body).toMatchObject({
100
+ credential: SOURCE_TOKEN,
101
+ call: {
102
+ input: {
103
+ audience: DOMAIN,
104
+ attenuation: { kind: 'identity', self: true },
105
+ },
106
+ },
107
+ })
108
+ const delegatedTtl = kernelRequests[1]!.body!.call.input.ttlSeconds
109
+ expect(delegatedTtl).toBeGreaterThan(0)
110
+ expect(delegatedTtl).toBeLessThan(120)
111
+ expect(sourceAudiences).toEqual([KERNEL, KERNEL])
112
+ expect(
113
+ observed.filter((entry) => entry.url.endsWith('/.well-known/astrale/token')),
114
+ ).toHaveLength(1)
115
+ })
116
+
117
+ /** @evidence TEST-CLI-EXCHANGE-NO-LEGACY-FALLBACK */
118
+ test('fails closed when issuer discovery does not advertise exchange', async () => {
119
+ const fetch: Fetch = async (input, init) => {
120
+ const url = String(input)
121
+ if (url === INVOCATION) {
122
+ const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
123
+ return invocationResponse(
124
+ body.id,
125
+ body.call.input && Object.keys(body.call.input).length === 0
126
+ ? { id: 'user-1' }
127
+ : 'kernel-destination-envelope',
128
+ new Headers(init?.headers).get('accept')!,
129
+ )
130
+ }
131
+ if (url.endsWith('/.well-known/openid-configuration'))
132
+ return jsonResponse(configuration(false))
133
+ throw new Error('exchange endpoint must not be called')
134
+ }
135
+ const resolver = createExchangeCredentialResolver(
136
+ TARGET,
137
+ { resolve: async () => SOURCE_TOKEN },
138
+ fetch,
139
+ 5_000,
140
+ new ExchangeCredentialCache(join(directory, 'credentials.json')),
141
+ )
142
+ await expect(resolver.resolve(KERNEL, new AbortController().signal)).rejects.toMatchObject({
143
+ code: 'TOKEN_EXCHANGE_UNSUPPORTED',
144
+ })
145
+ })
146
+
147
+ test('rejects an exchanged token bound to another Kernel', async () => {
148
+ const wrong = token(DOMAIN, issuer.accept('https://other-kernel.example'), 'user-1', EXPIRES_AT)
149
+ const fetch = exchangeFetch(wrong)
150
+ const resolver = createExchangeCredentialResolver(
151
+ TARGET,
152
+ { resolve: async () => SOURCE_TOKEN },
153
+ fetch,
154
+ 5_000,
155
+ new ExchangeCredentialCache(join(directory, 'credentials.json')),
156
+ )
157
+ await expect(resolver.resolve(KERNEL, new AbortController().signal)).rejects.toMatchObject({
158
+ code: 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
159
+ })
160
+ })
161
+ })
162
+
163
+ function exchangeFetch(exchanged: string): Fetch {
164
+ return async (input, init) => {
165
+ const url = String(input)
166
+ if (url === INVOCATION) {
167
+ const body = JSON.parse(await new Response(init?.body).text()) as Record<string, any>
168
+ return invocationResponse(
169
+ body.id,
170
+ body.call.input && Object.keys(body.call.input).length === 0
171
+ ? { id: 'user-1' }
172
+ : 'kernel-destination-envelope',
173
+ new Headers(init?.headers).get('accept')!,
174
+ )
175
+ }
176
+ if (url.endsWith('/.well-known/openid-configuration')) return jsonResponse(configuration(true))
177
+ return jsonResponse(
178
+ { token: exchanged, expiresAt: EXPIRES_AT },
179
+ 200,
180
+ 'application/vnd.astrale+json',
181
+ )
182
+ }
183
+ }
184
+
185
+ function configuration(enabled: boolean) {
186
+ return {
187
+ issuer: DOMAIN,
188
+ jwks_uri: `${DOMAIN}/.well-known/jwks.json`,
189
+ subject_types_supported: ['public'],
190
+ id_token_signing_alg_values_supported: ['EdDSA'],
191
+ ...(enabled ? { token_exchange_endpoint: `${DOMAIN}/.well-known/astrale/token` } : {}),
192
+ }
193
+ }
194
+
195
+ function invocationResponse(id: number, result: unknown, contentType: string): Response {
196
+ return new Response(JSON.stringify({ id, result }), {
197
+ headers: { 'content-type': contentType, 'cache-control': 'no-store' },
198
+ })
199
+ }
200
+
201
+ function jsonResponse(value: unknown, status = 200, contentType = 'application/json'): Response {
202
+ return new Response(JSON.stringify(value), {
203
+ status,
204
+ headers: { 'content-type': contentType, 'cache-control': 'no-store' },
205
+ })
206
+ }
207
+
208
+ function token(iss: string, aud: string, user: string, exp: number): string {
209
+ const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString('base64url')
210
+ const proof = `${encode({ alg: 'EdDSA', typ: 'JWT' })}.${encode({
211
+ iss: aud,
212
+ sub: user,
213
+ aud,
214
+ exp,
215
+ delegation: { v: 1, expr: { kind: 'identity', id: user } },
216
+ })}.signature`
217
+ return `${encode({ alg: 'EdDSA', typ: 'JWT' })}.${encode({
218
+ iss,
219
+ sub: 'admin-domain',
220
+ aud,
221
+ exp,
222
+ grant: { v: 1, expr: { kind: 'identity', credential: proof } },
223
+ })}.signature`
224
+ }
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDJTCCAg2gAwIBAgIULD/vMZdJ/u94wRaoyRt5crvKLL0wDQYJKoZIhvcNAQEL
3
+ BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDgxMTE0MDUxNFoXDTM2MDgw
4
+ ODE0MDUxNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5
+ AAOCAQ8AMIIBCgKCAQEApheXGRylshfFubAJ1sBoecE9q+giTVJZlAKqXE+Cp/wn
6
+ CQv/uOMQtus6ikSjDmyk8/qiSRAGZYzmZjjt5ICttav7Kb0krdRAnOSB9zd4+hEP
7
+ jq7O5IcAlriey8KKZInugB7GD7HDVGKYPrJpJrOmfxXW0s1Vj55nehP3vn59obfH
8
+ EE4mJ50SV9k/VS5AZ3nJHQwKyGjIKvkHI9Ll0++Wu5/ny84TwYkgY8RYwMwfj2Z0
9
+ ltqGo7bsanwk3JFfrphBSYteAxcgF0Y//Vv557RnM2+zX7W6K1FD7s7Nen4/vx01
10
+ cEacj59lNTDANUNJNNj+cCpol4Ie7Krk0deq/Ih3KQIDAQABo28wbTAdBgNVHQ4E
11
+ FgQU7+UbkfznwiPgzxoBtkPHay+Ifu8wHwYDVR0jBBgwFoAU7+UbkfznwiPgzxoB
12
+ tkPHay+Ifu8wDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH
13
+ BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAGsPr3yfMuVyCuPQMCaUAWY8Qulepl1V
14
+ NIvqyEAb94F1L8Mfh3UNPF9UljBGcI1RBL8e21iNF7vnR0WBF2SSTlf18/qcErVA
15
+ 4A7AwlqIVUXjERN9wKesCS/w92CBswLxq/6KrnLahngoBW/tDJ/uzZLXvSdCnYG7
16
+ NMZMpIB0rLdT6GYr1KT/hUSKOKLqC5x0WtEAdoIOqCyYaZrxso4gIicPMWTS3Eho
17
+ 9wzXiIHOme9rtlEPnDPVseuIw2ADRPQHAHXiIlyUcJwmt01mgAKPoMp3wCgTaj2H
18
+ NO7KMXAdJgGmmrCkP3dOWri81NlQVoOrbE432bAfKE7S+KehDYJpeAU=
19
+ -----END CERTIFICATE-----
@@ -0,0 +1 @@
1
+ LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQ21GNWNaSEtXeUY4VzUKc0FuV3dHaDV3VDJyNkNKTlVsbVVBcXBjVDRLbi9DY0pDLys0NHhDMjZ6cUtSS01PYktUeitxSkpFQVpsak9abQpPTzNrZ0syMXEvc3B2U1N0MUVDYzVJSDNOM2o2RVErT3JzN2tod0NXdUo3THdvcGtpZTZBSHNZUHNjTlVZcGcrCnNta21zNlovRmRiU3pWV1BubWQ2RS9lK2ZuMmh0OGNRVGlZbm5SSlgyVDlWTGtCbmVja2REQXJJYU1ncStRY2oKMHVYVDc1YTduK2ZMemhQQmlTQmp4RmpBekIrUFpuU1cyb2FqdHV4cWZDVGNrVit1bUVGSmkxNERGeUFYUmovOQpXL25udEdjemI3TmZ0Ym9yVVVQdXpzMTZmaisvSFRWd1JweVBuMlUxTU1BMVEwazAyUDV3S21pWGdoN3NxdVRSCjE2cjhpSGNwQWdNQkFBRUNnZ0VBRHZPRnJ3V0VwdXo3NkJUQU5mdzVDMmpKVCszRUZMaktob2ZjNVJWbE9oRDUKUkplM2ErSEkwZWFIN1FWenRpc3lpQ0h5Tk9pRDR1VlZvak1TdXlTWVBvZmVRcW9ib0toQUxyOWxNYWRsTlFHUApLSFYwKzE1cHY2ZWI1OVM1MTRZUkdhaHJoT01vSmJNQWFIaHYvY05qUkFKbnhhOGxPaG4wejZqM2ZzYnRNVDd3Ck11dFpVMm85RWRjc09iUHBnemlnZVFEbmw0NVlNbFVsLzY5b2lSR2w0NWw1a2xyaUtMdk9YQWcydWJXTFR0QWYKWVJjeW9mOEdySmFXL1F5ZmRGVVloRUUydVNqUHNyMkFLMVNsMlBDZ1FZNmRNOUZZaDVkckZSNDU1R0VDUm94MApoaFVZS1NQRFhVU1VESUNMcVZ2UTdlOEhTMlVvUXZiaUdRRndMZHp3d1FLQmdRRGJYUVBMNlA5UDRZQ2tUZFFtCnQ4TTNvSlNYbXZHd2JRcTd2WVU5K3ZSYkMyYWRHekljaWYyT1E5OFFsc0R3Yy9XQ2psT01GalJjdTk3NVBueGsKR0RTZXZFdHpjR0JidkwzS094VzJybHo0NnVKOGw2MTkyMkE0a3BUbTNHREdBNFFabE9UZ2tWSkZyWHR5WDVXaApBQ1FkY0twWURFSXRYZm5kUXV5dGZ4anNzUUtCZ1FEQjFQQ2x3bkZqOHFMa2RMZ2xlODczLzJsdC9taGZFcnA5CmNJYkxtdFY3allsZDlzZExJb3RUL2ZyOU9odjUvKzdIUFBGNmQrb3pBcUpQSnBmVjhnNUpWMWhON1pRVWY4eW8KSldTbTZyRWphL0hWRCtsVENBLy9neU1DcFRGTXRhU2wraFBhcVpRb2ZWRm1BRm9OWjFrR0oxdm9iaEhhVmlwSwpQTHFrVGFIditRS0JnUUNydmtFd3JuVGxwNW0rQ2FrbHZ3MjZTYURNcVdTOFNrTHpCY2lCNUM3WDhTOGZwYUV3Ck11RXZOdmczMXErc0VqMlBQN3N0bkZ6V2xjaFd2NU5JRXFOdis5L2V1NGJWakFuVGdUclkwZDY4aTdsUzdyWEkKS2E5RjI0MTB5ZW1DWGxKWWJQaGMwYW5tSDVMYzB2VWxIaVVvWDRNZWVFcC9sbW4vejF2WjRmNHpvUUtCZ0V3MQpVN1JvWWJxaFFFaEFWOUpzL2kvbFhPZGhPbG4rL0JHcnQyTy82YVdkUFdBVFVjSUp2K3Z6Rk1hUkhVVEt5bS9OClYwZ0djNjlBbnQ0dDEwN0h6N1FvUWxESlV3cElRUWZ2UThhWXliS2FrMGRvN1J5M004c0xIUzNDRTBNTXU4S0MKTTJJcWdGVDNDSlRwR0g3aW5rN2NGTGVxNXh3SCtyS2JjK3A3bDJySkFvR0JBSmIxVDRpZFZPdmFZRFlINE1pUwpmUUpGU29QaFZqaklURmU2aG5hNkN0akNHdXoySGNqdXpRMWVFcVhRbzVIYkxRaGdYQnpsM3pUZllYTDJMWGhWCk9WRDB3UmJ2MFAyUk5nNXpqUERQMU8vL2ZISDFkc1hFUzNCZ2t6M3czUnduZXhucXZIRTU5Vm5VQ01yUnFOREYKeXAwVkQzM1F6aWhuRFdScWxhVUxSTit4Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import type { ConnectionContext } from '../session'
4
+ import type { AdminConnectionOptions } from '../target'
5
+
6
+ import { lookupManagedInstance } from '../session'
7
+
8
+ describe('implicit managed target owner discovery', () => {
9
+ test('keeps target credentials out of the admin inventory lookup', async () => {
10
+ const owned = {
11
+ id: 'owned-id',
12
+ slug: 'owned',
13
+ url: 'https://owned.eu.astrale.ai',
14
+ state: 'ready',
15
+ } as const
16
+ let capturedOptions: AdminConnectionOptions | undefined
17
+ const openAdmin = async <Value>(
18
+ options: AdminConnectionOptions,
19
+ action: (context: ConnectionContext) => Promise<Value>,
20
+ ): Promise<Value> => {
21
+ capturedOptions = options
22
+ return action({} as ConnectionContext)
23
+ }
24
+ const connect = async () => ({ list: async () => [owned] }) as never
25
+
26
+ await lookupManagedInstance(
27
+ 'owned',
28
+ {
29
+ as: 'target-identity',
30
+ creds: 'target-delegation',
31
+ timeout: '45000',
32
+ },
33
+ openAdmin,
34
+ connect,
35
+ )
36
+
37
+ expect(capturedOptions).toEqual({ timeout: '45000' })
38
+ })
39
+ })
@@ -0,0 +1,51 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { SignJWT } from 'jose'
3
+ import { webcrypto } from 'node:crypto'
4
+
5
+ import { resolveSelfIdAuthenticated } from '../self'
6
+
7
+ describe('resolveSelfIdAuthenticated', () => {
8
+ /** @evidence TEST-CLI-SELF-USES-AUTHENTICATED-EFFECTIVE-PRINCIPAL */
9
+ test('uses child whoami for a manager-to-child Management carrier', async () => {
10
+ const key = await generateEs256()
11
+ const credential = await new SignJWT({ carrier: 'management' })
12
+ .setProtectedHeader({ alg: 'ES256' })
13
+ .setIssuer('https://manager.example')
14
+ .setSubject('manager-principal')
15
+ .setAudience('https://child.example')
16
+ .sign(key)
17
+ const opts = { creds: credential, url: 'https://manager.example/children/child/invoke' }
18
+
19
+ const resolved = await resolveSelfIdAuthenticated(opts, {
20
+ whoami: async (received) => {
21
+ expect(received).toBe(opts)
22
+ return { id: 'child-kernel-principal', slug: 'child' }
23
+ },
24
+ })
25
+
26
+ expect(resolved).toEqual({ id: 'child-kernel-principal', slug: 'child' })
27
+ })
28
+
29
+ test('resolves an imported root without requiring a local registration', async () => {
30
+ await expect(
31
+ resolveSelfIdAuthenticated(
32
+ { as: 'platform-root', url: 'https://manager.example/invoke' },
33
+ { whoami: async () => ({ id: 'manager-kernel-principal' }) },
34
+ ),
35
+ ).resolves.toEqual({ id: 'manager-kernel-principal' })
36
+ })
37
+
38
+ test('fails closed when authenticated whoami returns no NodeId', async () => {
39
+ await expect(
40
+ resolveSelfIdAuthenticated({}, { whoami: async () => ({ id: ' ' }) }),
41
+ ).rejects.toMatchObject({ name: 'SelfResolutionError' })
42
+ })
43
+ })
44
+
45
+ async function generateEs256(): Promise<CryptoKey> {
46
+ const pair = await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
47
+ 'sign',
48
+ 'verify',
49
+ ])
50
+ return pair.privateKey
51
+ }
@@ -0,0 +1,140 @@
1
+ import type { AuthApi } from '@astrale-os/kernel-client/auth'
2
+ import type { GraphApi } from '@astrale-os/kernel-client/graph'
3
+ import type { ClientSession } from '@astrale-os/kernel-client/session'
4
+
5
+ import { issuer } from '@astrale-os/sdk/auth'
6
+ import { describe, expect, test } from 'bun:test'
7
+
8
+ import type { AstraleConfig } from '../../lib/config'
9
+ import type { ConnectionContext, ConnectionFactory } from '../session'
10
+
11
+ import { createClientSessionOptions, withResolvedClientSession } from '../session'
12
+
13
+ const target = Object.freeze({
14
+ url: 'https://gateway.example/instances/child/invoke',
15
+ kernelIssuer: issuer.accept('https://child.example'),
16
+ })
17
+
18
+ const config: AstraleConfig = {
19
+ issuer: 'https://cli.example',
20
+ admin: {
21
+ name: 'admin',
22
+ url: 'https://admin.example',
23
+ kernelIssuer: 'https://admin.example',
24
+ },
25
+ telemetry: { enabled: false },
26
+ }
27
+
28
+ const context: ConnectionContext = Object.freeze({
29
+ session: {} as ClientSession,
30
+ callerSession: {} as ClientSession,
31
+ graph: {} as GraphApi,
32
+ auth: {} as AuthApi,
33
+ target,
34
+ })
35
+
36
+ describe('connection session', () => {
37
+ /** @evidence TEST-CLI-CONNECTION-PINS-SOURCE-ISSUER */
38
+ test('pins the selected canonical Kernel issuer without a transport escape hatch', async () => {
39
+ const auth = { ttlSeconds: 3_600, resolve: async () => ({ credential: 'credential' }) }
40
+ const options = createClientSessionOptions(target, globalThis.fetch, auth, 2_500)
41
+
42
+ expect(options.kernel).toBe(target.kernelIssuer)
43
+ expect(options).not.toHaveProperty('url')
44
+ expect(options).not.toHaveProperty('sourceIssuer')
45
+ })
46
+
47
+ /** @evidence TEST-CLI-CONNECTION-OMITS-EXPLICIT-ANONYMOUS-CREDENTIAL */
48
+ test('constructs an anonymous Client Session without an auth resolver', () => {
49
+ const options = createClientSessionOptions(target, globalThis.fetch, undefined, 2_500)
50
+
51
+ expect(options.kernel).toBe(target.kernelIssuer)
52
+ expect(options.auth).toBeUndefined()
53
+ expect(Object.hasOwn(options, 'auth')).toBe(false)
54
+ })
55
+
56
+ /** @evidence TEST-CLI-CONNECTION-CLOSES-OWNED-CLIENTS */
57
+ test('closes its owned connection after success, failure, and cancellation', async () => {
58
+ for (const outcome of ['success', 'failure', 'cancellation'] as const) {
59
+ let closes = 0
60
+ const open: ConnectionFactory = () => ({
61
+ context,
62
+ close() {
63
+ closes += 1
64
+ },
65
+ })
66
+ const pending = withResolvedClientSession(
67
+ target,
68
+ {},
69
+ config,
70
+ async (received) => {
71
+ expect(received).toBe(context)
72
+ if (outcome === 'failure') throw new Error('action failed')
73
+ if (outcome === 'cancellation') throw new DOMException('cancelled', 'AbortError')
74
+ return 'done'
75
+ },
76
+ open,
77
+ )
78
+ if (outcome === 'success') await expect(pending).resolves.toBe('done')
79
+ else await expect(pending).rejects.toThrow()
80
+ expect(closes).toBe(1)
81
+ }
82
+ })
83
+
84
+ /** @evidence TEST-CLI-CONNECTION-REJECTS-INVALID-TIMEOUT-BEFORE-OPEN */
85
+ test('rejects invalid timeouts before constructing a connection', async () => {
86
+ for (const timeout of ['0', '-1', '1.5', 'nope', '999999999999999999999']) {
87
+ let opened = false
88
+ await expect(
89
+ withResolvedClientSession(
90
+ target,
91
+ { timeout },
92
+ config,
93
+ async () => undefined,
94
+ () => {
95
+ opened = true
96
+ throw new Error('must not open')
97
+ },
98
+ ),
99
+ ).rejects.toMatchObject({ code: 'INVALID_FLAG' })
100
+ expect(opened).toBe(false)
101
+ }
102
+
103
+ let receivedTimeout = 0
104
+ await withResolvedClientSession(
105
+ target,
106
+ { timeout: '2500' },
107
+ config,
108
+ async () => undefined,
109
+ (_target, timeoutMs) => {
110
+ receivedTimeout = timeoutMs
111
+ return { context, close() {} }
112
+ },
113
+ )
114
+ expect(receivedTimeout).toBe(2_500)
115
+ })
116
+
117
+ /** @evidence TEST-CLI-CONNECTION-REJECTS-ANONYMOUS-CREDENTIAL-CONFLICT */
118
+ test('rejects anonymous plus explicit credentials before opening a connection', async () => {
119
+ for (const options of [
120
+ { anonymous: true, as: 'alice' },
121
+ { anonymous: true, creds: 'token' },
122
+ { anonymous: true, as: 'alice', creds: 'token' },
123
+ ]) {
124
+ let opened = false
125
+ await expect(
126
+ withResolvedClientSession(
127
+ target,
128
+ options,
129
+ config,
130
+ async () => undefined,
131
+ () => {
132
+ opened = true
133
+ throw new Error('must not open')
134
+ },
135
+ ),
136
+ ).rejects.toMatchObject({ code: 'INVALID_FLAG' })
137
+ expect(opened).toBe(false)
138
+ }
139
+ })
140
+ })
@@ -0,0 +1,93 @@
1
+ import { issuer } from '@astrale-os/sdk/auth'
2
+ import { describe, expect, test } from 'bun:test'
3
+
4
+ import type { AstraleConfig } from '../../lib/config'
5
+ import type { InstanceStore } from '../../lib/instance'
6
+
7
+ import { resolveAdminConnectionTarget, resolveConnectionTarget } from '../target'
8
+
9
+ const config: AstraleConfig = {
10
+ issuer: 'https://cli.example',
11
+ admin: {
12
+ name: 'control',
13
+ url: 'https://admin.example/api',
14
+ kernelIssuer: 'https://admin.example/issuer',
15
+ domainIssuer: 'https://admin-domain.example',
16
+ },
17
+ telemetry: { enabled: false },
18
+ }
19
+
20
+ const instances: InstanceStore = {
21
+ active: 'staging',
22
+ instances: {
23
+ staging: {
24
+ url: 'https://staging.example/api',
25
+ issuer: 'https://identity.staging.example',
26
+ defaultIdentity: 'alice',
27
+ caFile: '/etc/astrale/staging-ca.pem',
28
+ },
29
+ },
30
+ }
31
+
32
+ describe('connection target', () => {
33
+ /** @evidence TEST-CLI-CONNECTION-SELECTS-EXACT-TARGET */
34
+ test('preserves URL, bookmark, active, managed, and Admin target semantics', async () => {
35
+ expect(
36
+ await resolveConnectionTarget({ url: 'https://direct.example/invoke' }, config, {
37
+ instances,
38
+ }),
39
+ ).toEqual({
40
+ url: 'https://direct.example/invoke',
41
+ kernelIssuer: issuer.accept('https://direct.example/invoke'),
42
+ })
43
+
44
+ expect(await resolveConnectionTarget({}, config, { instances })).toEqual({
45
+ url: 'https://staging.example/api',
46
+ kernelIssuer: issuer.accept('https://identity.staging.example'),
47
+ slug: 'staging',
48
+ defaultIdentity: 'alice',
49
+ caFile: '/etc/astrale/staging-ca.pem',
50
+ })
51
+
52
+ expect(
53
+ await resolveConnectionTarget(
54
+ { instance: 'staging', url: 'https://override.example/invoke' },
55
+ config,
56
+ { instances },
57
+ ),
58
+ ).toMatchObject({
59
+ url: 'https://override.example/invoke',
60
+ kernelIssuer: 'https://identity.staging.example',
61
+ slug: 'staging',
62
+ })
63
+
64
+ expect(
65
+ await resolveConnectionTarget({ instance: 'remote' }, config, {
66
+ instances,
67
+ managed: async (slug) => ({
68
+ id: 'managed-id',
69
+ slug,
70
+ url: 'https://managed.example',
71
+ }),
72
+ }),
73
+ ).toEqual({
74
+ url: 'https://managed.example',
75
+ kernelIssuer: issuer.accept('https://managed.example'),
76
+ slug: 'remote',
77
+ })
78
+
79
+ expect(await resolveConnectionTarget({ instance: 'control' }, config, { instances })).toEqual({
80
+ url: 'https://admin.example/api',
81
+ kernelIssuer: issuer.accept('https://admin.example/issuer'),
82
+ domainIssuer: issuer.accept('https://admin-domain.example'),
83
+ slug: 'control',
84
+ })
85
+
86
+ expect(await resolveAdminConnectionTarget({}, config, instances)).toEqual({
87
+ url: 'https://admin.example/api',
88
+ kernelIssuer: issuer.accept('https://admin.example/issuer'),
89
+ domainIssuer: issuer.accept('https://admin-domain.example'),
90
+ slug: 'control',
91
+ })
92
+ })
93
+ })
@@ -1,7 +1,8 @@
1
1
  import type { AstraleConfig } from '../lib/config'
2
2
 
3
3
  import { AuthError } from '../errors'
4
- import { getDefault, getIdentity, type Identity } from '../lib/identity'
4
+ import { getDefault, getIdentity, type Identity } from '../identity/registry'
5
+ import { signAs } from '../keys/index'
5
6
  import {
6
7
  accessTokenForAudience,
7
8
  classifyRefreshFailure,
@@ -12,8 +13,7 @@ import {
12
13
  IdpSessionMissingError,
13
14
  IdpSessionNoRefreshTokenError,
14
15
  } from '../lib/idp-session'
15
- import { signAs } from '../lib/keys'
16
- import { KEYS_DIR } from '../lib/paths'
16
+ import { KEYS_DIR } from '../state/index'
17
17
 
18
18
  export type KeyIdentityAuthOptions = {
19
19
  issuer: string
@@ -37,7 +37,7 @@ export async function resolveCredential(
37
37
  opts: { as?: string; creds?: string; defaultIdentity?: string },
38
38
  config: AstraleConfig,
39
39
  audience: string = config.issuer,
40
- instanceSlug?: string,
40
+ registrationKey?: string,
41
41
  ): Promise<string> {
42
42
  if (opts.creds) return opts.creds
43
43
  // Track the resolved identity so the catch block can tailor its hint: an
@@ -60,7 +60,7 @@ export async function resolveCredential(
60
60
  return await signAs(
61
61
  identity.subject,
62
62
  KEYS_DIR,
63
- resolveKeyIdentityAuthOptions(identity, config, audience, instanceSlug),
63
+ resolveKeyIdentityAuthOptions(identity, config, audience, registrationKey),
64
64
  )
65
65
  }
66
66
 
@@ -74,7 +74,7 @@ export async function resolveCredential(
74
74
  return await signAs(
75
75
  identity.subject,
76
76
  KEYS_DIR,
77
- resolveKeyIdentityAuthOptions(identity, config, audience, instanceSlug),
77
+ resolveKeyIdentityAuthOptions(identity, config, audience, registrationKey),
78
78
  )
79
79
  } catch (e) {
80
80
  const message = e instanceof Error ? e.message : 'Failed to resolve credentials'
@@ -128,9 +128,9 @@ export function resolveKeyIdentityAuthOptions(
128
128
  identity: Identity,
129
129
  config: AstraleConfig,
130
130
  audience: string = config.issuer,
131
- instanceSlug?: string,
131
+ registrationKey?: string,
132
132
  ): KeyIdentityAuthOptions {
133
- const registration = instanceSlug ? identity.registrations?.[instanceSlug] : undefined
133
+ const registration = registrationKey ? identity.registrations?.[registrationKey] : undefined
134
134
  return {
135
135
  issuer:
136
136
  registration?.iss ?? identity.issuer ?? systemIdentityIssuer(identity, audience, config),
@@ -0,0 +1,7 @@
1
+ import { call, type Call } from '@astrale-os/kernel-client'
2
+ import { Path } from '@astrale-os/sdk/graph/path'
3
+
4
+ /** Convert the CLI's untrusted text/JSON boundary into the one public Call representation. */
5
+ export function createPathCall(path: string, input: unknown): Call {
6
+ return call(Path.parse(path), input as Parameters<typeof call>[1])
7
+ }