@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,53 @@
1
+ import { fileExists, keypairPaths } from '../../../keys/index'
2
+ import { KEYS_DIR } from '../../../state/index'
3
+ import {
4
+ createIdentity,
5
+ exportIdentity,
6
+ importIdentity,
7
+ deleteIdentity,
8
+ getDefault,
9
+ readIdentities,
10
+ setDefault,
11
+ setIdentityMode,
12
+ setRegistration,
13
+ upsertIdpIdentity,
14
+ } from '../../index'
15
+
16
+ await createIdentity('alice')
17
+ await createIdentity('bob', { mode: 'remote' })
18
+ await importIdentity(await exportIdentity('alice'), { name: 'alice-alias' })
19
+ await setDefault('alice')
20
+ await setIdentityMode('bob', 'local')
21
+ await setRegistration('alice', 'production', {
22
+ iss: 'https://kernel.example',
23
+ sub: 'node-alice',
24
+ registeredAt: '2026-08-11T12:00:00.000Z',
25
+ })
26
+
27
+ let selectedDeletion = ''
28
+ try {
29
+ await deleteIdentity('alice')
30
+ } catch (error) {
31
+ selectedDeletion = error instanceof Error ? error.message : String(error)
32
+ }
33
+ await deleteIdentity('bob')
34
+ await deleteIdentity('alice-alias')
35
+ await upsertIdpIdentity('workos', {
36
+ subject: 'user_123',
37
+ idp: 'workos',
38
+ issuer: 'https://idp.example',
39
+ audience: 'https://kernel.example',
40
+ use: false,
41
+ })
42
+
43
+ const store = await readIdentities()
44
+ const selected = await getDefault()
45
+ console.log(
46
+ JSON.stringify({
47
+ store,
48
+ selected,
49
+ selectedDeletion,
50
+ aliceKey: await fileExists(keypairPaths('alice', KEYS_DIR).privatePath),
51
+ bobKey: await fileExists(keypairPaths('bob', KEYS_DIR).privatePath),
52
+ }),
53
+ )
@@ -0,0 +1,64 @@
1
+ import type { ProvisionRequest } from '@astrale-os/sdk/auth'
2
+
3
+ import { LocalBinding } from '@astrale-os/sdk/graph/model'
4
+ import { expect, test } from 'bun:test'
5
+
6
+ import { submitIdentityProvision } from '../registration'
7
+
8
+ /** @evidence TEST-CLI-IDENTITY-REGISTER-DOMAIN-MEDIATED */
9
+ test('submits the exact self-proven request through an explicit Domain callable', async () => {
10
+ const binding = LocalBinding('identity')
11
+ const request = {
12
+ idempotencyKey: 'identity-register:operator',
13
+ mutation: {
14
+ format: 'astrale.graph.mutation',
15
+ version: 'v2',
16
+ preconditions: [],
17
+ operations: [],
18
+ },
19
+ identities: {
20
+ identity: {
21
+ credentials: {
22
+ publicKey: { kty: 'EC', crv: 'P-256', x: 'x', y: 'y', alg: 'ES256' },
23
+ proof: 'self-proven-request.jwt.signature',
24
+ },
25
+ },
26
+ },
27
+ } as unknown as ProvisionRequest
28
+ const calls: unknown[] = []
29
+ let directCalls = 0
30
+ const result = await submitIdentityProvision({
31
+ binding,
32
+ request,
33
+ via: '/:ops.example:function.provisionOperator',
34
+ direct: {
35
+ async provision() {
36
+ directCalls += 1
37
+ throw new Error('direct provision must not run')
38
+ },
39
+ },
40
+ callable: {
41
+ async call(call) {
42
+ calls.push(call)
43
+ return {
44
+ createdNodes: { identity: 'operator-node' },
45
+ identities: {
46
+ identity: { issuer: 'https://identity.example', subject: 'operator-node' },
47
+ },
48
+ }
49
+ },
50
+ },
51
+ })
52
+
53
+ expect(directCalls).toBe(0)
54
+ expect(calls).toHaveLength(1)
55
+ expect(JSON.parse(JSON.stringify(calls[0]))).toEqual({
56
+ target: '/:ops.example:function.provisionOperator',
57
+ input: JSON.parse(JSON.stringify(request)),
58
+ })
59
+ expect(result).toEqual({
60
+ iss: 'https://identity.example',
61
+ sub: 'operator-node',
62
+ nodeId: 'operator-node',
63
+ })
64
+ })
@@ -0,0 +1,70 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { mkdtemp, readFile, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+
6
+ describe('identity registry journey', () => {
7
+ let root = ''
8
+
9
+ beforeEach(async () => {
10
+ root = await mkdtemp(join(tmpdir(), 'astrale-identity-registry-'))
11
+ })
12
+
13
+ afterEach(async () => {
14
+ await rm(root, { recursive: true, force: true })
15
+ })
16
+
17
+ /** @evidence TEST-CLI-IDENTITY-REGISTRY-JOURNEY */
18
+ test('runs create, selection, registration, mode, IdP, and deletion in a real process', async () => {
19
+ const child = Bun.spawn(
20
+ [process.execPath, join(import.meta.dir, 'fixtures', 'registry-journey.ts')],
21
+ {
22
+ env: { ...process.env, ASTRALE_HOME: root },
23
+ stdout: 'pipe',
24
+ stderr: 'pipe',
25
+ },
26
+ )
27
+ const [exitCode, stdout, stderr] = await Promise.all([
28
+ child.exited,
29
+ new Response(child.stdout).text(),
30
+ new Response(child.stderr).text(),
31
+ ])
32
+ expect(exitCode, stderr).toBe(0)
33
+
34
+ const result = JSON.parse(stdout) as {
35
+ store: {
36
+ default: string
37
+ identities: Record<
38
+ string,
39
+ {
40
+ source?: string
41
+ mode?: string
42
+ registrations?: Record<string, { iss: string; sub: string; registeredAt: string }>
43
+ }
44
+ >
45
+ }
46
+ selected: { name: string; subject: string }
47
+ selectedDeletion: string
48
+ aliceKey: boolean
49
+ bobKey: boolean
50
+ }
51
+ expect(result.store.default).toBe('alice')
52
+ expect(Object.keys(result.store.identities).sort()).toEqual(['alice', 'manager', 'workos'])
53
+ expect(result.store.identities.alice.registrations?.production).toEqual({
54
+ iss: 'https://kernel.example',
55
+ sub: 'node-alice',
56
+ registeredAt: '2026-08-11T12:00:00.000Z',
57
+ })
58
+ expect(result.store.identities.workos).toMatchObject({ source: 'idp', mode: 'remote' })
59
+ expect(result.selected).toMatchObject({ name: 'alice', subject: 'alice' })
60
+ expect(result.selectedDeletion).toContain('Cannot delete the default identity')
61
+ expect(result.aliceKey).toBe(true)
62
+ expect(result.bobKey).toBe(false)
63
+
64
+ const persisted = JSON.parse(await readFile(join(root, 'identities.json'), 'utf-8')) as {
65
+ version?: unknown
66
+ }
67
+ expect(persisted.version).toBe(1)
68
+ expect(stdout).not.toContain('"d":')
69
+ })
70
+ })
@@ -0,0 +1,198 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { access, mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ import { keypairPaths, persistKeypair, readKeypair } from '../../keys/index'
8
+ import { readIdentityStore, updateIdentityStore } from '../../state/index'
9
+ import {
10
+ decodeIdentityExport,
11
+ encodeIdentityExport,
12
+ exportIdentity,
13
+ importIdentity,
14
+ isEncryptedIdentityExport,
15
+ writeIdentityExport,
16
+ type IdentityExport,
17
+ } from '../index'
18
+
19
+ describe('identity transfer', () => {
20
+ let root = ''
21
+ let statePath = ''
22
+ let keysDir = ''
23
+ const now = () => new Date('2026-08-11T12:00:00.000Z')
24
+
25
+ beforeEach(async () => {
26
+ root = await mkdtemp(join(tmpdir(), 'astrale-identity-transfer-'))
27
+ statePath = join(root, 'identities.json')
28
+ keysDir = join(root, 'keys')
29
+ })
30
+
31
+ afterEach(async () => {
32
+ await rm(root, { recursive: true, force: true })
33
+ })
34
+
35
+ async function envelope(subject: string, suffix: string): Promise<IdentityExport> {
36
+ const pair = await persistKeypair(subject, {
37
+ keysDir: join(root, `source-${suffix}`),
38
+ kid: `${subject}-${suffix}`,
39
+ })
40
+ return decodeIdentityExport(
41
+ JSON.stringify({
42
+ version: 1,
43
+ subject,
44
+ mode: 'local',
45
+ kid: pair.kid,
46
+ issuer: 'https://identity.example',
47
+ privateJwk: pair.privateJwk,
48
+ publicJwk: pair.publicJwk,
49
+ }),
50
+ )
51
+ }
52
+
53
+ /** @evidence TEST-CLI-IDENTITY-TRANSFER-REJECTS */
54
+ test('rejects malformed, newer, and mismatched content before durable mutation', async () => {
55
+ await expect(decodeIdentityExport('{')).rejects.toThrow(/not valid JSON/)
56
+ await expect(
57
+ decodeIdentityExport(
58
+ JSON.stringify({
59
+ version: 2,
60
+ subject: 'alice',
61
+ mode: 'local',
62
+ privateJwk: {},
63
+ publicJwk: {},
64
+ }),
65
+ ),
66
+ ).rejects.toThrow(/invalid or unsupported shape/)
67
+
68
+ const alice = await envelope('alice', 'a')
69
+ const bob = await envelope('bob', 'b')
70
+ const forged = {
71
+ ...alice,
72
+ publicJwk: { ...bob.publicJwk, kid: alice.kid },
73
+ } as IdentityExport
74
+ const mismatch = await importIdentity(forged, {
75
+ state: { path: statePath, now },
76
+ keysDir,
77
+ }).catch((error) => error)
78
+ expect(mismatch).toBeInstanceOf(Error)
79
+ expect((mismatch as Error).message).toMatch(/do not form a valid matching pair/)
80
+ expect((mismatch as Error).message).not.toContain(String(alice.privateJwk.d))
81
+
82
+ await expect(access(statePath)).rejects.toThrow()
83
+ await expect(access(keypairPaths('alice', keysDir).privatePath)).rejects.toThrow()
84
+ })
85
+
86
+ /** @evidence TEST-CLI-IDENTITY-TRANSFER-ROUNDTRIP */
87
+ test('converges legacy, V1, and encrypted representations on one envelope', async () => {
88
+ const current = await envelope('alice', 'roundtrip')
89
+ const { version: _, ...legacy } = current
90
+ expect(await decodeIdentityExport(JSON.stringify(legacy))).toEqual(current)
91
+
92
+ const plaintext = await encodeIdentityExport(current)
93
+ expect(isEncryptedIdentityExport(plaintext)).toBe(false)
94
+ expect(await decodeIdentityExport(plaintext)).toEqual(current)
95
+
96
+ const encrypted = await encodeIdentityExport(current, 'correct-horse-battery')
97
+ expect(isEncryptedIdentityExport(encrypted)).toBe(true)
98
+ expect(await decodeIdentityExport(encrypted, 'correct-horse-battery')).toEqual(current)
99
+ await expect(decodeIdentityExport(encrypted, 'wrong')).rejects.toThrow(/Could not decrypt/)
100
+ })
101
+
102
+ /** @evidence TEST-CLI-IDENTITY-TRANSFER-SCHEMA */
103
+ test('keeps the portable V1 JSON Schema aligned with the admitted identity envelope', async () => {
104
+ const schemaPath = fileURLToPath(
105
+ new URL('../.spec/schemas/identity-export-v1.schema.json', import.meta.url),
106
+ )
107
+ const schema = JSON.parse(await readFile(schemaPath, 'utf8')) as {
108
+ readonly $id: string
109
+ readonly additionalProperties: boolean
110
+ readonly required: readonly string[]
111
+ readonly properties: Readonly<Record<string, unknown>>
112
+ }
113
+ const identityEnvelope = await envelope('manager-principal', 'host-bootstrap')
114
+
115
+ expect(schema.$id).toBe('https://schemas.astrale.ai/cli/identity-export/1')
116
+ expect(schema.additionalProperties).toBe(false)
117
+ expect(schema.required).toEqual(['version', 'subject', 'privateJwk', 'publicJwk'])
118
+ expect(Object.keys(schema.properties).sort()).toEqual(
119
+ ['issuer', 'kid', 'mode', 'privateJwk', 'publicJwk', 'subject', 'version'].sort(),
120
+ )
121
+ await expect(decodeIdentityExport(JSON.stringify(identityEnvelope))).resolves.toEqual(
122
+ identityEnvelope,
123
+ )
124
+ })
125
+
126
+ /** @evidence TEST-CLI-IDENTITY-IMPORT-ORDERED */
127
+ test('checks conflicts before key replacement and publishes registry after keys', async () => {
128
+ const first = await envelope('alice', 'first')
129
+ const imported = await importIdentity(first, {
130
+ name: 'operator',
131
+ state: { path: statePath, now },
132
+ keysDir,
133
+ })
134
+ expect(imported.subject).toBe('alice')
135
+ expect((await readKeypair('alice', keysDir)).kid).toBe(first.kid)
136
+ expect((await readIdentityStore({ path: statePath })).identities.operator).toEqual(imported)
137
+
138
+ const privatePath = keypairPaths('alice', keysDir).privatePath
139
+ const originalPrivate = await readFile(privatePath, 'utf-8')
140
+ const replacement = await envelope('alice', 'replacement')
141
+ await expect(
142
+ importIdentity(replacement, {
143
+ name: 'operator',
144
+ state: { path: statePath, now },
145
+ keysDir,
146
+ }),
147
+ ).rejects.toThrow('already exists')
148
+ expect(await readFile(privatePath, 'utf-8')).toBe(originalPrivate)
149
+
150
+ await updateIdentityStore(
151
+ (store) => ({
152
+ next: {
153
+ ...store,
154
+ identities: {
155
+ ...store.identities,
156
+ remote: {
157
+ subject: 'remote-user',
158
+ createdAt: now().toISOString(),
159
+ source: 'idp',
160
+ mode: 'remote',
161
+ idp: 'workos',
162
+ issuer: 'https://idp.example',
163
+ },
164
+ },
165
+ },
166
+ value: undefined,
167
+ }),
168
+ { path: statePath, now },
169
+ )
170
+ await expect(
171
+ importIdentity(await envelope('remote-user', 'remote'), {
172
+ name: 'remote',
173
+ replace: true,
174
+ state: { path: statePath, now },
175
+ keysDir,
176
+ }),
177
+ ).rejects.toThrow(/IdP-backed/)
178
+ await expect(access(keypairPaths('remote-user', keysDir).privatePath)).rejects.toThrow()
179
+ })
180
+
181
+ /** @evidence TEST-CLI-IDENTITY-EXPORT-PRIVATE */
182
+ test('exports a proven pair through one atomic mode-0600 file', async () => {
183
+ await importIdentity(await envelope('alice', 'export'), {
184
+ state: { path: statePath, now },
185
+ keysDir,
186
+ })
187
+ const exported = await exportIdentity('alice', {
188
+ state: { path: statePath, now },
189
+ keysDir,
190
+ })
191
+ const path = join(root, 'alice.identity.json')
192
+ await writeIdentityExport(path, await encodeIdentityExport(exported))
193
+
194
+ expect((await stat(path)).mode & 0o777).toBe(0o600)
195
+ expect(await decodeIdentityExport(await readFile(path, 'utf-8'))).toEqual(exported)
196
+ expect((await readdir(root)).some((entry) => entry.endsWith('.tmp'))).toBe(false)
197
+ })
198
+ })
@@ -0,0 +1,23 @@
1
+ export {
2
+ createIdentity,
3
+ deleteIdentity,
4
+ getDefault,
5
+ getIdentity,
6
+ readIdentities,
7
+ setDefault,
8
+ setIdentityMode,
9
+ setRegistration,
10
+ upsertIdpIdentity,
11
+ } from './registry'
12
+ export {
13
+ decodeIdentityExport,
14
+ encodeIdentityExport,
15
+ exportIdentity,
16
+ importIdentity,
17
+ isEncryptedIdentityExport,
18
+ writeIdentityExport,
19
+ } from './transfer'
20
+ export { acceptProvisionedIdentity, submitIdentityProvision } from './registration'
21
+ export type { IdentityProvisionSubmission, IdentityRegistrationResult } from './registration'
22
+ export type { Identity, IdentityStore, Registration } from './registry'
23
+ export type { IdentityExport, IdentityFileOptions, IdentityImportOptions } from './transfer'
@@ -0,0 +1,72 @@
1
+ import type { Call } from '@astrale-os/kernel-client'
2
+ import type { ProvisionRequest } from '@astrale-os/sdk/auth'
3
+ import type { LocalBinding } from '@astrale-os/sdk/graph/model'
4
+
5
+ import { call } from '@astrale-os/kernel-client'
6
+ import { issuer } from '@astrale-os/sdk/auth'
7
+ import { NodeId } from '@astrale-os/sdk/graph/node'
8
+ import { Path } from '@astrale-os/sdk/graph/path'
9
+
10
+ export interface IdentityRegistrationResult {
11
+ readonly iss: string
12
+ readonly sub: string
13
+ readonly nodeId?: string
14
+ }
15
+
16
+ export interface IdentityProvisionSubmission {
17
+ readonly request: ProvisionRequest
18
+ readonly binding: LocalBinding
19
+ readonly via?: string
20
+ readonly direct: {
21
+ provision(request: ProvisionRequest): Promise<unknown>
22
+ }
23
+ readonly callable: {
24
+ call(call: Call): Promise<unknown>
25
+ }
26
+ }
27
+
28
+ /** Submit one prepared request either directly or through its explicit Domain authority owner. */
29
+ export async function submitIdentityProvision(
30
+ input: IdentityProvisionSubmission,
31
+ ): Promise<IdentityRegistrationResult> {
32
+ const result =
33
+ input.via === undefined
34
+ ? await input.direct.provision(input.request)
35
+ : await input.callable.call(
36
+ // Client admits the portable value before transport. ProvisionRequest is the
37
+ // stricter semantic type but does not declare the portable Object index signature.
38
+ call(Path.parse(input.via), input.request as unknown as Call['input']),
39
+ )
40
+ return acceptProvisionedIdentity(result, input.binding)
41
+ }
42
+
43
+ /** Admit only the exact binding the CLI prepared; remote callables remain untrusted input. */
44
+ export function acceptProvisionedIdentity(
45
+ value: unknown,
46
+ binding: LocalBinding,
47
+ ): IdentityRegistrationResult {
48
+ const result = record(value, 'Provision result')
49
+ const identities = record(result.identities, 'Provision result identities')
50
+ const identity = record(identities[binding], 'Provision result identity binding')
51
+ const createdNodes = record(result.createdNodes, 'Provision result created Nodes')
52
+ const node = createdNodes[binding]
53
+ return Object.freeze({
54
+ iss: issuer.accept(text(identity.issuer, 'Provisioned Identity issuer')),
55
+ sub: text(identity.subject, 'Provisioned Identity subject'),
56
+ ...(node === undefined ? {} : { nodeId: NodeId(text(node, 'Provisioned Identity Node')) }),
57
+ })
58
+ }
59
+
60
+ function record(value: unknown, label: string): Record<string, unknown> {
61
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
62
+ throw new TypeError(`${label} must be an object.`)
63
+ }
64
+ return value as Record<string, unknown>
65
+ }
66
+
67
+ function text(value: unknown, label: string): string {
68
+ if (typeof value !== 'string' || value.length === 0) {
69
+ throw new TypeError(`${label} must be non-empty text.`)
70
+ }
71
+ return value
72
+ }
@@ -0,0 +1,172 @@
1
+ import { persistKeypair, removeKeypair } from '../keys/index'
2
+ import { deleteIdpSession } from '../lib/idp'
3
+ import { validateName } from '../lib/validation'
4
+ import {
5
+ readIdentityStore,
6
+ updateIdentityStore,
7
+ type Identity,
8
+ type IdentityMode,
9
+ type IdentityStore,
10
+ type Registration,
11
+ ExchangeCredentialCache,
12
+ } from '../state/index'
13
+
14
+ export type { Identity, IdentityStore, Registration } from '../state/index'
15
+
16
+ export async function readIdentities(): Promise<IdentityStore> {
17
+ return readIdentityStore()
18
+ }
19
+
20
+ export async function createIdentity(
21
+ name: string,
22
+ options: {
23
+ readonly subject?: string
24
+ readonly mode?: IdentityMode
25
+ readonly issuer?: string
26
+ readonly kid?: string
27
+ } = {},
28
+ ): Promise<Identity> {
29
+ validateName(name, 'Identity')
30
+ return updateIdentityStore(async (store) => {
31
+ if (store.identities[name]) throw new Error(`Identity "${name}" already exists`)
32
+
33
+ const subject = options.subject ?? name
34
+ const generated = await persistKeypair(subject, { kid: options.kid })
35
+ const identity: Identity = {
36
+ subject,
37
+ createdAt: new Date().toISOString(),
38
+ source: 'key',
39
+ mode: options.mode ?? 'local',
40
+ kid: generated.kid,
41
+ issuer: options.issuer,
42
+ }
43
+ return {
44
+ next: { ...store, identities: { ...store.identities, [name]: identity } },
45
+ value: identity,
46
+ }
47
+ })
48
+ }
49
+
50
+ export async function deleteIdentity(name: string): Promise<void> {
51
+ await updateIdentityStore(async (store) => {
52
+ const entry = store.identities[name]
53
+ if (!entry) throw new Error(`Identity "${name}" not found`)
54
+ if (store.default === name) {
55
+ throw new Error(
56
+ `Cannot delete the default identity "${name}". Switch default first with: astrale identity use <other>`,
57
+ )
58
+ }
59
+ if ((entry.source ?? 'key') === 'idp') await deleteIdpSession(name)
60
+ else if (!hasAnotherKeyIdentity(store, name, entry.subject)) await removeKeypair(entry.subject)
61
+
62
+ const { [name]: _, ...identities } = store.identities
63
+ return { next: { ...store, identities }, value: undefined }
64
+ })
65
+ await new ExchangeCredentialCache().clear()
66
+ }
67
+
68
+ function hasAnotherKeyIdentity(store: IdentityStore, name: string, subject: string): boolean {
69
+ return Object.entries(store.identities).some(
70
+ ([candidateName, candidate]) =>
71
+ candidateName !== name &&
72
+ (candidate.source ?? 'key') === 'key' &&
73
+ candidate.subject === subject,
74
+ )
75
+ }
76
+
77
+ export async function setDefault(name: string): Promise<void> {
78
+ await updateIdentityStore((store) => {
79
+ if (!store.identities[name]) throw new Error(`Identity "${name}" not found`)
80
+ return { next: { ...store, default: name }, value: undefined }
81
+ })
82
+ }
83
+
84
+ export async function getDefault(): Promise<Identity & { readonly name: string }> {
85
+ const store = await readIdentities()
86
+ const identity = store.identities[store.default]
87
+ if (!identity) {
88
+ throw new Error(
89
+ `Default identity "${store.default}" not found. Run: astrale identity create ${store.default}`,
90
+ )
91
+ }
92
+ return { ...identity, name: store.default }
93
+ }
94
+
95
+ export async function getIdentity(name: string): Promise<Identity> {
96
+ const store = await readIdentities()
97
+ const identity = store.identities[name]
98
+ if (!identity) {
99
+ throw new Error(`Identity "${name}" not found. Run: astrale identity create ${name}`)
100
+ }
101
+ return identity
102
+ }
103
+
104
+ export async function upsertIdpIdentity(
105
+ name: string,
106
+ options: {
107
+ readonly subject: string
108
+ readonly idp: string
109
+ readonly issuer: string
110
+ readonly audience?: string
111
+ readonly claims?: Readonly<Record<string, unknown>>
112
+ readonly use?: boolean
113
+ },
114
+ ): Promise<Identity> {
115
+ validateName(name, 'Identity')
116
+ validateName(options.idp, 'IdP')
117
+ return updateIdentityStore((store) => {
118
+ const existing = store.identities[name]
119
+ if (existing && (existing.source ?? 'key') !== 'idp') {
120
+ throw new Error(`Identity "${name}" already exists and is key-backed`)
121
+ }
122
+ const identity: Identity = {
123
+ subject: options.subject,
124
+ createdAt: existing?.createdAt ?? new Date().toISOString(),
125
+ source: 'idp',
126
+ mode: 'remote',
127
+ idp: options.idp,
128
+ issuer: options.issuer,
129
+ audience: options.audience,
130
+ claims: options.claims,
131
+ registrations: existing?.registrations,
132
+ }
133
+ return {
134
+ next: {
135
+ default: options.use === false ? store.default : name,
136
+ identities: { ...store.identities, [name]: identity },
137
+ },
138
+ value: identity,
139
+ }
140
+ })
141
+ }
142
+
143
+ export async function setRegistration(
144
+ name: string,
145
+ instanceSlug: string,
146
+ registration: Registration,
147
+ ): Promise<void> {
148
+ await updateIdentityStore((store) => {
149
+ const entry = store.identities[name]
150
+ if (!entry) throw new Error(`Identity "${name}" not found`)
151
+ const identity: Identity = {
152
+ ...entry,
153
+ registrations: { ...entry.registrations, [instanceSlug]: registration },
154
+ }
155
+ return {
156
+ next: { ...store, identities: { ...store.identities, [name]: identity } },
157
+ value: undefined,
158
+ }
159
+ })
160
+ }
161
+
162
+ export async function setIdentityMode(name: string, mode: IdentityMode): Promise<void> {
163
+ await updateIdentityStore((store) => {
164
+ const entry = store.identities[name]
165
+ if (!entry) throw new Error(`Identity "${name}" not found`)
166
+ const identity: Identity = { ...entry, mode }
167
+ return {
168
+ next: { ...store, identities: { ...store.identities, [name]: identity } },
169
+ value: undefined,
170
+ }
171
+ })
172
+ }