@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
@@ -1,129 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
- import { CompactEncrypt, compactDecrypt, decodeProtectedHeader } from 'jose'
3
- import { access, mkdtemp, rm, writeFile } from 'node:fs/promises'
4
- import { tmpdir } from 'node:os'
5
- import { join } from 'node:path'
6
-
7
- import { IdentityKeyMissingError } from '../../errors'
8
- import {
9
- generateEd25519Jwk,
10
- keypairPaths,
11
- listIdentityKeys,
12
- persistKeypair,
13
- removeKeypair,
14
- signAs,
15
- } from '../keys'
16
-
17
- describe('DESIGN — per-identity keys', () => {
18
- let tmp = ''
19
- const savedStrict = process.env.ASTRALE_STRICT_IDENTITIES
20
-
21
- beforeEach(async () => {
22
- tmp = await mkdtemp(join(tmpdir(), 'astrale-keys-test-'))
23
- delete process.env.ASTRALE_STRICT_IDENTITIES
24
- })
25
-
26
- afterEach(async () => {
27
- if (savedStrict) process.env.ASTRALE_STRICT_IDENTITIES = savedStrict
28
- else delete process.env.ASTRALE_STRICT_IDENTITIES
29
- await rm(tmp, { recursive: true, force: true })
30
- })
31
-
32
- test('keypairPaths routes manager to legacy filenames', () => {
33
- const p = keypairPaths('manager', tmp)
34
- expect(p.privatePath.endsWith('manager.private.jwk')).toBe(true)
35
- expect(p.publicPath.endsWith('manager.public.jwk')).toBe(true)
36
- })
37
-
38
- test('keypairPaths routes others to <name>.*.jwk', () => {
39
- const p = keypairPaths('alice', tmp)
40
- expect(p.privatePath.endsWith('alice.private.jwk')).toBe(true)
41
- expect(p.publicPath.endsWith('alice.public.jwk')).toBe(true)
42
- })
43
-
44
- test('persistKeypair writes both files with kid', async () => {
45
- const result = await persistKeypair('alice', { keysDir: tmp })
46
- expect(result.kid).toMatch(/^alice-key-/)
47
- const { privatePath, publicPath } = keypairPaths('alice', tmp)
48
- await access(privatePath)
49
- await access(publicPath)
50
- })
51
-
52
- test('signAs(alice) works when alice has her own key', async () => {
53
- await persistKeypair('alice', { keysDir: tmp })
54
- const jwt = await signAs('alice', tmp)
55
- expect(jwt.split('.').length).toBe(3)
56
- })
57
-
58
- test('signAs(alice) uses the key file algorithm for EdDSA identities', async () => {
59
- const { privateJwk, publicJwk } = await generateEd25519Jwk('alice-ed25519')
60
- const { privatePath, publicPath } = keypairPaths('alice', tmp)
61
- await writeFile(privatePath, JSON.stringify(privateJwk, null, 2))
62
- await writeFile(publicPath, JSON.stringify(publicJwk, null, 2))
63
-
64
- const jwt = await signAs('alice', tmp)
65
-
66
- expect(decodeProtectedHeader(jwt).alg).toBe('EdDSA')
67
- })
68
-
69
- test('signAs(alice) falls back to manager key with warning', async () => {
70
- await persistKeypair('manager', { keysDir: tmp })
71
- const jwt = await signAs('alice', tmp)
72
- expect(jwt.split('.').length).toBe(3)
73
- })
74
-
75
- test('signAs(alice) errors when strict mode and no alice key', async () => {
76
- await persistKeypair('manager', { keysDir: tmp })
77
- process.env.ASTRALE_STRICT_IDENTITIES = '1'
78
- await expect(signAs('alice', tmp)).rejects.toThrow(IdentityKeyMissingError)
79
- })
80
-
81
- test('signAs errors when no keys at all', async () => {
82
- await expect(signAs('manager', tmp)).rejects.toThrow(IdentityKeyMissingError)
83
- })
84
-
85
- test('removeKeypair is idempotent', async () => {
86
- await persistKeypair('alice', { keysDir: tmp })
87
- await removeKeypair('alice', tmp)
88
- await removeKeypair('alice', tmp)
89
- const { privatePath } = keypairPaths('alice', tmp)
90
- await expect(access(privatePath)).rejects.toThrow()
91
- })
92
-
93
- test('listIdentityKeys returns names with private keys', async () => {
94
- await persistKeypair('manager', { keysDir: tmp })
95
- await persistKeypair('alice', { keysDir: tmp })
96
- await persistKeypair('bob', { keysDir: tmp })
97
- const names = await listIdentityKeys(tmp)
98
- expect(names.sort()).toEqual(['alice', 'bob', 'manager'])
99
- })
100
-
101
- test('encrypted export envelope round-trip (JWE PBES2)', async () => {
102
- await persistKeypair('alice', { keysDir: tmp })
103
- const plain = JSON.stringify({ subject: 'alice', secret: 'x' })
104
- const passphrase = 'correct-horse-battery'
105
- const enc = await new CompactEncrypt(new TextEncoder().encode(plain))
106
- .setProtectedHeader({ alg: 'PBES2-HS256+A128KW', enc: 'A256GCM' })
107
- .encrypt(new TextEncoder().encode(passphrase))
108
-
109
- expect(enc.split('.').length).toBe(5)
110
-
111
- const { plaintext } = await compactDecrypt(enc, new TextEncoder().encode(passphrase), {
112
- keyManagementAlgorithms: ['PBES2-HS256+A128KW'],
113
- })
114
- expect(new TextDecoder().decode(plaintext)).toBe(plain)
115
- })
116
-
117
- test('encrypted export fails with wrong passphrase', async () => {
118
- const plain = JSON.stringify({ subject: 'alice' })
119
- const enc = await new CompactEncrypt(new TextEncoder().encode(plain))
120
- .setProtectedHeader({ alg: 'PBES2-HS256+A128KW', enc: 'A256GCM' })
121
- .encrypt(new TextEncoder().encode('correct'))
122
-
123
- await expect(
124
- compactDecrypt(enc, new TextEncoder().encode('wrong'), {
125
- keyManagementAlgorithms: ['PBES2-HS256+A128KW'],
126
- }),
127
- ).rejects.toThrow()
128
- })
129
- })
@@ -1,49 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { resolve } from 'node:path'
3
-
4
- import { AstraleError } from '../errors'
5
-
6
- export async function loadPrivateJwk(keyPath: string): Promise<Record<string, unknown>> {
7
- const filePath = resolve(keyPath)
8
- const raw = await readFile(filePath, 'utf-8')
9
- return JSON.parse(raw) as Record<string, unknown>
10
- }
11
-
12
- /**
13
- * True when the kernel rejected a `Domain.install` call because the
14
- * identity binding's signature failed to verify — i.e. the private/public
15
- * JWK halves don't match. Callers re-throw with a regenerate-the-keypair
16
- * hint instead of leaking the raw `AuthenticationError`.
17
- */
18
- export function isSignatureVerificationError(e: unknown): boolean {
19
- if (!e || typeof e !== 'object') return false
20
- const name = (e as { name?: unknown }).name
21
- const msg = (e as { message?: unknown }).message
22
- return (
23
- name === 'AuthenticationError' &&
24
- typeof msg === 'string' &&
25
- /signature verification failed/i.test(msg)
26
- )
27
- }
28
-
29
- /**
30
- * Resolve the JOSE algorithm to use with this key. Prefers `privateJwk.alg`
31
- * when present, falls back to inferring from `crv`/`kty` so keys generated
32
- * by older CLIs (which didn't stamp `alg`) keep working without manual
33
- * editing — see META_TRACE #34. Throws a clean error if neither path
34
- * resolves an algorithm.
35
- */
36
- export function inferAlg(privateJwk: Record<string, unknown>, keyPath?: string): string {
37
- const explicit = privateJwk.alg
38
- if (typeof explicit === 'string' && explicit.length > 0) return explicit
39
- const crv = privateJwk.crv
40
- const kty = privateJwk.kty
41
- if (kty === 'EC' && crv === 'P-256') return 'ES256'
42
- if (kty === 'OKP' && crv === 'Ed25519') return 'EdDSA'
43
- const where = keyPath ? ` at ${keyPath}` : ''
44
- throw new AstraleError(
45
- 'INVALID_KEY_FILE',
46
- `JWK${where} is missing both \`alg\` and a recognizable \`(kty, crv)\` pair — cannot pick a signing algorithm.`,
47
- 'Re-stamp the file with `"alg": "ES256"` (P-256 EC keys) or `"alg": "EdDSA"` (Ed25519 OKP keys), or regenerate via `astrale domain init` / `astrale identity create`.',
48
- )
49
- }
package/src/lib/env.ts DELETED
@@ -1,49 +0,0 @@
1
- import { homedir } from 'node:os'
2
- import { join } from 'node:path'
3
-
4
- export type Paths = {
5
- home: string
6
- keys: string
7
- data: string
8
- config: string
9
- install: string
10
- identities: string
11
- instances: string
12
- idps: string
13
- idpSessionsDir: string
14
- /** Per-IdP config dir: `~/.astrale/idps/<name>/`. */
15
- idpDir: (name: string) => string
16
- /** Per-identity IdP session cache: `~/.astrale/idp-sessions/<name>.json`. */
17
- idpSession: (identityName: string) => string
18
- }
19
-
20
- /**
21
- * Resolve the Astrale home dir.
22
- *
23
- * Priority: explicit arg → `ASTRALE_HOME` env var → `$HOME/.astrale`.
24
- */
25
- function resolveHome(home?: string): string {
26
- return home ?? process.env.ASTRALE_HOME ?? join(homedir(), '.astrale')
27
- }
28
-
29
- export function createPaths(home?: string): Paths {
30
- const base = resolveHome(home)
31
- const idpsDir = join(base, 'idps')
32
- const idpSessionsDir = join(base, 'idp-sessions')
33
- return {
34
- home: base,
35
- keys: process.env.ASTRALE_KEYS_DIR ?? join(base, 'keys'),
36
- data: process.env.ASTRALE_DATA_DIR ?? join(base, 'data'),
37
- config: join(base, 'config.json'),
38
- install: join(base, 'install.json'),
39
- identities: join(base, 'identities.json'),
40
- instances: join(base, 'instances.json'),
41
- idps: join(idpsDir, 'index.json'),
42
- idpSessionsDir,
43
- idpDir: (name: string) => join(idpsDir, name),
44
- idpSession: (identityName: string) => join(idpSessionsDir, `${identityName}.json`),
45
- }
46
- }
47
-
48
- /** Default singleton used by all lib modules. */
49
- export const paths: Paths = createPaths()
@@ -1,126 +0,0 @@
1
- import { randomUUID } from 'node:crypto'
2
- import { mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
3
- import { dirname } from 'node:path'
4
-
5
- /** Write-tmp-then-rename so readers never observe a partial file. Mode 0600. */
6
- export async function atomicWrite(path: string, data: string): Promise<void> {
7
- const tmp = `${path}.${randomUUID()}.tmp`
8
- await writeFile(tmp, data, { mode: 0o600 })
9
- await rename(tmp, path)
10
- }
11
-
12
- export type FileLockOptions = {
13
- /** Delay between acquisition attempts while another process holds the lock. */
14
- pollIntervalMs?: number
15
- /** A lock file whose mtime is older than this is considered abandoned. */
16
- staleAfterMs?: number
17
- /** Give up acquiring after this long. */
18
- timeoutMs?: number
19
- }
20
-
21
- /**
22
- * Run `fn` under a cross-process mutex backed by a create-exclusive lock file.
23
- *
24
- * Contenders poll until the holder releases (unlinks) the lock. Abandoned
25
- * locks are taken over when the holder pid is dead or the file's mtime
26
- * exceeds `staleAfterMs` — a crash mid-section can therefore delay other
27
- * processes by up to `staleAfterMs`, never deadlock them.
28
- */
29
- export async function withFileLock<T>(
30
- lockPath: string,
31
- fn: () => Promise<T>,
32
- opts: FileLockOptions = {},
33
- ): Promise<T> {
34
- const pollIntervalMs = opts.pollIntervalMs ?? 100
35
- const staleAfterMs = opts.staleAfterMs ?? 30_000
36
- const timeoutMs = opts.timeoutMs ?? 60_000
37
- const deadline = Date.now() + timeoutMs
38
-
39
- for (;;) {
40
- const acquired = await tryAcquire(lockPath)
41
- if (acquired) {
42
- try {
43
- return await fn()
44
- } finally {
45
- await unlink(lockPath).catch(() => undefined)
46
- }
47
- }
48
- if (await takeoverIfAbandoned(lockPath, staleAfterMs)) continue
49
- if (Date.now() >= deadline) {
50
- throw new Error(`Timed out after ${timeoutMs}ms waiting for lock ${lockPath}`)
51
- }
52
- await sleep(pollIntervalMs)
53
- }
54
- }
55
-
56
- async function tryAcquire(lockPath: string): Promise<boolean> {
57
- try {
58
- const handle = await open(lockPath, 'wx', 0o600)
59
- try {
60
- await handle.writeFile(
61
- JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }),
62
- )
63
- } finally {
64
- await handle.close()
65
- }
66
- return true
67
- } catch (e) {
68
- const code = (e as { code?: string }).code
69
- if (code === 'EEXIST') return false
70
- if (code === 'ENOENT') {
71
- await mkdir(dirname(lockPath), { recursive: true })
72
- return tryAcquire(lockPath)
73
- }
74
- throw e
75
- }
76
- }
77
-
78
- /**
79
- * Remove the lock if its holder is gone (dead pid, fast) or it outlived
80
- * `staleAfterMs` (mtime, covers unreadable pids and cross-container holders).
81
- * Returns true when the caller should immediately retry acquisition —
82
- * `open('wx')` re-arbitrates between concurrent takeover candidates.
83
- */
84
- async function takeoverIfAbandoned(lockPath: string, staleAfterMs: number): Promise<boolean> {
85
- try {
86
- const info = await stat(lockPath)
87
- if (Date.now() - info.mtimeMs > staleAfterMs) {
88
- await unlink(lockPath).catch(() => undefined)
89
- return true
90
- }
91
- } catch (e) {
92
- // Lock vanished between attempts: holder released — retry right away.
93
- if ((e as { code?: string }).code === 'ENOENT') return true
94
- throw e
95
- }
96
- const pid = await readLockPid(lockPath)
97
- if (pid !== undefined && pid !== process.pid && !isPidAlive(pid)) {
98
- await unlink(lockPath).catch(() => undefined)
99
- return true
100
- }
101
- return false
102
- }
103
-
104
- async function readLockPid(lockPath: string): Promise<number | undefined> {
105
- try {
106
- const raw = await readFile(lockPath, 'utf-8')
107
- const pid = (JSON.parse(raw) as { pid?: unknown }).pid
108
- return typeof pid === 'number' ? pid : undefined
109
- } catch {
110
- return undefined
111
- }
112
- }
113
-
114
- function isPidAlive(pid: number): boolean {
115
- try {
116
- process.kill(pid, 0)
117
- return true
118
- } catch (e) {
119
- // EPERM means the pid exists but belongs to another user — still alive.
120
- return (e as { code?: string }).code === 'EPERM'
121
- }
122
- }
123
-
124
- function sleep(ms: number): Promise<void> {
125
- return new Promise((resolve) => setTimeout(resolve, ms))
126
- }
@@ -1,256 +0,0 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises'
2
- import { dirname } from 'node:path'
3
- import { z } from 'zod'
4
-
5
- import { deleteIdpSession } from './idp'
6
- import { persistKeypair, removeKeypair } from './keys'
7
- import { log } from './log'
8
- import { IDENTITIES_PATH } from './paths'
9
- import { RegistryModeSchema, validateName, type RegistryMode } from './validation'
10
-
11
- export const RegistrationSchema = z.object({
12
- iss: z.string(),
13
- sub: z.string(),
14
- registeredAt: z.string(),
15
- })
16
-
17
- export const IdentitySchema = z.object({
18
- subject: z.string(),
19
- createdAt: z.string(),
20
- /** `key` identities sign local JWTs; `idp` identities reuse OAuth/OIDC access tokens. */
21
- source: z.enum(['key', 'idp']).optional(),
22
- // `local` = machine-only, `remote` = mirrored via astrale cloud (§2.7).
23
- mode: RegistryModeSchema.optional(),
24
- /** JWK thumbprint of the identity keypair. Optional for legacy entries. */
25
- kid: z.string().optional(),
26
- /** IdP registry name for source=idp identities. */
27
- idp: z.string().optional(),
28
- /** OIDC issuer for source=idp identities. */
29
- issuer: z.string().url().optional(),
30
- /** Optional preferred audience used during IdP login/refresh flows. */
31
- audience: z.string().optional(),
32
- /** Non-secret claims snapshot from the last successful IdP login. */
33
- claims: z.record(z.string(), z.unknown()).optional(),
34
- /**
35
- * Cache of `(iss, sub)` pairs returned by `Identity::registerIdentity`,
36
- * keyed by instance slug. Populated by `astrale identity register`; consulted
37
- * by `resolveCredential` so `--as <name> -i <instance>` signs JWTs the kernel
38
- * accepts.
39
- */
40
- registrations: z.record(z.string(), RegistrationSchema).optional(),
41
- })
42
-
43
- export type Registration = z.infer<typeof RegistrationSchema>
44
-
45
- export const IdentityStoreSchema = z.object({
46
- default: z.string(),
47
- identities: z.record(z.string(), IdentitySchema),
48
- })
49
-
50
- export type Identity = z.infer<typeof IdentitySchema>
51
- export type IdentityStore = z.infer<typeof IdentityStoreSchema>
52
-
53
- function seed(): IdentityStore {
54
- return {
55
- default: 'manager',
56
- identities: {
57
- manager: {
58
- subject: 'manager',
59
- createdAt: new Date().toISOString(),
60
- source: 'key',
61
- mode: 'local',
62
- },
63
- },
64
- }
65
- }
66
-
67
- export async function readIdentities(): Promise<IdentityStore> {
68
- try {
69
- const raw = await readFile(IDENTITIES_PATH, 'utf-8')
70
- return IdentityStoreSchema.parse(JSON.parse(raw))
71
- } catch (e) {
72
- if (e instanceof z.ZodError) {
73
- log.warn(`Invalid identities at ${IDENTITIES_PATH} — using defaults`)
74
- } else if ((e as { code?: string }).code !== 'ENOENT') {
75
- // Present-but-unreadable file (malformed JSON → SyntaxError, or a
76
- // non-missing-file read error). ENOENT is the legitimate first-run
77
- // case and stays silent; anything else would otherwise be discarded
78
- // and could be overwritten by a later writeIdentities.
79
- log.warn(`Could not read identities at ${IDENTITIES_PATH} — using defaults`)
80
- }
81
- return seed()
82
- }
83
- }
84
-
85
- export async function writeIdentities(store: IdentityStore): Promise<void> {
86
- await mkdir(dirname(IDENTITIES_PATH), { recursive: true })
87
- await writeFile(IDENTITIES_PATH, JSON.stringify(store, null, 2) + '\n')
88
- }
89
-
90
- export async function createIdentity(
91
- name: string,
92
- opts: {
93
- subject?: string
94
- mode?: RegistryMode
95
- issuer?: string
96
- kid?: string
97
- skipKeygen?: boolean
98
- } = {},
99
- ): Promise<Identity> {
100
- validateName(name, 'Identity')
101
- const store = await readIdentities()
102
- if (store.identities[name]) {
103
- throw new Error(`Identity "${name}" already exists`)
104
- }
105
- const subject = opts.subject ?? name
106
- let kid: string | undefined
107
- if (!opts.skipKeygen) {
108
- const result = await persistKeypair(subject)
109
- kid = result.kid
110
- }
111
- const identity: Identity = {
112
- subject,
113
- createdAt: new Date().toISOString(),
114
- source: 'key',
115
- mode: opts.mode ?? 'local',
116
- kid: opts.kid ?? kid,
117
- issuer: opts.issuer,
118
- }
119
- store.identities[name] = identity
120
- await writeIdentities(store)
121
- return identity
122
- }
123
-
124
- export async function upsertKeyIdentity(
125
- name: string,
126
- opts: {
127
- subject: string
128
- mode?: RegistryMode
129
- issuer?: string
130
- kid?: string
131
- },
132
- ): Promise<Identity> {
133
- validateName(name, 'Identity')
134
- const store = await readIdentities()
135
- const existing = store.identities[name]
136
- if (existing && (existing.source ?? 'key') !== 'key') {
137
- throw new Error(`Identity "${name}" already exists and is IdP-backed`)
138
- }
139
- const identity: Identity = {
140
- subject: opts.subject,
141
- createdAt: existing?.createdAt ?? new Date().toISOString(),
142
- source: 'key',
143
- mode: opts.mode ?? existing?.mode ?? 'local',
144
- kid: opts.kid,
145
- issuer: opts.issuer,
146
- }
147
- store.identities[name] = identity
148
- await writeIdentities(store)
149
- return identity
150
- }
151
-
152
- export async function deleteIdentity(name: string): Promise<void> {
153
- const store = await readIdentities()
154
- const entry = store.identities[name]
155
- if (!entry) {
156
- throw new Error(`Identity "${name}" not found`)
157
- }
158
- if (store.default === name) {
159
- throw new Error(
160
- `Cannot delete the default identity "${name}". Switch default first with: astrale identity use <other>`,
161
- )
162
- }
163
- if ((entry.source ?? 'key') === 'idp') {
164
- await deleteIdpSession(name)
165
- } else {
166
- await removeKeypair(entry.subject)
167
- }
168
- delete store.identities[name]
169
- await writeIdentities(store)
170
- }
171
-
172
- export async function setDefault(name: string): Promise<void> {
173
- const store = await readIdentities()
174
- if (!store.identities[name]) {
175
- throw new Error(`Identity "${name}" not found`)
176
- }
177
- store.default = name
178
- await writeIdentities(store)
179
- }
180
-
181
- export async function getDefault(): Promise<Identity & { name: string }> {
182
- const store = await readIdentities()
183
- const identity = store.identities[store.default]
184
- if (!identity) {
185
- throw new Error(
186
- `Default identity "${store.default}" not found. Run: astrale identity create ${store.default}`,
187
- )
188
- }
189
- return { ...identity, name: store.default }
190
- }
191
-
192
- export async function getIdentity(name: string): Promise<Identity> {
193
- const store = await readIdentities()
194
- const identity = store.identities[name]
195
- if (!identity) {
196
- throw new Error(`Identity "${name}" not found. Run: astrale identity create ${name}`)
197
- }
198
- return identity
199
- }
200
-
201
- export async function upsertIdpIdentity(
202
- name: string,
203
- opts: {
204
- subject: string
205
- idp: string
206
- issuer: string
207
- audience?: string
208
- claims?: Record<string, unknown>
209
- use?: boolean
210
- },
211
- ): Promise<Identity> {
212
- validateName(name, 'Identity')
213
- validateName(opts.idp, 'IdP')
214
- const store = await readIdentities()
215
- const existing = store.identities[name]
216
- if (existing && (existing.source ?? 'key') !== 'idp') {
217
- throw new Error(`Identity "${name}" already exists and is key-backed`)
218
- }
219
- const identity: Identity = {
220
- subject: opts.subject,
221
- createdAt: existing?.createdAt ?? new Date().toISOString(),
222
- source: 'idp',
223
- mode: 'remote',
224
- idp: opts.idp,
225
- issuer: opts.issuer,
226
- audience: opts.audience,
227
- claims: opts.claims,
228
- registrations: existing?.registrations,
229
- }
230
- store.identities[name] = identity
231
- if (opts.use !== false) store.default = name
232
- await writeIdentities(store)
233
- return identity
234
- }
235
-
236
- /** Record (or replace) the kernel-derived (iss, sub) pair for an identity on a target instance. */
237
- export async function setRegistration(
238
- name: string,
239
- instanceSlug: string,
240
- registration: Registration,
241
- ): Promise<void> {
242
- const store = await readIdentities()
243
- const entry = store.identities[name]
244
- if (!entry) throw new Error(`Identity "${name}" not found`)
245
- entry.registrations = { ...entry.registrations, [instanceSlug]: registration }
246
- await writeIdentities(store)
247
- }
248
-
249
- /** Migrate an identity to a new registry mode (local ↔ remote, §2.7). */
250
- export async function setIdentityMode(name: string, mode: RegistryMode): Promise<void> {
251
- const store = await readIdentities()
252
- const entry = store.identities[name]
253
- if (!entry) throw new Error(`Identity "${name}" not found`)
254
- entry.mode = mode
255
- await writeIdentities(store)
256
- }