@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.
- package/README.md +3 -2
- package/dist/astrale.js +2599 -2703
- package/dist/public/connect-core.js +2019 -3050
- package/dist/public/keys/index.js +1851 -2885
- package/dist/public/paths/index.js +1830 -2872
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/lib/instance.d.ts +10 -0
- package/package.json +8 -8
- package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
- package/src/commands/__tests__/install-identity-override.test.ts +14 -3
- package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
- package/src/commands/__tests__/instance-use.test.ts +67 -0
- package/src/commands/__tests__/view-build.test.ts +58 -0
- package/src/commands/domain/install.ts +6 -5
- package/src/commands/domain/uninstall.ts +128 -0
- package/src/commands/instance/active.ts +13 -1
- package/src/commands/instance/bookmark.ts +26 -3
- package/src/commands/instance/list.ts +18 -4
- package/src/commands/instance/use.ts +54 -7
- package/src/commands/view.ts +28 -16
- package/src/connection/.spec/architecture.md +5 -0
- package/src/connection/.spec/laws/connection.ts +20 -0
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/auth.test.ts +27 -1
- package/src/connection/__tests__/ca-fetch.test.ts +8 -1
- package/src/connection/__tests__/errors.test.ts +410 -36
- package/src/connection/__tests__/exchange.test.ts +46 -5
- package/src/connection/__tests__/reasons.test.ts +78 -0
- package/src/connection/auth.ts +11 -9
- package/src/connection/command.ts +1 -1
- package/src/connection/errors.ts +139 -160
- package/src/connection/exchange.ts +14 -2
- package/src/connection/reasons.ts +179 -0
- package/src/lib/__tests__/instance.test.ts +51 -1
- package/src/lib/__tests__/view-assets.test.ts +33 -1
- package/src/lib/__tests__/view-server.test.ts +68 -0
- package/src/lib/ca-fetch.ts +9 -3
- package/src/lib/instance.ts +31 -0
- package/src/lib/view/assets.ts +16 -2
- package/src/program/__tests__/program.test.ts +2 -1
- package/src/program/build.ts +2 -1
- package/studio/package.json +7 -8
- package/viewer/dist/main.js +57 -57
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
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
|
+
})
|
package/src/lib/ca-fetch.ts
CHANGED
|
@@ -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
|
|
44
|
-
|
|
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
|
})
|
package/src/lib/instance.ts
CHANGED
|
@@ -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)
|
package/src/lib/view/assets.ts
CHANGED
|
@@ -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
|
-
'
|
|
190
|
+
'73c4dc12159039257a1847701b16ef963f5c96a188f3656604dd2770e74c116e',
|
|
190
191
|
)
|
|
191
192
|
})
|
|
192
193
|
|
package/src/program/build.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/studio/package.json
CHANGED
|
@@ -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.
|
|
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.
|
|
64
|
-
"@astrale-os/kernel-core": "file:../vendor/kernel/astrale-os-kernel-core-0.9.0-beta.
|
|
65
|
-
"@astrale-os/kernel-dsl": "file:../vendor/kernel/astrale-os-kernel-dsl-0.2.0-beta.
|
|
66
|
-
"@astrale-os/kernel-
|
|
67
|
-
"@astrale-os/kernel-
|
|
68
|
-
"@astrale-os/
|
|
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
|
}
|