@astrale-os/cli 0.8.1-alpha.6 → 1.0.0-beta.0
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 +18 -5
- package/dist/astrale.js +2661 -2714
- 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 +9 -9
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- 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 +84 -20
- 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 +483 -33
- 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 +9 -1
- package/src/connection/errors.ts +149 -159
- package/src/connection/exchange.ts +14 -2
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +193 -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/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +8 -9
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/viewer/dist/main.js +57 -57
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- package/studio/server/view-dev-server.ts +0 -372
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export type FunctionInputIssue = Readonly<{
|
|
2
|
+
code: string
|
|
3
|
+
path?: string
|
|
4
|
+
message: string
|
|
5
|
+
}>
|
|
6
|
+
|
|
7
|
+
export type QueryInputRepair =
|
|
8
|
+
| Readonly<{ phase: 'decode' | 'input'; path: string }>
|
|
9
|
+
| Readonly<{ phase: 'plan'; issue: string; path?: string }>
|
|
10
|
+
| Readonly<{
|
|
11
|
+
phase: 'limit'
|
|
12
|
+
limit: string
|
|
13
|
+
maximum: number
|
|
14
|
+
actual: number
|
|
15
|
+
path?: string
|
|
16
|
+
}>
|
|
17
|
+
|
|
18
|
+
export type SchemaUpgradeDetails =
|
|
19
|
+
| {
|
|
20
|
+
readonly origin?: string
|
|
21
|
+
readonly issue?: undefined
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
readonly origin: string
|
|
25
|
+
readonly issue: 'issuer-changed'
|
|
26
|
+
readonly installedIssuer: string
|
|
27
|
+
readonly replacementIssuer: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const JSON_POINTER = /^(?:\/(?:[^~/]|~[01])*)*$/u
|
|
31
|
+
const MAXIMUM_FUNCTION_ISSUES = 32
|
|
32
|
+
const MAXIMUM_FUNCTION_ISSUE_MESSAGE_LENGTH = 512
|
|
33
|
+
|
|
34
|
+
export function reasonCode(reason: unknown): string | undefined {
|
|
35
|
+
if (!record(reason) || typeof reason.code !== 'string') return undefined
|
|
36
|
+
return stableCode(reason.code) ? reason.code : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Admit only bounded caller-safe Function input issues established by the Kernel. */
|
|
40
|
+
export function functionInputIssues(reason: unknown): readonly FunctionInputIssue[] {
|
|
41
|
+
if (!reasonWithCode(reason, 'FUNCTION_INPUT_INVALID')) return Object.freeze([])
|
|
42
|
+
const issues = reason.details.issues
|
|
43
|
+
if (!Array.isArray(issues)) return Object.freeze([])
|
|
44
|
+
return Object.freeze(
|
|
45
|
+
issues.slice(0, MAXIMUM_FUNCTION_ISSUES).flatMap((candidate): FunctionInputIssue[] => {
|
|
46
|
+
if (
|
|
47
|
+
!record(candidate) ||
|
|
48
|
+
typeof candidate.code !== 'string' ||
|
|
49
|
+
!stableCode(candidate.code) ||
|
|
50
|
+
typeof candidate.message !== 'string' ||
|
|
51
|
+
candidate.message.length === 0 ||
|
|
52
|
+
candidate.message.length > MAXIMUM_FUNCTION_ISSUE_MESSAGE_LENGTH ||
|
|
53
|
+
candidate.message.normalize('NFC') !== candidate.message ||
|
|
54
|
+
containsControl(candidate.message) ||
|
|
55
|
+
(candidate.path !== undefined && !coordinate(candidate.path))
|
|
56
|
+
) {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
return [
|
|
60
|
+
Object.freeze({
|
|
61
|
+
code: candidate.code,
|
|
62
|
+
...(candidate.path === undefined ? {} : { path: candidate.path }),
|
|
63
|
+
message: candidate.message,
|
|
64
|
+
}),
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Admit only public Query-input repair variants; unknown details remain machine-only. */
|
|
71
|
+
export function queryInputRepair(reason: unknown): QueryInputRepair | undefined {
|
|
72
|
+
if (!reasonWithCode(reason, 'QUERY_INPUT_INVALID')) return undefined
|
|
73
|
+
const details = reason.details
|
|
74
|
+
if (details.phase === 'decode' || details.phase === 'input') {
|
|
75
|
+
if (!exact(details, ['phase', 'path']) || !coordinate(details.path)) return undefined
|
|
76
|
+
return Object.freeze({ phase: details.phase, path: details.path })
|
|
77
|
+
}
|
|
78
|
+
if (details.phase === 'plan') {
|
|
79
|
+
const fields = details.path === undefined ? ['phase', 'issue'] : ['phase', 'issue', 'path']
|
|
80
|
+
if (
|
|
81
|
+
!exact(details, fields) ||
|
|
82
|
+
typeof details.issue !== 'string' ||
|
|
83
|
+
!stableCode(details.issue) ||
|
|
84
|
+
(details.path !== undefined && !coordinate(details.path))
|
|
85
|
+
) {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
phase: 'plan',
|
|
90
|
+
issue: details.issue,
|
|
91
|
+
...(details.path === undefined ? {} : { path: details.path }),
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
if (details.phase !== 'limit') return undefined
|
|
95
|
+
const fields =
|
|
96
|
+
details.path === undefined
|
|
97
|
+
? ['phase', 'limit', 'maximum', 'actual']
|
|
98
|
+
: ['phase', 'limit', 'maximum', 'actual', 'path']
|
|
99
|
+
if (
|
|
100
|
+
!exact(details, fields) ||
|
|
101
|
+
typeof details.limit !== 'string' ||
|
|
102
|
+
!Number.isSafeInteger(details.maximum) ||
|
|
103
|
+
(details.maximum as number) < 0 ||
|
|
104
|
+
!Number.isSafeInteger(details.actual) ||
|
|
105
|
+
(details.actual as number) <= (details.maximum as number) ||
|
|
106
|
+
(details.path !== undefined && !coordinate(details.path))
|
|
107
|
+
) {
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
phase: 'limit',
|
|
112
|
+
limit: details.limit,
|
|
113
|
+
maximum: details.maximum as number,
|
|
114
|
+
actual: details.actual as number,
|
|
115
|
+
...(details.path === undefined ? {} : { path: details.path }),
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Decode bounded recovery guidance while preserving the admitted reason itself elsewhere. */
|
|
120
|
+
export function schemaUpgradeDetails(reason: unknown): SchemaUpgradeDetails | undefined {
|
|
121
|
+
if (!record(reason) || reason.code !== 'SCHEMA_UPGRADE_INCOMPATIBLE') return undefined
|
|
122
|
+
const details = record(reason.details) ? reason.details : {}
|
|
123
|
+
const origin = typeof details.origin === 'string' ? details.origin : undefined
|
|
124
|
+
if (
|
|
125
|
+
details.issue === 'issuer-changed' &&
|
|
126
|
+
origin !== undefined &&
|
|
127
|
+
typeof details.installedIssuer === 'string' &&
|
|
128
|
+
typeof details.replacementIssuer === 'string'
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
origin,
|
|
132
|
+
issue: details.issue,
|
|
133
|
+
installedIssuer: details.installedIssuer,
|
|
134
|
+
replacementIssuer: details.replacementIssuer,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return origin === undefined ? {} : { origin }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function schemaUpgradeHint(details: SchemaUpgradeDetails): string {
|
|
141
|
+
const target = details.origin ?? '<origin>'
|
|
142
|
+
const explanation =
|
|
143
|
+
details.issue === 'issuer-changed'
|
|
144
|
+
? 'A replacement cannot change an installed Domain issuer.'
|
|
145
|
+
: 'The replacement changes an immutable part of the installed Domain.'
|
|
146
|
+
return (
|
|
147
|
+
`${explanation} If this change is intentional, first run ` +
|
|
148
|
+
`\`astrale domain uninstall ${target}\`, then install it again. ` +
|
|
149
|
+
'The Kernel refuses uninstall while dependents or business data remain; uninstall never deletes business data.'
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function reasonWithCode(
|
|
154
|
+
input: unknown,
|
|
155
|
+
code: string,
|
|
156
|
+
): input is Readonly<{ code: string; details: Readonly<Record<string, unknown>> }> {
|
|
157
|
+
return (
|
|
158
|
+
record(input) &&
|
|
159
|
+
exact(input, ['code', 'details']) &&
|
|
160
|
+
input.code === code &&
|
|
161
|
+
record(input.details)
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function coordinate(input: unknown): input is string {
|
|
166
|
+
return (
|
|
167
|
+
typeof input === 'string' &&
|
|
168
|
+
input.length <= 1_024 &&
|
|
169
|
+
input.normalize('NFC') === input &&
|
|
170
|
+
JSON_POINTER.test(input)
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function stableCode(input: string): boolean {
|
|
175
|
+
return /^[A-Z][A-Z0-9_]{0,127}$/u.test(input) && input.normalize('NFC') === input
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function containsControl(input: string): boolean {
|
|
179
|
+
for (const character of input) {
|
|
180
|
+
const point = character.codePointAt(0)!
|
|
181
|
+
if (point <= 0x1f || point === 0x7f) return true
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function record(input: unknown): input is Readonly<Record<string, unknown>> {
|
|
187
|
+
return input !== null && typeof input === 'object' && !Array.isArray(input)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function exact(input: Readonly<Record<string, unknown>>, fields: readonly string[]): boolean {
|
|
191
|
+
const actual = Object.keys(input)
|
|
192
|
+
return actual.length === fields.length && actual.every((field) => fields.includes(field))
|
|
193
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
findBookmarkTrustConflicts,
|
|
5
|
+
InstanceStoreSchema,
|
|
6
|
+
normalizeInstanceKernelUrl,
|
|
7
|
+
sanitizeStore,
|
|
8
|
+
} from '../instance'
|
|
4
9
|
|
|
5
10
|
describe('InstanceStoreSchema', () => {
|
|
6
11
|
test('parses valid store with url', () => {
|
|
@@ -134,3 +139,48 @@ describe('sanitizeStore — read must not rewrite', () => {
|
|
|
134
139
|
expect(changed).toBe(false)
|
|
135
140
|
})
|
|
136
141
|
})
|
|
142
|
+
|
|
143
|
+
describe('bookmark TLS trust collisions', () => {
|
|
144
|
+
test('finds the same normalized URL with a different CA configuration', () => {
|
|
145
|
+
const store = InstanceStoreSchema.parse({
|
|
146
|
+
active: 'stable',
|
|
147
|
+
instances: {
|
|
148
|
+
stable: {
|
|
149
|
+
url: 'https://local.example/kernel/',
|
|
150
|
+
caFile: '/certs/stable.pem',
|
|
151
|
+
},
|
|
152
|
+
alias: {
|
|
153
|
+
url: 'https://local.example/kernel',
|
|
154
|
+
caFile: '/certs/old.pem',
|
|
155
|
+
},
|
|
156
|
+
other: {
|
|
157
|
+
url: 'https://other.example/kernel',
|
|
158
|
+
caFile: '/certs/old.pem',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
expect(
|
|
164
|
+
findBookmarkTrustConflicts(
|
|
165
|
+
store,
|
|
166
|
+
'stable',
|
|
167
|
+
'https://local.example/kernel',
|
|
168
|
+
'/certs/stable.pem',
|
|
169
|
+
),
|
|
170
|
+
).toEqual([{ name: 'alias', caFile: '/certs/old.pem' }])
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('treats custom CA versus system trust as a meaningful difference', () => {
|
|
174
|
+
const store = InstanceStoreSchema.parse({
|
|
175
|
+
active: 'custom',
|
|
176
|
+
instances: {
|
|
177
|
+
custom: { url: 'https://local.example', caFile: '/certs/local.pem' },
|
|
178
|
+
system: { url: 'https://local.example' },
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
findBookmarkTrustConflicts(store, 'custom', 'https://local.example', '/certs/local.pem'),
|
|
184
|
+
).toEqual([{ name: 'system', caFile: null }])
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -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
|
+
'b21a8c98f0bb75432b460d4562c2d86f39ffb8e88349962e4611ccbf65fa9f73',
|
|
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
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{m as L}from"./index-
|
|
1
|
+
import{m as L}from"./index-LGSWRrk8.js";function S(y,b){for(var u=0;u<b.length;u++){const a=b[u];if(typeof a!="string"&&!Array.isArray(a)){for(const i in a)if(i!=="default"&&!(i in y)){const n=Object.getOwnPropertyDescriptor(a,i);n&&Object.defineProperty(y,i,n.get?n:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(y,Symbol.toStringTag,{value:"Module"}))}function w(y){throw new Error('Could not dynamically require "'+y+'". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.')}var O={exports:{}},j;function C(){return j||(j=1,(function(y,b){(function(u){y.exports=u()})(function(){return(function(){function u(a,i,n){function d(f,_){if(!i[f]){if(!a[f]){var h=typeof w=="function"&&w;if(!_&&h)return h(f,!0);if(g)return g(f,!0);var o=new Error("Cannot find module '"+f+"'");throw o.code="MODULE_NOT_FOUND",o}var e=i[f]={exports:{}};a[f][0].call(e.exports,function(r){var t=a[f][1][r];return d(t||r)},e,e.exports,u,a,i,n)}return i[f].exports}for(var g=typeof w=="function"&&w,m=0;m<n.length;m++)d(n[m]);return d}return u})()({1:[function(u,a,i){Object.defineProperty(i,"__esModule",{value:!0}),i.default=void 0;function n(o){"@babel/helpers - typeof";return n=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},n(o)}function d(o,e){if(!(o instanceof e))throw new TypeError("Cannot call a class as a function")}function g(o,e){for(var r=0;r<e.length;r++){var t=e[r];t.enumerable=t.enumerable||!1,t.configurable=!0,"value"in t&&(t.writable=!0),Object.defineProperty(o,f(t.key),t)}}function m(o,e,r){return e&&g(o.prototype,e),Object.defineProperty(o,"prototype",{writable:!1}),o}function f(o){var e=_(o,"string");return n(e)=="symbol"?e:e+""}function _(o,e){if(n(o)!="object"||!o)return o;var r=o[Symbol.toPrimitive];if(r!==void 0){var t=r.call(o,e);if(n(t)!="object")return t;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(o)}i.default=(function(){function o(){var e=this,r=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},t=r.defaultLayoutOptions,s=t===void 0?{}:t,l=r.algorithms,v=l===void 0?["layered","stress","mrtree","radial","force","disco","sporeOverlap","sporeCompaction","rectpacking"]:l,c=r.workerFactory,p=r.workerUrl;if(d(this,o),this.defaultLayoutOptions=s,this.initialized=!1,typeof p>"u"&&typeof c>"u")throw new Error("Cannot construct an ELK without both 'workerUrl' and 'workerFactory'.");var k=c;typeof p<"u"&&typeof c>"u"&&(k=function(M){return new Worker(M)});var E=k(p);if(typeof E.postMessage!="function")throw new TypeError("Created worker does not provide the required 'postMessage' function.");this.worker=new h(E),this.worker.postMessage({cmd:"register",algorithms:v}).then(function(P){return e.initialized=!0}).catch(console.err)}return m(o,[{key:"layout",value:function(r){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},s=t.layoutOptions,l=s===void 0?this.defaultLayoutOptions:s,v=t.logging,c=v===void 0?!1:v,p=t.measureExecutionTime,k=p===void 0?!1:p;return r?this.worker.postMessage({cmd:"layout",graph:r,layoutOptions:l,options:{logging:c,measureExecutionTime:k}}):Promise.reject(new Error("Missing mandatory parameter 'graph'."))}},{key:"knownLayoutAlgorithms",value:function(){return this.worker.postMessage({cmd:"algorithms"})}},{key:"knownLayoutOptions",value:function(){return this.worker.postMessage({cmd:"options"})}},{key:"knownLayoutCategories",value:function(){return this.worker.postMessage({cmd:"categories"})}},{key:"terminateWorker",value:function(){this.worker&&this.worker.terminate()}}])})();var h=(function(){function o(e){var r=this;if(d(this,o),e===void 0)throw new Error("Missing mandatory parameter 'worker'.");this.resolvers={},this.worker=e,this.worker.onmessage=function(t){setTimeout(function(){r.receive(r,t)},0)}}return m(o,[{key:"postMessage",value:function(r){var t=this.id||0;this.id=t+1,r.id=t;var s=this;return new Promise(function(l,v){s.resolvers[t]=function(c,p){c?(s.convertGwtStyleError(c),v(c)):l(p)},s.worker.postMessage(r)})}},{key:"receive",value:function(r,t){var s=t.data,l=r.resolvers[s.id];l&&(delete r.resolvers[s.id],s.error?l(s.error):l(null,s.data))}},{key:"terminate",value:function(){this.worker&&this.worker.terminate()}},{key:"convertGwtStyleError",value:function(r){if(r){var t=r.__java$exception;t&&(t.cause&&t.cause.backingJsObject&&(r.cause=t.cause.backingJsObject,this.convertGwtStyleError(r.cause)),delete r.__java$exception)}}}])})()},{}],2:[function(u,a,i){var n=u("./elk-api.js").default;Object.defineProperty(a.exports,"__esModule",{value:!0}),a.exports=n,n.default=n},{"./elk-api.js":1}]},{},[2])(2)})})(O)),O.exports}var x=C();const A=L(x),q=S({__proto__:null,default:A},[x]);export{q as e};
|