@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,272 +1,50 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { SignJWT } from 'jose'
3
- import { webcrypto } from 'node:crypto'
4
2
 
5
- import {
6
- containsSelfRef,
7
- expandSelfReferences,
8
- resolveSelfNodeId,
9
- selfRefusalError,
10
- type SelfResolverContext,
11
- type SelfResolution,
12
- } from '../self'
13
-
14
- // ── Regex behavior ───────────────────────────────────────────
3
+ import { containsSelfRef, expandSelfReferences } from '../self'
15
4
 
16
5
  describe('containsSelfRef / expandSelfReferences', () => {
17
6
  const ID = 'node-abc-123'
18
7
 
19
- // Cases that MUST match — documented positions only.
20
8
  test.each([
21
9
  ['@self', `@${ID}`],
22
10
  ['@self::deployFunction', `@${ID}::deployFunction`],
23
- ['@self/functions', `@${ID}/functions`], // path navigation
11
+ ['@self/functions', `@${ID}/functions`],
24
12
  ['@self/functions/foo', `@${ID}/functions/foo`],
25
13
  ['node=@self', `node=@${ID}`],
26
14
  ['target=@self::method', `target=@${ID}::method`],
27
15
  ['target=@self/sub', `target=@${ID}/sub`],
28
- ['x=@self::m y=@self', `x=@${ID}::m y=@${ID}`], // double match
29
- ])('expands %p %p', (input, expected) => {
16
+ ['x=@self::m y=@self', `x=@${ID}::m y=@${ID}`],
17
+ ])('expands %p -> %p', (input, expected) => {
30
18
  expect(containsSelfRef(input)).toBe(true)
31
19
  expect(expandSelfReferences(input, ID)).toBe(expected)
32
20
  })
33
21
 
34
- // selfId carrying `String.replace` substitution metacharacters must not
35
- // be re-interpreted (function-form replacement guards this).
36
22
  test.each([
37
23
  ['@self', 'x$&y', '@x$&y'],
38
24
  ['@self', 'x$$y', '@x$$y'],
39
25
  ['@self', 'x$1y', '@x$1y'],
40
26
  ['@self', 'x$`y', '@x$`y'],
41
27
  ['@self::m', 'x$&y', '@x$&y::m'],
42
- ])('selfId with `$` metachars is treated literally: %p × %p %p', (input, id, expected) => {
28
+ ])('treats replacement metacharacters literally: %p x %p -> %p', (input, id, expected) => {
43
29
  expect(expandSelfReferences(input, id)).toBe(expected)
44
30
  })
45
31
 
46
- // Cases that MUST NOT match — substrings, JSON-like, comma-lists.
47
32
  test.each([
48
33
  'prefix@self',
49
34
  '@selfsuffix',
50
35
  '@self.example',
51
36
  '@self,@other',
52
37
  'node=before@self',
53
- '{"ref":"@self"}', // inside quoted JSON — lookbehind requires `=` or `^`
38
+ '{"ref":"@self"}',
54
39
  'no-self-here',
55
- ])('does NOT expand %p', (input) => {
40
+ ])('does not expand %p', (input) => {
56
41
  expect(containsSelfRef(input)).toBe(false)
57
42
  expect(expandSelfReferences(input, ID)).toBe(input)
58
43
  })
59
44
 
60
- test('containsSelfRef is stateless across calls (regex.lastIndex reset)', () => {
61
- // Both calls should return true even though the underlying global regex
62
- // tracks lastIndex internally. The implementation must reset it.
45
+ test('is stateless across calls', () => {
63
46
  expect(containsSelfRef('@self')).toBe(true)
64
47
  expect(containsSelfRef('@self')).toBe(true)
65
48
  expect(containsSelfRef('@self')).toBe(true)
66
49
  })
67
50
  })
68
-
69
- // ── Resolver — refusal paths ─────────────────────────────────
70
-
71
- describe('resolveSelfNodeId refusals', () => {
72
- test('bootstrap manager identity → manager', () => {
73
- const ctx: SelfResolverContext = {
74
- identity: { name: 'manager', subject: 'manager', createdAt: 'x' },
75
- instanceSlug: 'manager',
76
- instanceSigned: false,
77
- }
78
- const r = resolveSelfNodeId(ctx)
79
- expect(r).toEqual({ reason: 'manager' })
80
- })
81
-
82
- test('identity without registration on slug → no-registration', () => {
83
- const ctx: SelfResolverContext = {
84
- identity: { name: 'alice', subject: 'alice', createdAt: 'x' },
85
- instanceSlug: 'kernel-aidev',
86
- instanceSigned: false,
87
- }
88
- const r = resolveSelfNodeId(ctx)
89
- expect(r).toEqual({
90
- reason: 'no-registration',
91
- identityName: 'alice',
92
- instanceSlug: 'kernel-aidev',
93
- })
94
- })
95
-
96
- test('instance-signed path → instance-signed', () => {
97
- const ctx: SelfResolverContext = {
98
- identity: { name: 'manager', subject: 'manager', createdAt: 'x' },
99
- instanceSlug: 'child-with-keypair',
100
- instanceSigned: true,
101
- }
102
- const r = resolveSelfNodeId(ctx)
103
- expect(r).toEqual({ reason: 'instance-signed', instanceSlug: 'child-with-keypair' })
104
- })
105
-
106
- test('no slug (--url without -i) → url-no-slug', () => {
107
- const ctx: SelfResolverContext = {
108
- identity: { name: 'alice', subject: 'alice', createdAt: 'x' },
109
- instanceSlug: undefined,
110
- instanceSigned: false,
111
- }
112
- const r = resolveSelfNodeId(ctx)
113
- expect(r).toEqual({ reason: 'url-no-slug' })
114
- })
115
-
116
- test('--creds with malformed JWT → creds-no-sub', () => {
117
- const ctx: SelfResolverContext = {
118
- credsJwt: 'not-a-jwt',
119
- instanceSigned: false,
120
- }
121
- const r = resolveSelfNodeId(ctx)
122
- expect(r).toEqual({ reason: 'creds-no-sub' })
123
- })
124
-
125
- test('--creds with valid JWT but empty sub → creds-no-sub', async () => {
126
- const key = await generateEs256()
127
- const jwt = await new SignJWT({})
128
- .setProtectedHeader({ alg: 'ES256' })
129
- .setIssuer('test')
130
- .setAudience('test')
131
- .sign(key)
132
- const ctx: SelfResolverContext = { credsJwt: jwt, instanceSigned: false }
133
- const r = resolveSelfNodeId(ctx)
134
- expect(r).toEqual({ reason: 'creds-no-sub' })
135
- })
136
-
137
- test('--creds with whitespace-only sub → creds-no-sub', async () => {
138
- const key = await generateEs256()
139
- const jwt = await new SignJWT({})
140
- .setProtectedHeader({ alg: 'ES256' })
141
- .setSubject(' ')
142
- .setIssuer('test')
143
- .setAudience('test')
144
- .sign(key)
145
- const ctx: SelfResolverContext = { credsJwt: jwt, instanceSigned: false }
146
- const r = resolveSelfNodeId(ctx)
147
- expect(r).toEqual({ reason: 'creds-no-sub' })
148
- })
149
- })
150
-
151
- // ── Resolver — happy paths ───────────────────────────────────
152
-
153
- describe('resolveSelfNodeId happy paths', () => {
154
- test('--as identity with registration → registration sub', () => {
155
- const ctx: SelfResolverContext = {
156
- identity: {
157
- name: 'alice',
158
- subject: 'alice',
159
- createdAt: 'x',
160
- registrations: {
161
- 'kernel-aidev': { iss: 'https://aidev/', sub: 'node-id-aidev', registeredAt: 'x' },
162
- },
163
- },
164
- instanceSlug: 'kernel-aidev',
165
- instanceSigned: false,
166
- }
167
- const r = resolveSelfNodeId(ctx)
168
- expect(r).toEqual({ id: 'node-id-aidev' })
169
- })
170
-
171
- test('default identity with registration → registration sub', () => {
172
- const ctx: SelfResolverContext = {
173
- identity: {
174
- name: 'computer',
175
- subject: 'kernel-node-id',
176
- createdAt: 'x',
177
- registrations: {
178
- kernel: { iss: 'https://k/', sub: 'kernel-node-id', registeredAt: 'x' },
179
- },
180
- },
181
- instanceSlug: 'kernel',
182
- instanceSigned: false,
183
- }
184
- const r: SelfResolution = resolveSelfNodeId(ctx)
185
- expect(r).toEqual({ id: 'kernel-node-id' })
186
- })
187
-
188
- test('--creds with valid sub → sub wins over identity', async () => {
189
- const key = await generateEs256()
190
- const jwt = await new SignJWT({ sub: 'creds-derived-id' })
191
- .setProtectedHeader({ alg: 'ES256' })
192
- .setIssuer('test')
193
- .setSubject('creds-derived-id')
194
- .setAudience('test')
195
- .sign(key)
196
- const ctx: SelfResolverContext = {
197
- identity: { name: 'alice', subject: 'alice', createdAt: 'x' },
198
- instanceSlug: 'kernel',
199
- credsJwt: jwt,
200
- instanceSigned: false,
201
- }
202
- const r = resolveSelfNodeId(ctx)
203
- expect(r).toEqual({ id: 'creds-derived-id' })
204
- })
205
-
206
- test('manager identity WITH registrations → uses registration sub (not refused)', () => {
207
- const ctx: SelfResolverContext = {
208
- identity: {
209
- name: 'manager',
210
- subject: 'manager',
211
- createdAt: 'x',
212
- registrations: {
213
- 'some-child': { iss: 'x', sub: 'mgr-node-id-on-child', registeredAt: 'x' },
214
- },
215
- },
216
- instanceSlug: 'some-child',
217
- instanceSigned: false,
218
- }
219
- const r = resolveSelfNodeId(ctx)
220
- expect(r).toEqual({ id: 'mgr-node-id-on-child' })
221
- })
222
- })
223
-
224
- // ── Error builder ────────────────────────────────────────────
225
-
226
- describe('selfRefusalError', () => {
227
- test('manager refusal carries metadata + actionable message', () => {
228
- const e = selfRefusalError({ reason: 'manager' })
229
- expect(e).toBeInstanceOf(Error)
230
- expect(e.name).toBe('SelfRefusalError')
231
- expect(e.message).toContain('bootstrap `manager` identity')
232
- expect(e.message).toContain('astrale identity create')
233
- expect((e as Error & { selfRefusal?: { reason: string } }).selfRefusal?.reason).toBe('manager')
234
- })
235
-
236
- test('no-registration message references the specific identity + slug', () => {
237
- const e = selfRefusalError({
238
- reason: 'no-registration',
239
- identityName: 'alice',
240
- instanceSlug: 'kernel-aidev',
241
- })
242
- expect(e.message).toContain('alice')
243
- expect(e.message).toContain('kernel-aidev')
244
- expect(e.message).toContain('astrale identity register alice -i kernel-aidev')
245
- })
246
-
247
- test('instance-signed message hints at --as', () => {
248
- const e = selfRefusalError({ reason: 'instance-signed', instanceSlug: 'child' })
249
- expect(e.message).toContain('--as')
250
- expect(e.message).toContain('child')
251
- })
252
-
253
- test('url-no-slug message hints at -i', () => {
254
- const e = selfRefusalError({ reason: 'url-no-slug' })
255
- expect(e.message).toContain('-i')
256
- })
257
-
258
- test('creds-no-sub message hints at literal @<nodeId>', () => {
259
- const e = selfRefusalError({ reason: 'creds-no-sub' })
260
- expect(e.message).toContain('@<nodeId>')
261
- })
262
- })
263
-
264
- // ── Helpers ──────────────────────────────────────────────────
265
-
266
- async function generateEs256(): Promise<CryptoKey> {
267
- const pair = await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [
268
- 'sign',
269
- 'verify',
270
- ])
271
- return pair.privateKey
272
- }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
 
3
- import type { IdentityStore } from '../identity'
3
+ import type { IdentityStore } from '../../identity/index'
4
4
  import type { InstanceStore } from '../instance'
5
5
 
6
6
  import { resolveUseTarget } from '../use-target'
@@ -1,26 +1,54 @@
1
1
  import type { IntentMessage, MountedWindow, ResolvedView, Shell } from '@astrale-os/shell'
2
2
 
3
+ import { NO_HOST_CAPABILITIES } from '@astrale-os/shell'
3
4
  import { describe, expect, test } from 'bun:test'
4
5
 
5
6
  import {
6
7
  handleOpenIntent,
7
8
  installOpenIntentHandler,
8
- mountWithHandshakeFallback,
9
9
  type OpenIntentHost,
10
10
  } from '../view/open-intent'
11
11
 
12
+ const digest = (character: string) => `sha256:${character.repeat(64)}` as const
13
+ const target = (value: string) => value as ResolvedView['target']
14
+ const issuer = (value: string) => value as ResolvedView['route']['issuer']
15
+ const revision = (character: string) => digest(character) as ResolvedView['route']['revision']
16
+
12
17
  const profile: ResolvedView = {
13
- id: 'profile-view',
14
- path: '/shell/views/profile',
15
- url: 'https://shell.test/profile',
16
- origin: 'class',
18
+ target: target('@person-1'),
19
+ route: {
20
+ key: 'shell.test:view.profile',
21
+ declaration: {
22
+ target: {
23
+ kind: 'definition',
24
+ definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
25
+ },
26
+ auth: 'required',
27
+ },
28
+ href: 'https://shell.test/profile',
29
+ handshake: 'shell',
30
+ issuer: issuer('https://shell.test'),
31
+ etag: digest('a'),
32
+ revision: revision('b'),
33
+ },
17
34
  }
18
35
  const card: ResolvedView = {
19
- id: 'card-view',
20
- path: '/shell/views/card',
21
- url: 'https://shell.test/card',
22
- handshake: 'none',
23
- origin: 'class',
36
+ target: target('@person-1'),
37
+ route: {
38
+ key: 'shell.test:view.card',
39
+ declaration: {
40
+ target: {
41
+ kind: 'definition',
42
+ definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
43
+ },
44
+ auth: 'public',
45
+ },
46
+ href: 'https://shell.test/card',
47
+ handshake: 'none',
48
+ issuer: issuer('https://shell.test'),
49
+ etag: digest('c'),
50
+ revision: revision('d'),
51
+ },
24
52
  }
25
53
 
26
54
  function openMessage(viewId?: string, correlationId?: string): IntentMessage<'open'> {
@@ -41,13 +69,21 @@ function mounted(windowId: string, onClose?: () => void): MountedWindow {
41
69
  windowId,
42
70
  window: {
43
71
  windowId,
44
- functionId: 'view',
72
+ functionId: 'shell.test:view.profile',
73
+ targetNodeId: '@person-1',
74
+ children: [],
75
+ view: profile,
76
+ location: { target: '@person-1', params: {} },
77
+ presentation: { kind: 'inline', constrained: false },
45
78
  isolation: 'shared',
46
- state: 'active',
47
- delegationToken: null,
48
- capabilities: { intents: [] },
79
+ state: 'ready',
80
+ credential: { state: 'none' },
81
+ capabilities: NO_HOST_CAPABILITIES,
49
82
  },
50
83
  handle: { element: {} as HTMLElement },
84
+ credential: { state: 'none' },
85
+ presentation: { kind: 'inline', constrained: false },
86
+ focus() {},
51
87
  close: async () => {
52
88
  onClose?.()
53
89
  return { kind: 'closed' }
@@ -74,11 +110,11 @@ function harness(views: readonly ResolvedView[] = [profile, card]) {
74
110
  events.push(`current:${next.windowId}`)
75
111
  current = next
76
112
  },
77
- mount: async (view, nodeId) => {
78
- events.push(`mount:${view.id}:${nodeId}`)
113
+ mount: async (view) => {
114
+ events.push(`mount:${view.route.key}:${view.target}`)
79
115
  return mounted('new-window')
80
116
  },
81
- opened: (view, nodeId) => events.push(`opened:${view.path}:${nodeId}`),
117
+ opened: (view) => events.push(`opened:${view.route.key}:${view.target}`),
82
118
  failed: (error) => events.push(`failed:${error instanceof Error ? error.message : error}`),
83
119
  reply: (message, windowId) => {
84
120
  if (!message.envelope.correlationId) return
@@ -123,40 +159,35 @@ function harness(views: readonly ResolvedView[] = [profile, card]) {
123
159
  }
124
160
 
125
161
  describe('open intent host', () => {
126
- test('uses kernel preference and replies before retiring the requesting child', async () => {
162
+ /** @evidence TEST-CLI-VIEW-OPEN-PRESERVES-RESOLVED-SELECTION */
163
+ test('uses the complete resolved placement and replies before retiring the requester', async () => {
127
164
  const h = harness()
128
165
  await handleOpenIntent(h.shell, h.host, openMessage(undefined, 'corr-1'))
129
166
 
130
167
  expect(h.events).toEqual([
131
168
  'resolve:person-1',
132
- 'mount:profile-view:person-1',
169
+ 'mount:shell.test:view.profile:@person-1',
133
170
  'current:new-window',
134
- 'opened:/shell/views/profile:person-1',
171
+ 'opened:shell.test:view.profile:@person-1',
135
172
  'reply:old-window',
136
173
  'close:old-window',
137
174
  ])
138
175
  expect(h.current()?.windowId).toBe('new-window')
139
- expect(h.sent).toEqual([
140
- {
141
- windowId: 'old-window',
142
- message: {
143
- type: 'intent',
144
- version: 1,
145
- envelope: {
146
- name: 'intentReply',
147
- payload: { correlationId: 'corr-1', result: { windowId: 'new-window' } },
148
- sender: { windowId: 'root' },
149
- },
176
+ expect(h.sent[0]).toMatchObject({
177
+ windowId: 'old-window',
178
+ message: {
179
+ envelope: {
180
+ payload: { correlationId: 'corr-1', result: { windowId: 'new-window' } },
150
181
  },
151
182
  },
152
- ])
183
+ })
153
184
  })
154
185
 
155
- test('honors an explicit view and keeps fire-and-forget navigation reply-free', async () => {
186
+ test('selects an explicit canonical View key without flattening its placement', async () => {
156
187
  const h = harness()
157
- await handleOpenIntent(h.shell, h.host, openMessage('card-view'))
188
+ await handleOpenIntent(h.shell, h.host, openMessage('shell.test:view.card'))
158
189
 
159
- expect(h.events).toContain('mount:card-view:person-1')
190
+ expect(h.events).toContain('mount:shell.test:view.card:@person-1')
160
191
  expect(h.sent).toEqual([])
161
192
  })
162
193
 
@@ -170,36 +201,28 @@ describe('open intent host', () => {
170
201
  'reply:old-window',
171
202
  'failed:No view resolves for this node',
172
203
  ])
173
- expect(h.sent[0]!.message.envelope).toEqual({
174
- name: 'intentReply',
175
- payload: {
176
- correlationId: 'corr-fail',
177
- error: { message: 'No view resolves for this node' },
178
- },
179
- sender: { windowId: 'root' },
180
- })
181
204
  })
182
205
 
183
- test('keeps the current view when the replacement mount fails', async () => {
206
+ /** @evidence TEST-CLI-VIEW-HANDSHAKE-FAILS-CLOSED */
207
+ test('makes one replacement mount attempt and never retries as a different placement mode', async () => {
184
208
  const h = harness([profile])
185
- h.host.mount = async () => {
186
- h.events.push('mount:failed')
187
- throw new Error('replacement failed')
209
+ let attempts = 0
210
+ h.host.mount = async (view) => {
211
+ attempts++
212
+ h.events.push(`mount:${view.route.handshake}:failed`)
213
+ throw new Error('handshake failed')
188
214
  }
189
215
 
190
216
  await handleOpenIntent(h.shell, h.host, openMessage(undefined, 'corr-mount'))
191
217
 
218
+ expect(attempts).toBe(1)
192
219
  expect(h.current()?.windowId).toBe('old-window')
193
220
  expect(h.events).toEqual([
194
221
  'resolve:person-1',
195
- 'mount:failed',
222
+ 'mount:shell:failed',
196
223
  'reply:old-window',
197
- 'failed:replacement failed',
224
+ 'failed:handshake failed',
198
225
  ])
199
- expect(h.sent[0]!.message.envelope.payload).toEqual({
200
- correlationId: 'corr-mount',
201
- error: { message: 'replacement failed' },
202
- })
203
226
  })
204
227
 
205
228
  test('serializes overlapping opens', async () => {
@@ -246,63 +269,14 @@ describe('open intent host', () => {
246
269
  'resolve:1',
247
270
  'mount:new-1',
248
271
  'current:new-1',
249
- 'opened:/shell/views/profile:person-1',
272
+ 'opened:shell.test:view.profile:@person-1',
250
273
  'close:old-window',
251
274
  'resolve:2',
252
275
  'mount:new-2',
253
276
  'current:new-2',
254
- 'opened:/shell/views/profile:person-1',
277
+ 'opened:shell.test:view.profile:@person-1',
255
278
  'close:new-1',
256
279
  ])
257
280
  expect(base.current()?.windowId).toBe('new-2')
258
281
  })
259
282
  })
260
-
261
- describe('mountWithHandshakeFallback', () => {
262
- test('mounts a plain view once', async () => {
263
- const attempts: string[] = []
264
- const result = await mountWithHandshakeFallback({
265
- handshake: 'none',
266
- attempts: 2,
267
- mount: async (handshake) => {
268
- attempts.push(handshake)
269
- return 'plain-window'
270
- },
271
- cleanupFailedAttempt: () => attempts.push('cleanup'),
272
- })
273
- expect(result).toEqual({ mounted: 'plain-window', handshake: 'none' })
274
- expect(attempts).toEqual(['none'])
275
- })
276
-
277
- test('retries shell mounts, cleans each failure, then falls back to plain', async () => {
278
- const attempts: string[] = []
279
- const result = await mountWithHandshakeFallback({
280
- handshake: 'shell',
281
- attempts: 2,
282
- mount: async (handshake) => {
283
- attempts.push(handshake)
284
- if (handshake === 'shell') throw new Error('handshake failed')
285
- return 'plain-window'
286
- },
287
- cleanupFailedAttempt: () => attempts.push('cleanup'),
288
- })
289
- expect(result).toEqual({ mounted: 'plain-window', handshake: 'none' })
290
- expect(attempts).toEqual(['shell', 'cleanup', 'shell', 'cleanup', 'none'])
291
- })
292
-
293
- test('cleans and surfaces the decisive plain fallback failure', async () => {
294
- const attempts: string[] = []
295
- const run = mountWithHandshakeFallback({
296
- handshake: 'shell',
297
- attempts: 2,
298
- mount: async (handshake) => {
299
- attempts.push(handshake)
300
- throw new Error(handshake === 'shell' ? 'handshake failed' : 'plain failed')
301
- },
302
- cleanupFailedAttempt: () => attempts.push('cleanup'),
303
- })
304
-
305
- await expect(run).rejects.toThrow('plain failed')
306
- expect(attempts).toEqual(['shell', 'cleanup', 'shell', 'cleanup', 'none', 'cleanup'])
307
- })
308
- })
@@ -0,0 +1,99 @@
1
+ import { afterEach, describe, expect, test } from 'bun:test'
2
+ import { closeSync } from 'node:fs'
3
+ import { chmod, mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import type { ViewServeConfig } from '../view/session'
8
+
9
+ import {
10
+ configPath,
11
+ logPath,
12
+ openSessionLog,
13
+ recordPath,
14
+ removeSessionFiles,
15
+ saveRecord,
16
+ saveServeConfig,
17
+ } from '../view/session'
18
+
19
+ const temporaryDirectories: string[] = []
20
+
21
+ afterEach(async () => {
22
+ await Promise.all(
23
+ temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
24
+ )
25
+ })
26
+
27
+ function mode(value: Awaited<ReturnType<typeof stat>>): number {
28
+ return Number(value.mode) & 0o777
29
+ }
30
+
31
+ const target = (value: string) => value as ViewServeConfig['session']['view']['target']
32
+ const issuer = (value: string) => value as ViewServeConfig['session']['view']['route']['issuer']
33
+ const revision = (character: string) =>
34
+ `sha256:${character.repeat(64)}` as ViewServeConfig['session']['view']['route']['revision']
35
+
36
+ describe('view session private state', () => {
37
+ /** @evidence TEST-CLI-VIEW-CREDENTIAL-CONFIG-IS-OWNER-ONLY */
38
+ test('repairs directory/log modes and atomically stores raw carriers as 0600', async () => {
39
+ const root = await mkdtemp(join(tmpdir(), 'astrale-view-state-'))
40
+ temporaryDirectories.push(root)
41
+ const directory = join(root, 'view')
42
+ await mkdir(directory, { mode: 0o755 })
43
+ await chmod(directory, 0o755)
44
+
45
+ const config = {
46
+ session: {
47
+ id: 'v-a1b2c3',
48
+ pid: 0,
49
+ port: 4419,
50
+ nonce: 'nonce',
51
+ pageUrl: 'http://127.0.0.1:4419/s/nonce/',
52
+ view: {
53
+ target: target('/:example.test'),
54
+ route: {
55
+ key: 'example.test:view.main',
56
+ href: 'https://example.test/ui',
57
+ handshake: 'shell',
58
+ issuer: issuer('https://example.test'),
59
+ etag: `sha256:${'a'.repeat(64)}`,
60
+ revision: revision('b'),
61
+ declaration: { target: { kind: 'domain' }, auth: 'required' },
62
+ },
63
+ },
64
+ createdAt: '2026-08-12T00:00:00.000Z',
65
+ },
66
+ kernel: { creds: 'raw-secret-carrier' },
67
+ proxy: {
68
+ kernelUrl: 'https://kernel.test',
69
+ issuer: 'https://kernel.test',
70
+ direct: true,
71
+ },
72
+ idleMs: 60_000,
73
+ } satisfies ViewServeConfig
74
+
75
+ await saveServeConfig(config, directory)
76
+ await saveRecord(config.session, directory)
77
+ const descriptor = await openSessionLog(config.session.id, directory)
78
+ closeSync(descriptor)
79
+
80
+ expect(mode(await stat(directory))).toBe(0o700)
81
+ expect(mode(await stat(configPath(config.session.id, directory)))).toBe(0o600)
82
+ expect(mode(await stat(recordPath(config.session.id, directory)))).toBe(0o600)
83
+ expect(mode(await stat(logPath(config.session.id, directory)))).toBe(0o600)
84
+ expect(await readFile(configPath(config.session.id, directory), 'utf8')).toContain(
85
+ 'raw-secret-carrier',
86
+ )
87
+
88
+ await removeSessionFiles(config.session.id, directory)
89
+ await expect(stat(configPath(config.session.id, directory))).rejects.toMatchObject({
90
+ code: 'ENOENT',
91
+ })
92
+ await expect(stat(recordPath(config.session.id, directory))).rejects.toMatchObject({
93
+ code: 'ENOENT',
94
+ })
95
+ await expect(stat(logPath(config.session.id, directory))).rejects.toMatchObject({
96
+ code: 'ENOENT',
97
+ })
98
+ })
99
+ })