@astrale-os/cli 0.8.1-alpha.6 → 0.8.1-alpha.7

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 (44) hide show
  1. package/README.md +3 -2
  2. package/dist/astrale.js +2599 -2703
  3. package/dist/public/connect-core.js +2019 -3050
  4. package/dist/public/keys/index.js +1851 -2885
  5. package/dist/public/paths/index.js +1830 -2872
  6. package/dist/types/connection/auth.d.ts +3 -0
  7. package/dist/types/lib/instance.d.ts +10 -0
  8. package/package.json +8 -8
  9. package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
  10. package/src/commands/__tests__/install-identity-override.test.ts +14 -3
  11. package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
  12. package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
  13. package/src/commands/__tests__/instance-use.test.ts +67 -0
  14. package/src/commands/__tests__/view-build.test.ts +58 -0
  15. package/src/commands/domain/install.ts +6 -5
  16. package/src/commands/domain/uninstall.ts +128 -0
  17. package/src/commands/instance/active.ts +13 -1
  18. package/src/commands/instance/bookmark.ts +26 -3
  19. package/src/commands/instance/list.ts +18 -4
  20. package/src/commands/instance/use.ts +54 -7
  21. package/src/commands/view.ts +28 -16
  22. package/src/connection/.spec/architecture.md +5 -0
  23. package/src/connection/.spec/laws/connection.ts +20 -0
  24. package/src/connection/.spec/layout.ts +1 -0
  25. package/src/connection/__tests__/auth.test.ts +27 -1
  26. package/src/connection/__tests__/ca-fetch.test.ts +8 -1
  27. package/src/connection/__tests__/errors.test.ts +410 -36
  28. package/src/connection/__tests__/exchange.test.ts +46 -5
  29. package/src/connection/__tests__/reasons.test.ts +78 -0
  30. package/src/connection/auth.ts +11 -9
  31. package/src/connection/command.ts +1 -1
  32. package/src/connection/errors.ts +139 -160
  33. package/src/connection/exchange.ts +14 -2
  34. package/src/connection/reasons.ts +179 -0
  35. package/src/lib/__tests__/instance.test.ts +51 -1
  36. package/src/lib/__tests__/view-assets.test.ts +33 -1
  37. package/src/lib/__tests__/view-server.test.ts +68 -0
  38. package/src/lib/ca-fetch.ts +9 -3
  39. package/src/lib/instance.ts +31 -0
  40. package/src/lib/view/assets.ts +16 -2
  41. package/src/program/__tests__/program.test.ts +2 -1
  42. package/src/program/build.ts +2 -1
  43. package/studio/package.json +7 -8
  44. package/viewer/dist/main.js +57 -57
@@ -1,5 +1,14 @@
1
1
  import { afterEach, describe, expect, test } from 'bun:test'
2
- import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import {
3
+ mkdtemp,
4
+ mkdir,
5
+ readFile,
6
+ realpath,
7
+ rm,
8
+ symlink,
9
+ utimes,
10
+ writeFile,
11
+ } from 'node:fs/promises'
3
12
  import { tmpdir } from 'node:os'
4
13
  import { dirname, join, relative } from 'node:path'
5
14
  import { pathToFileURL } from 'node:url'
@@ -61,6 +70,29 @@ describe('viewer asset resolution', () => {
61
70
  expect(await readFile(join(dist, 'main.js'), 'utf8')).toContain('viewer ready')
62
71
  })
63
72
 
73
+ test('rebuilds stale viewer assets in a source checkout', async () => {
74
+ const root = await mkdtemp(join(tmpdir(), 'astrale-view-stale-'))
75
+ temporaryDirectories.push(root)
76
+ const module = join(root, 'src', 'lib', 'view', 'assets.ts')
77
+ const source = join(root, 'viewer')
78
+ const dist = join(source, 'dist')
79
+
80
+ await mkdir(dirname(module), { recursive: true })
81
+ await mkdir(dist, { recursive: true })
82
+ await writeFile(module, '')
83
+ await writeFile(join(source, 'main.ts'), 'document.body.textContent = "fresh viewer"\n')
84
+ await writeFile(join(source, 'index.html'), '<!doctype html><body>fresh</body>\n')
85
+ await writeFile(join(dist, 'main.js'), 'document.body.textContent = "stale viewer"\n')
86
+ await writeFile(join(dist, 'index.html'), '<!doctype html><body>stale</body>\n')
87
+ const future = new Date(Date.now() + 2_000)
88
+ await utimes(join(source, 'main.ts'), future, future)
89
+
90
+ await ensureViewerAssets(pathToFileURL(module).href, join(root, 'bin', 'astrale'))
91
+
92
+ expect(await readFile(join(dist, 'main.js'), 'utf8')).toContain('fresh viewer')
93
+ expect(await readFile(join(dist, 'index.html'), 'utf8')).toContain('fresh')
94
+ })
95
+
64
96
  test('uses the bundled module location when invoked through a global bin symlink', async () => {
65
97
  const root = await mkdtemp(join(tmpdir(), 'astrale-view-bundle-'))
66
98
  temporaryDirectories.push(root)
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { once } from 'node:events'
3
+
4
+ import type { ViewServeConfig } from '../view/session'
5
+
6
+ import { findFreePort } from '../port'
7
+ import { startViewServer } from '../view/server'
8
+
9
+ const digest = (character: string) => `sha256:${character.repeat(64)}` as const
10
+ const target = (value: string) => value as ViewServeConfig['session']['view']['target']
11
+ const issuer = (value: string) => value as ViewServeConfig['session']['view']['route']['issuer']
12
+ const revision = (character: string) =>
13
+ digest(character) as ViewServeConfig['session']['view']['route']['revision']
14
+
15
+ describe('view session server credentials', () => {
16
+ /** @evidence TEST-CLI-PLAIN-VIEW-RECEIVES-NO-CREDENTIAL */
17
+ test('refuses to mint a token for a handshake-none View', async () => {
18
+ const nonce = 'plain-view'
19
+ const port = await findFreePort(48_000, 200)
20
+ if (port === null) throw new Error('test port window exhausted')
21
+ const config = {
22
+ session: {
23
+ id: 'v-plain',
24
+ pid: 0,
25
+ port,
26
+ nonce,
27
+ pageUrl: `http://127.0.0.1:${port}/`,
28
+ view: {
29
+ target: target('/:example.test'),
30
+ route: {
31
+ key: 'example.test:view.public',
32
+ declaration: { target: { kind: 'domain' }, auth: 'required' },
33
+ href: 'https://example.test/ui/public',
34
+ handshake: 'none',
35
+ issuer: issuer('https://example.test'),
36
+ etag: digest('a'),
37
+ revision: revision('b'),
38
+ },
39
+ },
40
+ createdAt: '2026-08-20T00:00:00.000Z',
41
+ },
42
+ kernel: { creds: 'must-not-be-used' },
43
+ proxy: {
44
+ kernelUrl: 'https://kernel.test',
45
+ issuer: 'https://kernel.test',
46
+ direct: true,
47
+ },
48
+ idleMs: 60_000,
49
+ } satisfies ViewServeConfig
50
+ const server = startViewServer(config)
51
+ await once(server, 'listening')
52
+
53
+ try {
54
+ const response = await fetch(`http://127.0.0.1:${port}/s/${nonce}/token`, {
55
+ method: 'POST',
56
+ })
57
+
58
+ expect(response.status).toBe(403)
59
+ expect(await response.json()).toEqual({
60
+ error: 'plain views have no Astrale credential privilege',
61
+ })
62
+ } finally {
63
+ await new Promise<void>((resolve, reject) => {
64
+ server.close((error) => (error ? reject(error) : resolve()))
65
+ })
66
+ }
67
+ })
68
+ })
@@ -3,6 +3,7 @@ import type { IncomingHttpHeaders, request as httpRequest } from 'node:http'
3
3
  import { Buffer } from 'node:buffer'
4
4
  import { readFileSync } from 'node:fs'
5
5
  import { request as httpsRequest } from 'node:https'
6
+ import { rootCertificates } from 'node:tls'
6
7
 
7
8
  /** Create a Fetch capability whose HTTPS requests trust one CLI-selected CA file. */
8
9
  export function fetchWithCaFile(
@@ -32,7 +33,9 @@ function fetchWithNode(url: URL, init: RequestInit | undefined, ca: Buffer): Pro
32
33
  {
33
34
  method: init?.method ?? 'GET',
34
35
  headers: headersInitToRecord(init?.headers),
35
- ca,
36
+ // `ca` replaces Node's default trust set. Retain public roots because one
37
+ // Client Session may reach both a private Kernel and a public Domain issuer.
38
+ ca: [...rootCertificates, ca],
36
39
  },
37
40
  (response) => {
38
41
  const chunks: Buffer[] = []
@@ -40,8 +43,11 @@ function fetchWithNode(url: URL, init: RequestInit | undefined, ca: Buffer): Pro
40
43
  chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)),
41
44
  )
42
45
  response.on('end', () => {
43
- const result = new Response(Buffer.concat(chunks), {
44
- status: response.statusCode ?? 0,
46
+ const status = response.statusCode ?? 0
47
+ const body =
48
+ status === 204 || status === 205 || status === 304 ? null : Buffer.concat(chunks)
49
+ const result = new Response(body, {
50
+ status,
45
51
  statusText: response.statusMessage,
46
52
  headers: responseHeaders(response.headers),
47
53
  })
@@ -68,6 +68,11 @@ export type ResolvedInstance = {
68
68
  status?: string
69
69
  }
70
70
 
71
+ export type BookmarkTrustConflict = {
72
+ readonly name: string
73
+ readonly caFile: string | null
74
+ }
75
+
71
76
  function seed(): InstanceStore {
72
77
  return { active: '', instances: {} }
73
78
  }
@@ -198,6 +203,32 @@ export function resolveInstanceKey(store: InstanceStore, identifier: string): st
198
203
  return null
199
204
  }
200
205
 
206
+ /**
207
+ * Find other bookmarks for the same normalized Kernel URL whose TLS trust
208
+ * configuration differs. System trust (`undefined`) is a configuration too:
209
+ * mixing it with a custom CA is exactly as significant as mixing two CA files.
210
+ */
211
+ export function findBookmarkTrustConflicts(
212
+ store: InstanceStore,
213
+ name: string,
214
+ url: string,
215
+ caFile?: string,
216
+ ): BookmarkTrustConflict[] {
217
+ const normalizedUrl = normalizeInstanceKernelUrl(url)
218
+ const configuredCa = caFile ?? null
219
+ return Object.entries(store.instances).flatMap(([candidateName, entry]) => {
220
+ if (
221
+ candidateName === name ||
222
+ entry.url === undefined ||
223
+ normalizeInstanceKernelUrl(entry.url) !== normalizedUrl ||
224
+ (entry.caFile ?? null) === configuredCa
225
+ ) {
226
+ return []
227
+ }
228
+ return [{ name: candidateName, caFile: entry.caFile ?? null }]
229
+ })
230
+ }
231
+
201
232
  export async function addInstance(key: string, opts: AddInstanceOpts = {}): Promise<InstanceEntry> {
202
233
  validateName(key, 'Instance')
203
234
  if (RESERVED_SLUGS.has(key)) throw new ReservedSlugError(key)
@@ -1,4 +1,4 @@
1
- import { existsSync } from 'node:fs'
1
+ import { existsSync, statSync } from 'node:fs'
2
2
  import { copyFile } from 'node:fs/promises'
3
3
  import { dirname, join } from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
@@ -34,8 +34,8 @@ export async function ensureViewerAssets(
34
34
  entry = process.argv[1] ?? '.',
35
35
  ): Promise<string> {
36
36
  const dist = viewerDistDir(moduleUrl, entry)
37
- if (hasViewerBundle(dist)) return dist
38
37
  const srcDir = join(dist, '..')
38
+ if (hasViewerBundle(dist) && !viewerSourceIsNewer(srcDir, dist)) return dist
39
39
  const bun = (
40
40
  globalThis as { Bun?: { build: (o: object) => Promise<{ success: boolean; logs: unknown[] }> } }
41
41
  ).Bun
@@ -62,3 +62,17 @@ function hasViewerBundle(directory: string): boolean {
62
62
  function hasViewerSource(directory: string): boolean {
63
63
  return existsSync(join(directory, 'main.ts')) && existsSync(join(directory, 'index.html'))
64
64
  }
65
+
66
+ function viewerSourceIsNewer(source: string, dist: string): boolean {
67
+ if (!hasViewerSource(source)) return false
68
+ if (!hasViewerBundle(dist)) return true
69
+ const newestSource = Math.max(
70
+ statSync(join(source, 'main.ts')).mtimeMs,
71
+ statSync(join(source, 'index.html')).mtimeMs,
72
+ )
73
+ const oldestOutput = Math.min(
74
+ statSync(join(dist, 'main.js')).mtimeMs,
75
+ statSync(join(dist, 'index.html')).mtimeMs,
76
+ )
77
+ return newestSource > oldestOutput
78
+ }
@@ -142,6 +142,7 @@ describe('program composition', () => {
142
142
  'domain install',
143
143
  'domain list',
144
144
  'domain publish',
145
+ 'domain uninstall',
145
146
  'get',
146
147
  'identity',
147
148
  'identity create',
@@ -186,7 +187,7 @@ describe('program composition', () => {
186
187
  'whoami',
187
188
  ])
188
189
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
189
- '0f4739269db4c1dc8e1ba8cdf7840d7fbbe9c809234d2e21eef143a9de856382',
190
+ '73c4dc12159039257a1847701b16ef963f5c96a188f3656604dd2770e74c116e',
190
191
  )
191
192
  })
192
193
 
@@ -80,11 +80,12 @@ export async function buildProgram(): Promise<Command> {
80
80
 
81
81
  registerGroup(program, {
82
82
  name: 'domain',
83
- description: 'List, publish, and install domains (admin catalog + per-instance install)',
83
+ description: 'List, publish, install, and uninstall domains',
84
84
  commands: [
85
85
  withKernelOptions((await import('../commands/domain/list')).default),
86
86
  withKernelOptions((await import('../commands/domain/publish')).default),
87
87
  withKernelOptions((await import('../commands/domain/install')).default),
88
+ withKernelOptions((await import('../commands/domain/uninstall')).default),
88
89
  ],
89
90
  })
90
91
 
@@ -19,7 +19,7 @@
19
19
  "typecheck": "tsgo --noEmit"
20
20
  },
21
21
  "dependencies": {
22
- "@astrale-os/shell": "file:../vendor/astrale-os-shell-0.3.8-beta.1.tgz",
22
+ "@astrale-os/shell": "file:../vendor/astrale-os-shell-0.3.8-beta.2.tgz",
23
23
  "@dagrejs/dagre": "^3.0.0",
24
24
  "@radix-ui/react-collapsible": "^1.1.0",
25
25
  "@radix-ui/react-dialog": "^1.1.6",
@@ -60,12 +60,11 @@
60
60
  "vite": "^7.0.0"
61
61
  },
62
62
  "overrides": {
63
- "@astrale-os/kernel-client": "file:../vendor/kernel/astrale-os-kernel-client-0.6.0-beta.1.tgz",
64
- "@astrale-os/kernel-core": "file:../vendor/kernel/astrale-os-kernel-core-0.9.0-beta.1.tgz",
65
- "@astrale-os/kernel-dsl": "file:../vendor/kernel/astrale-os-kernel-dsl-0.2.0-beta.1.tgz",
66
- "@astrale-os/kernel-ports": "file:../vendor/kernel/astrale-os-kernel-ports-0.6.0-beta.1.tgz",
67
- "@astrale-os/kernel-protocol": "file:../vendor/kernel/astrale-os-kernel-protocol-0.5.0-beta.1.tgz",
68
- "@astrale-os/kernel-server": "file:../vendor/kernel/astrale-os-kernel-server-0.5.0-beta.1.tgz",
69
- "@astrale-os/sdk": "file:../vendor/astrale-os-sdk-0.5.0-beta.1.tgz"
63
+ "@astrale-os/kernel-client": "file:../vendor/kernel/astrale-os-kernel-client-0.6.0-beta.3.tgz",
64
+ "@astrale-os/kernel-core": "file:../vendor/kernel/astrale-os-kernel-core-0.9.0-beta.2.tgz",
65
+ "@astrale-os/kernel-dsl": "file:../vendor/kernel/astrale-os-kernel-dsl-0.2.0-beta.2.tgz",
66
+ "@astrale-os/kernel-protocol": "file:../vendor/kernel/astrale-os-kernel-protocol-0.5.0-beta.2.tgz",
67
+ "@astrale-os/kernel-server": "file:../vendor/kernel/astrale-os-kernel-server-0.5.0-beta.3.tgz",
68
+ "@astrale-os/sdk": "file:../vendor/astrale-os-sdk-0.5.0-beta.3.tgz"
70
69
  }
71
70
  }