@astrale-os/cli 1.0.0-beta.2 → 1.0.0-beta.20
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 +16 -1
- package/dist/astrale.js +51105 -58057
- package/dist/public/connect-core.js +2085 -30702
- package/dist/public/keys/index.js +884 -29476
- package/dist/public/paths/index.js +13101 -41452
- package/dist/types/admin/contract.d.ts +26 -0
- package/dist/types/admin/instance/client.d.ts +20 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +6 -10
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/connection/call.d.ts +3 -0
- package/dist/types/connection/command.d.ts +27 -0
- package/dist/types/connection/credential.d.ts +14 -0
- package/dist/types/connection/errors.d.ts +1 -0
- package/dist/types/connection/exchange.d.ts +9 -0
- package/dist/types/connection/failure/classify.d.ts +2 -0
- package/dist/types/connection/failure/index.d.ts +5 -0
- package/dist/types/connection/failure/model.d.ts +25 -0
- package/dist/types/connection/failure/render.d.ts +3 -0
- package/dist/types/connection/index.d.ts +8 -0
- package/dist/types/connection/reasons.d.ts +37 -0
- package/dist/types/connection/self.d.ts +37 -0
- package/dist/types/connection/session.d.ts +31 -0
- package/dist/types/connection/target.d.ts +38 -0
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/lib/admin-instance.d.ts +17 -0
- package/dist/types/lib/admin-target.d.ts +11 -36
- package/dist/types/lib/config.d.ts +7 -59
- package/dist/types/lib/failure-debug.d.ts +1 -0
- package/dist/types/lib/idp.d.ts +25 -173
- package/dist/types/lib/instance-target.d.ts +1 -1
- package/dist/types/lib/instance.d.ts +30 -88
- package/dist/types/lib/log.d.ts +5 -3
- package/dist/types/lib/output.d.ts +1 -1
- package/dist/types/lib/proc.d.ts +43 -0
- package/dist/types/lib/self.d.ts +9 -0
- package/dist/types/lib/update.d.ts +109 -0
- package/dist/types/lib/validation.d.ts +4 -1
- package/package.json +15 -20
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/.spec/architecture.md +12 -5
- package/src/admin/__tests__/fixture.ts +25 -0
- package/src/admin/catalog/.spec/api.d.ts +1 -3
- package/src/admin/catalog/.spec/architecture.md +5 -4
- package/src/admin/catalog/__tests__/client.test.ts +143 -69
- package/src/admin/catalog/client.ts +40 -54
- package/src/admin/catalog/model.ts +3 -4
- package/src/admin/contract.ts +46 -0
- package/src/admin/graph/.spec/api.d.ts +4 -10
- package/src/admin/graph/nodes.ts +1 -1
- package/src/admin/instance/.spec/api.d.ts +5 -9
- package/src/admin/instance/.spec/architecture.md +7 -7
- package/src/admin/instance/__tests__/client.test.ts +157 -175
- package/src/admin/instance/client.ts +39 -128
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +12 -18
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/call.test.ts +34 -0
- package/src/commands/__tests__/domain-install-operation.test.ts +7 -22
- package/src/commands/__tests__/domain-install-owned.test.ts +1 -1
- package/src/commands/__tests__/domain-list.test.ts +5 -25
- package/src/commands/__tests__/install-direct.test.ts +115 -8
- package/src/commands/__tests__/install-identity-override.test.ts +3 -14
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
- package/src/commands/__tests__/instance-status.test.ts +139 -0
- package/src/commands/__tests__/introspect-parse.test.ts +80 -1
- package/src/commands/__tests__/logs.test.ts +250 -2
- package/src/commands/__tests__/read-commands.test.ts +62 -10
- package/src/commands/__tests__/token-ttl.test.ts +49 -4
- package/src/commands/__tests__/update.test.ts +25 -3
- package/src/commands/__tests__/view.test.ts +49 -0
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +71 -59
- package/src/commands/domain/install.ts +54 -35
- package/src/commands/domain/list.ts +1 -1
- package/src/commands/domain/publish.ts +14 -11
- package/src/commands/domain/uninstall.ts +2 -2
- package/src/commands/get.ts +14 -15
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +5 -4
- package/src/commands/instance/create.ts +8 -16
- package/src/commands/instance/delete.ts +12 -9
- package/src/commands/instance/list.ts +2 -2
- package/src/commands/instance/status.ts +66 -20
- package/src/commands/instance/use.ts +16 -12
- package/src/commands/introspect.ts +48 -35
- package/src/commands/logs.ts +123 -33
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +35 -32
- package/src/commands/token.ts +42 -27
- package/src/commands/ui/__tests__/commands.test.ts +152 -0
- package/src/commands/ui/add.ts +51 -0
- package/src/commands/ui/doctor.ts +13 -0
- package/src/commands/ui/init.ts +38 -0
- package/src/commands/ui/list.ts +26 -0
- package/src/commands/ui/preset-apply.ts +19 -0
- package/src/commands/ui/preset-list.ts +12 -0
- package/src/commands/ui/shared.ts +25 -0
- package/src/commands/update.ts +31 -11
- package/src/commands/view.ts +82 -37
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +5 -4
- package/src/connection/.spec/flows/session.ts +2 -2
- package/src/connection/.spec/laws/connection.ts +6 -2
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +20 -1
- package/src/connection/__tests__/errors.test.ts +27 -20
- package/src/connection/__tests__/exchange.test.ts +30 -1
- package/src/connection/__tests__/failure-fixtures.ts +39 -0
- package/src/connection/__tests__/failure-safety.test.ts +75 -0
- package/src/connection/__tests__/self.test.ts +31 -11
- package/src/connection/__tests__/session.test.ts +3 -3
- package/src/connection/__tests__/target.test.ts +1 -0
- package/src/connection/auth.ts +13 -0
- package/src/connection/call.ts +1 -1
- package/src/connection/credential.ts +2 -2
- package/src/connection/errors.ts +1 -355
- package/src/connection/exchange.ts +47 -23
- package/src/connection/failure/classify.ts +110 -0
- package/src/connection/failure/index.ts +18 -0
- package/src/connection/failure/model.ts +28 -0
- package/src/connection/failure/render.ts +116 -0
- package/src/connection/reasons.ts +16 -0
- package/src/connection/self.ts +39 -21
- package/src/connection/session.ts +19 -8
- package/src/errors.ts +3 -2
- package/src/graph/.spec/api.d.ts +12 -5
- package/src/graph/.spec/layout.ts +9 -1
- package/src/graph/__tests__/mutation.test.ts +2 -2
- package/src/graph/__tests__/query.test.ts +25 -6
- package/src/graph/class.ts +30 -0
- package/src/graph/index.ts +1 -0
- package/src/graph/query.ts +21 -32
- package/src/identity/.spec/api.d.ts +2 -2
- package/src/identity/__tests__/registration.test.ts +1 -1
- package/src/identity/registration.ts +3 -3
- package/src/keys/__tests__/keys.test.ts +4 -0
- package/src/keys/credential.ts +1 -0
- package/src/lib/__tests__/admin-target.test.ts +12 -0
- package/src/lib/__tests__/binary.test.ts +16 -1
- package/src/lib/__tests__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/domain-publication.test.ts +10 -16
- package/src/lib/__tests__/idp.test.ts +34 -4
- package/src/lib/__tests__/instance-candidates.test.ts +1 -3
- package/src/lib/__tests__/instance-target.test.ts +7 -39
- package/src/lib/__tests__/log-errors.test.ts +64 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +1 -1
- package/src/lib/__tests__/update.test.ts +264 -11
- package/src/lib/__tests__/view-assets.test.ts +14 -0
- package/src/lib/__tests__/view-external-open-intent.test.ts +150 -0
- package/src/lib/__tests__/view-external-open-origins.test.ts +27 -0
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +26 -3
- package/src/lib/__tests__/view-session.test.ts +2 -1
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/admin-target.ts +21 -2
- package/src/lib/binary.ts +22 -5
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/domain-publication.ts +9 -7
- package/src/lib/failure-debug.ts +26 -0
- package/src/lib/idp.ts +34 -3
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/instance.ts +16 -11
- package/src/lib/log.ts +16 -9
- package/src/lib/output.ts +1 -1
- package/src/lib/proc.ts +7 -2
- package/src/lib/provision-instance.ts +34 -57
- package/src/lib/update.ts +298 -45
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/assets.ts +11 -2
- package/src/lib/view/external-open-intent.ts +41 -0
- package/src/lib/view/external-open-origins.ts +37 -0
- package/src/lib/view/host-capabilities.ts +17 -0
- package/src/lib/view/resolve.ts +1 -1
- package/src/lib/view/server.ts +18 -3
- package/src/lib/view/session.ts +2 -0
- package/src/program/.spec/api.d.ts +1 -0
- package/src/program/__tests__/program.test.ts +163 -6
- package/src/program/argv.ts +14 -0
- package/src/program/build.ts +26 -4
- package/src/program/command.ts +1 -0
- package/src/program/index.ts +1 -0
- package/src/program/registry.ts +2 -1
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- package/src/telemetry/__tests__/recorder.test.ts +1 -0
- package/src/telemetry/recorder.ts +9 -1
- package/src/ui/.spec/api.d.ts +14 -0
- package/src/ui/.spec/architecture.md +10 -0
- package/src/ui/.spec/laws.ts +36 -0
- package/src/ui/.spec/layout.ts +16 -0
- package/src/ui/__tests__/ui.test.ts +981 -0
- package/src/ui/index.ts +13 -0
- package/src/ui/lock.ts +87 -0
- package/src/ui/model.ts +83 -0
- package/src/ui/operations.ts +710 -0
- package/src/ui/project.ts +219 -0
- package/src/ui/release.ts +321 -0
- package/src/ui/runner.ts +18 -0
- package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
- package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
- package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
- package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
- package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
- package/studio/client/dist/index.html +3 -2
- package/studio/package.json +7 -13
- package/studio/server/agent/ask.test.ts +1 -1
- package/studio/server/agent/bridge/grant.test.ts +1 -1
- package/studio/server/agent/bridge/routes.test.ts +1 -1
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/harness/gateway/token.test.ts +1 -1
- package/studio/server/agent/harness/gateway/token.ts +3 -3
- package/studio/server/agent/prompts/anchors.test.ts +32 -36
- package/studio/server/agent/prompts/anchors.ts +20 -49
- package/studio/server/agent/prompts/system.test.ts +3 -2
- package/studio/server/agent/prompts/system.ts +3 -3
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/api/canvas.ts +0 -1
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +1 -1
- package/studio/server/cache.test.ts +23 -12
- package/studio/server/cache.ts +12 -15
- package/studio/server/domain.test.ts +50 -68
- package/studio/server/domain.ts +82 -63
- package/studio/server/handoff/copy.ts +1 -1
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/probe.test.ts +1 -1
- package/studio/server/introspect/anatomy/views/routes.ts +46 -37
- package/studio/server/introspect/anatomy/views.ts +9 -14
- package/studio/server/introspect/anatomy-extras.test.ts +26 -42
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +6 -9
- package/studio/server/introspect/bundle.ts +0 -3
- package/studio/server/introspect/canonical-schema.test.ts +94 -389
- package/studio/server/introspect/canonical-schema.ts +227 -457
- package/studio/server/introspect/core-extractor.ts +18 -127
- package/studio/server/introspect/core.ts +1 -2
- package/studio/server/introspect/diff.test.ts +11 -7
- package/studio/server/introspect/diff.ts +18 -55
- package/studio/server/introspect/extractor.ts +22 -117
- package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
- package/studio/server/introspect/overlay.test.ts +26 -54
- package/studio/server/introspect/overlay.ts +10 -37
- package/studio/server/introspect/revision.test.ts +1 -1
- package/studio/server/introspect/revision.ts +1 -1
- package/studio/server/introspect/runtime.test.ts +65 -183
- package/studio/server/introspect/runtime.ts +8 -25
- package/studio/server/introspect/schema-ir-json.test.ts +70 -0
- package/studio/server/introspect/schema-ir-json.ts +38 -68
- package/studio/server/introspect/schema-refs.test.ts +43 -88
- package/studio/server/introspect/schema-refs.ts +14 -49
- package/studio/server/introspect/source-overlay/handlers.ts +116 -636
- package/studio/server/introspect/source-overlay/kernel-calls.ts +0 -3
- package/studio/server/introspect/source-overlay/spans.ts +15 -45
- package/studio/server/state/baseline.test.ts +14 -9
- package/studio/server/state/baseline.ts +3 -5
- package/studio/server/state/visibility.ts +1 -5
- package/studio/server/views/model.ts +3 -3
- package/studio/server/views/target.test.ts +17 -74
- package/studio/server/views/target.ts +24 -50
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +8 -4
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/schema.ts +38 -107
- package/studio/shared/contracts/surface.test.ts +13 -15
- package/studio/shared/contracts/workspace.ts +2 -11
- package/studio/shared/schema/identity.test.ts +20 -44
- package/studio/shared/schema/identity.ts +9 -23
- package/viewer/dist/main.js +30 -51
- package/.check-workspace.cjs +0 -40
- package/src/admin/binding.ts +0 -168
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
- package/studio/client/dist/assets/elk-api-lwhFo7vB.js +0 -1
- package/studio/client/dist/assets/index-B-c-smo9.js +0 -8
- package/studio/client/dist/assets/index-BckHuAWk.js +0 -81
- package/studio/client/dist/assets/index-C4aNQ6dz.css +0 -1
- package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
- package/studio/server/introspect/anatomy/views/legacy.ts +0 -232
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { chmod, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
|
+
admitScriptInstall,
|
|
8
|
+
classifyUpdateExecution,
|
|
7
9
|
DEFAULT_UPDATE_CHANNEL,
|
|
8
10
|
InstallMetadataSchema,
|
|
11
|
+
readInstallMetadata,
|
|
9
12
|
releaseBase,
|
|
13
|
+
replaceStandaloneCohort,
|
|
10
14
|
shouldUpdate,
|
|
11
15
|
updateAstrale,
|
|
12
16
|
writeInstallMetadata,
|
|
13
17
|
type InstallMetadata,
|
|
18
|
+
type UpdateExecution,
|
|
14
19
|
} from '../update'
|
|
15
20
|
|
|
16
21
|
async function makeFakeRelease(
|
|
17
22
|
root: string,
|
|
18
23
|
version: string,
|
|
19
|
-
options: { binaryVersion?: string; legacyManifest?: boolean } = {},
|
|
24
|
+
options: { binaryVersion?: string; legacyManifest?: boolean; omitViewerIndex?: boolean } = {},
|
|
20
25
|
): Promise<string> {
|
|
21
26
|
const release = join(root, 'release')
|
|
22
27
|
const payload = join(root, 'payload')
|
|
@@ -28,9 +33,14 @@ async function makeFakeRelease(
|
|
|
28
33
|
`#!/usr/bin/env sh\nif [ "$1" = "--version" ]; then echo "${binaryVersion}"; exit 0; fi\necho astrale\n`,
|
|
29
34
|
)
|
|
30
35
|
await chmod(join(payload, 'astrale'), 0o755)
|
|
36
|
+
await mkdir(join(payload, 'viewer', 'dist'), { recursive: true })
|
|
37
|
+
await writeFile(join(payload, 'viewer', 'dist', 'main.js'), 'viewer main\n')
|
|
38
|
+
if (!options.omitViewerIndex) {
|
|
39
|
+
await writeFile(join(payload, 'viewer', 'dist', 'index.html'), '<!doctype html>\n')
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
const asset = join(release, 'astrale-darwin-arm64.tar.gz')
|
|
33
|
-
const tar = Bun.spawn(['tar', '-C', payload, '-czf', asset, 'astrale'])
|
|
43
|
+
const tar = Bun.spawn(['tar', '-C', payload, '-czf', asset, 'astrale', 'viewer'])
|
|
34
44
|
expect(await tar.exited).toBe(0)
|
|
35
45
|
const shaProc = Bun.spawn(['shasum', '-a', '256', asset], { stdout: 'pipe' })
|
|
36
46
|
const sha = (await new Response(shaProc.stdout).text()).trim().split(/\s+/)[0]
|
|
@@ -65,7 +75,7 @@ async function makeFakeRelease(
|
|
|
65
75
|
async function makeInstall(
|
|
66
76
|
root: string,
|
|
67
77
|
version: string,
|
|
68
|
-
): Promise<{ meta: InstallMetadata; path: string }> {
|
|
78
|
+
): Promise<{ meta: InstallMetadata; path: string; execution: UpdateExecution }> {
|
|
69
79
|
const bin = join(root, 'bin', 'astrale')
|
|
70
80
|
await mkdir(join(root, 'bin'), { recursive: true })
|
|
71
81
|
await writeFile(
|
|
@@ -73,6 +83,9 @@ async function makeInstall(
|
|
|
73
83
|
`#!/usr/bin/env sh\nif [ "$1" = "--version" ]; then echo "${version}"; exit 0; fi\necho old\n`,
|
|
74
84
|
)
|
|
75
85
|
await chmod(bin, 0o755)
|
|
86
|
+
await mkdir(join(root, 'bin', 'viewer', 'dist'), { recursive: true })
|
|
87
|
+
await writeFile(join(root, 'bin', 'viewer', 'dist', 'main.js'), 'old viewer main\n')
|
|
88
|
+
await writeFile(join(root, 'bin', 'viewer', 'dist', 'index.html'), 'old viewer html\n')
|
|
76
89
|
const path = join(root, 'home', 'install.json')
|
|
77
90
|
const meta: InstallMetadata = {
|
|
78
91
|
method: 'script',
|
|
@@ -82,10 +95,33 @@ async function makeInstall(
|
|
|
82
95
|
bin,
|
|
83
96
|
}
|
|
84
97
|
await writeInstallMetadata(meta, path)
|
|
85
|
-
return { meta, path }
|
|
98
|
+
return { meta, path, execution: { kind: 'standalone', executable: bin } }
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
describe('update helpers', () => {
|
|
102
|
+
test('classifies only a Bun-compiled executable as standalone', () => {
|
|
103
|
+
expect(
|
|
104
|
+
classifyUpdateExecution({
|
|
105
|
+
bunVersion: '1.3.14',
|
|
106
|
+
executable: '/tmp/astrale',
|
|
107
|
+
entry: '/$bunfs/root/astrale',
|
|
108
|
+
}),
|
|
109
|
+
).toEqual({ kind: 'standalone', executable: '/tmp/astrale' })
|
|
110
|
+
expect(
|
|
111
|
+
classifyUpdateExecution({
|
|
112
|
+
bunVersion: '1.3.14',
|
|
113
|
+
executable: '/opt/homebrew/bin/bun',
|
|
114
|
+
entry: '/tmp/astrale.ts',
|
|
115
|
+
}),
|
|
116
|
+
).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/bun' })
|
|
117
|
+
expect(
|
|
118
|
+
classifyUpdateExecution({
|
|
119
|
+
executable: '/opt/homebrew/bin/node',
|
|
120
|
+
entry: '/tmp/astrale.js',
|
|
121
|
+
}),
|
|
122
|
+
).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/node' })
|
|
123
|
+
})
|
|
124
|
+
|
|
89
125
|
test('defaults missing install metadata to the beta channel', () => {
|
|
90
126
|
expect(DEFAULT_UPDATE_CHANNEL).toBe('beta')
|
|
91
127
|
expect(
|
|
@@ -118,10 +154,103 @@ describe('update helpers', () => {
|
|
|
118
154
|
})
|
|
119
155
|
})
|
|
120
156
|
|
|
157
|
+
describe('script install admission', () => {
|
|
158
|
+
test('a metadata commit failure restores the exact previous file', async () => {
|
|
159
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
160
|
+
const path = join(root, 'install.json')
|
|
161
|
+
const original = '{"exact":"previous metadata"}\n'
|
|
162
|
+
await writeFile(path, original)
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
writeInstallMetadata(
|
|
166
|
+
{
|
|
167
|
+
method: 'script',
|
|
168
|
+
channel: 'beta',
|
|
169
|
+
version: '1.1.0',
|
|
170
|
+
repo: 'astrale-os/cli',
|
|
171
|
+
bin: '/tmp/astrale',
|
|
172
|
+
},
|
|
173
|
+
path,
|
|
174
|
+
{
|
|
175
|
+
mkdir,
|
|
176
|
+
rm,
|
|
177
|
+
writeFile,
|
|
178
|
+
rename: async (from, to) => {
|
|
179
|
+
if (String(from).endsWith('.next')) throw new Error('injected metadata commit failure')
|
|
180
|
+
await rename(from, to)
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
),
|
|
184
|
+
).rejects.toThrow('injected metadata commit failure')
|
|
185
|
+
|
|
186
|
+
expect(await readFile(path, 'utf8')).toBe(original)
|
|
187
|
+
await expect(readFile(`${path}.next`, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
|
|
188
|
+
await expect(readFile(`${path}.previous`, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('rejects malformed JSON with the stable metadata error', async () => {
|
|
192
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
193
|
+
const path = join(root, 'install.json')
|
|
194
|
+
await writeFile(path, '{')
|
|
195
|
+
|
|
196
|
+
await expect(readInstallMetadata(path)).rejects.toMatchObject({
|
|
197
|
+
code: 'UPDATE_BAD_INSTALL_METADATA',
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
test('admits a symlink only when it resolves to the recorded binary', async () => {
|
|
202
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
203
|
+
const { meta } = await makeInstall(root, '1.0.0')
|
|
204
|
+
const alias = join(root, 'astrale-alias')
|
|
205
|
+
await symlink(meta.bin, alias)
|
|
206
|
+
|
|
207
|
+
const admitted = await admitScriptInstall(meta, {
|
|
208
|
+
kind: 'standalone',
|
|
209
|
+
executable: alias,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
expect(admitted.metadata).toEqual(meta)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('rejects a standalone binary that does not own the recorded target', async () => {
|
|
216
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
217
|
+
const { meta } = await makeInstall(root, '1.0.0')
|
|
218
|
+
const other = join(root, 'other-astrale')
|
|
219
|
+
await writeFile(other, '#!/bin/sh\n')
|
|
220
|
+
|
|
221
|
+
await expect(
|
|
222
|
+
admitScriptInstall(meta, { kind: 'standalone', executable: other }),
|
|
223
|
+
).rejects.toMatchObject({ code: 'UPDATE_INSTALL_MISMATCH' })
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
121
227
|
describe('updateAstrale', () => {
|
|
228
|
+
test('a package-managed process never consults or mutates a coexisting script install', async () => {
|
|
229
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
230
|
+
const { path, meta } = await makeInstall(root, '1.0.0')
|
|
231
|
+
const before = await readFile(meta.bin, 'utf8')
|
|
232
|
+
process.env.ASTRALE_UPDATE_BASE = 'file:///definitely-not-a-release'
|
|
233
|
+
try {
|
|
234
|
+
const result = await updateAstrale({
|
|
235
|
+
currentVersion: '2.0.0',
|
|
236
|
+
installPath: path,
|
|
237
|
+
execution: { kind: 'package-managed', executable: '/opt/homebrew/bin/node' },
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(result).toEqual({
|
|
241
|
+
status: 'managed',
|
|
242
|
+
currentVersion: '2.0.0',
|
|
243
|
+
executable: '/opt/homebrew/bin/node',
|
|
244
|
+
})
|
|
245
|
+
expect(await readFile(meta.bin, 'utf8')).toBe(before)
|
|
246
|
+
} finally {
|
|
247
|
+
delete process.env.ASTRALE_UPDATE_BASE
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
|
|
122
251
|
test('check compares the installed release identity from metadata', async () => {
|
|
123
252
|
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
124
|
-
const { path } = await makeInstall(root, 'main-abc123')
|
|
253
|
+
const { path, execution } = await makeInstall(root, 'main-abc123')
|
|
125
254
|
const release = await makeFakeRelease(root, 'main-abc123', { binaryVersion: '1.0.0' })
|
|
126
255
|
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
127
256
|
try {
|
|
@@ -130,6 +259,7 @@ describe('updateAstrale', () => {
|
|
|
130
259
|
currentVersion: '1.0.0',
|
|
131
260
|
platform: { os: 'darwin', arch: 'arm64' },
|
|
132
261
|
installPath: path,
|
|
262
|
+
execution,
|
|
133
263
|
})
|
|
134
264
|
|
|
135
265
|
expect(result).toMatchObject({
|
|
@@ -144,7 +274,7 @@ describe('updateAstrale', () => {
|
|
|
144
274
|
|
|
145
275
|
test('check reports available update without replacing the binary', async () => {
|
|
146
276
|
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
147
|
-
const { path, meta } = await makeInstall(root, '1.0.0')
|
|
277
|
+
const { path, meta, execution } = await makeInstall(root, '1.0.0')
|
|
148
278
|
const release = await makeFakeRelease(root, '1.1.0')
|
|
149
279
|
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
150
280
|
try {
|
|
@@ -153,6 +283,7 @@ describe('updateAstrale', () => {
|
|
|
153
283
|
currentVersion: '1.0.0',
|
|
154
284
|
platform: { os: 'darwin', arch: 'arm64' },
|
|
155
285
|
installPath: path,
|
|
286
|
+
execution,
|
|
156
287
|
})
|
|
157
288
|
|
|
158
289
|
expect(result).toMatchObject({
|
|
@@ -168,7 +299,7 @@ describe('updateAstrale', () => {
|
|
|
168
299
|
|
|
169
300
|
test('supports legacy string asset manifests', async () => {
|
|
170
301
|
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
171
|
-
const { path, meta } = await makeInstall(root, '1.0.0')
|
|
302
|
+
const { path, meta, execution } = await makeInstall(root, '1.0.0')
|
|
172
303
|
const release = await makeFakeRelease(root, '1.1.0', { legacyManifest: true })
|
|
173
304
|
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
174
305
|
try {
|
|
@@ -177,6 +308,7 @@ describe('updateAstrale', () => {
|
|
|
177
308
|
currentVersion: '1.0.0',
|
|
178
309
|
platform: { os: 'darwin', arch: 'arm64' },
|
|
179
310
|
installPath: path,
|
|
311
|
+
execution,
|
|
180
312
|
})
|
|
181
313
|
|
|
182
314
|
expect(check).toMatchObject({
|
|
@@ -188,6 +320,7 @@ describe('updateAstrale', () => {
|
|
|
188
320
|
currentVersion: '1.0.0',
|
|
189
321
|
platform: { os: 'darwin', arch: 'arm64' },
|
|
190
322
|
installPath: path,
|
|
323
|
+
execution,
|
|
191
324
|
})
|
|
192
325
|
|
|
193
326
|
expect(result).toMatchObject({
|
|
@@ -204,7 +337,7 @@ describe('updateAstrale', () => {
|
|
|
204
337
|
|
|
205
338
|
test('updates the binary and install metadata', async () => {
|
|
206
339
|
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
207
|
-
const { path, meta } = await makeInstall(root, '1.0.0')
|
|
340
|
+
const { path, meta, execution } = await makeInstall(root, '1.0.0')
|
|
208
341
|
const release = await makeFakeRelease(root, '1.1.0')
|
|
209
342
|
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
210
343
|
try {
|
|
@@ -212,6 +345,7 @@ describe('updateAstrale', () => {
|
|
|
212
345
|
currentVersion: '1.0.0',
|
|
213
346
|
platform: { os: 'darwin', arch: 'arm64' },
|
|
214
347
|
installPath: path,
|
|
348
|
+
execution,
|
|
215
349
|
})
|
|
216
350
|
|
|
217
351
|
expect(result).toMatchObject({
|
|
@@ -224,6 +358,124 @@ describe('updateAstrale', () => {
|
|
|
224
358
|
expect(await versionProc.exited).toBe(0)
|
|
225
359
|
const updatedMeta = JSON.parse(await readFile(path, 'utf8')) as InstallMetadata
|
|
226
360
|
expect(updatedMeta.version).toBe('1.1.0')
|
|
361
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'main.js'), 'utf8')).toBe(
|
|
362
|
+
'viewer main\n',
|
|
363
|
+
)
|
|
364
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'index.html'), 'utf8')).toBe(
|
|
365
|
+
'<!doctype html>\n',
|
|
366
|
+
)
|
|
367
|
+
} finally {
|
|
368
|
+
delete process.env.ASTRALE_UPDATE_BASE
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('a partial Viewer archive leaves the installed cohort and metadata unchanged', async () => {
|
|
373
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
374
|
+
const { path, meta, execution } = await makeInstall(root, '1.0.0')
|
|
375
|
+
const release = await makeFakeRelease(root, '1.1.0', { omitViewerIndex: true })
|
|
376
|
+
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
377
|
+
const beforeBinary = await readFile(meta.bin, 'utf8')
|
|
378
|
+
const beforeMetadata = await readFile(path, 'utf8')
|
|
379
|
+
try {
|
|
380
|
+
await expect(
|
|
381
|
+
updateAstrale({
|
|
382
|
+
currentVersion: '1.0.0',
|
|
383
|
+
platform: { os: 'darwin', arch: 'arm64' },
|
|
384
|
+
installPath: path,
|
|
385
|
+
execution,
|
|
386
|
+
}),
|
|
387
|
+
).rejects.toThrow()
|
|
388
|
+
|
|
389
|
+
expect(await readFile(meta.bin, 'utf8')).toBe(beforeBinary)
|
|
390
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'main.js'), 'utf8')).toBe(
|
|
391
|
+
'old viewer main\n',
|
|
392
|
+
)
|
|
393
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'index.html'), 'utf8')).toBe(
|
|
394
|
+
'old viewer html\n',
|
|
395
|
+
)
|
|
396
|
+
expect(await readFile(path, 'utf8')).toBe(beforeMetadata)
|
|
397
|
+
} finally {
|
|
398
|
+
delete process.env.ASTRALE_UPDATE_BASE
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('a Viewer commit failure rolls back binary, Viewer, and metadata', async () => {
|
|
403
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
404
|
+
const { path, meta, execution } = await makeInstall(root, '1.0.0')
|
|
405
|
+
const release = await makeFakeRelease(root, '1.1.0')
|
|
406
|
+
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
407
|
+
const beforeBinary = await readFile(meta.bin, 'utf8')
|
|
408
|
+
const beforeMetadata = await readFile(path, 'utf8')
|
|
409
|
+
try {
|
|
410
|
+
await expect(
|
|
411
|
+
updateAstrale(
|
|
412
|
+
{
|
|
413
|
+
currentVersion: '1.0.0',
|
|
414
|
+
platform: { os: 'darwin', arch: 'arm64' },
|
|
415
|
+
installPath: path,
|
|
416
|
+
execution,
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
replaceStandaloneCohort: (installed, next, viewer) =>
|
|
420
|
+
replaceStandaloneCohort(installed, next, viewer, {
|
|
421
|
+
rename: async (from, to) => {
|
|
422
|
+
if (String(from).endsWith('viewer.next')) {
|
|
423
|
+
throw Object.assign(new Error('injected Viewer commit failure'), {
|
|
424
|
+
code: 'EIO',
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
await rename(from, to)
|
|
428
|
+
},
|
|
429
|
+
}),
|
|
430
|
+
},
|
|
431
|
+
),
|
|
432
|
+
).rejects.toThrow('injected Viewer commit failure')
|
|
433
|
+
|
|
434
|
+
expect(await readFile(meta.bin, 'utf8')).toBe(beforeBinary)
|
|
435
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'main.js'), 'utf8')).toBe(
|
|
436
|
+
'old viewer main\n',
|
|
437
|
+
)
|
|
438
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'index.html'), 'utf8')).toBe(
|
|
439
|
+
'old viewer html\n',
|
|
440
|
+
)
|
|
441
|
+
expect(await readFile(path, 'utf8')).toBe(beforeMetadata)
|
|
442
|
+
} finally {
|
|
443
|
+
delete process.env.ASTRALE_UPDATE_BASE
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
test('a metadata failure after cohort commit restores binary, Viewer, and metadata', async () => {
|
|
448
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
449
|
+
const { path, meta, execution } = await makeInstall(root, '1.0.0')
|
|
450
|
+
const release = await makeFakeRelease(root, '1.1.0')
|
|
451
|
+
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
452
|
+
const beforeBinary = await readFile(meta.bin, 'utf8')
|
|
453
|
+
const beforeMetadata = await readFile(path, 'utf8')
|
|
454
|
+
try {
|
|
455
|
+
await expect(
|
|
456
|
+
updateAstrale(
|
|
457
|
+
{
|
|
458
|
+
currentVersion: '1.0.0',
|
|
459
|
+
platform: { os: 'darwin', arch: 'arm64' },
|
|
460
|
+
installPath: path,
|
|
461
|
+
execution,
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
writeInstallMetadata: async () => {
|
|
465
|
+
throw new Error('injected metadata write failure')
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
),
|
|
469
|
+
).rejects.toThrow('injected metadata write failure')
|
|
470
|
+
|
|
471
|
+
expect(await readFile(meta.bin, 'utf8')).toBe(beforeBinary)
|
|
472
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'main.js'), 'utf8')).toBe(
|
|
473
|
+
'old viewer main\n',
|
|
474
|
+
)
|
|
475
|
+
expect(await readFile(join(dirname(meta.bin), 'viewer', 'dist', 'index.html'), 'utf8')).toBe(
|
|
476
|
+
'old viewer html\n',
|
|
477
|
+
)
|
|
478
|
+
expect(await readFile(path, 'utf8')).toBe(beforeMetadata)
|
|
227
479
|
} finally {
|
|
228
480
|
delete process.env.ASTRALE_UPDATE_BASE
|
|
229
481
|
}
|
|
@@ -231,7 +483,7 @@ describe('updateAstrale', () => {
|
|
|
231
483
|
|
|
232
484
|
test('updates canary-style releases whose binary reports the package version', async () => {
|
|
233
485
|
const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
|
|
234
|
-
const { path, meta } = await makeInstall(root, 'main-old123')
|
|
486
|
+
const { path, meta, execution } = await makeInstall(root, 'main-old123')
|
|
235
487
|
const release = await makeFakeRelease(root, 'main-new456', { binaryVersion: '1.0.0' })
|
|
236
488
|
process.env.ASTRALE_UPDATE_BASE = `file://${release}`
|
|
237
489
|
try {
|
|
@@ -239,6 +491,7 @@ describe('updateAstrale', () => {
|
|
|
239
491
|
currentVersion: '1.0.0',
|
|
240
492
|
platform: { os: 'darwin', arch: 'arm64' },
|
|
241
493
|
installPath: path,
|
|
494
|
+
execution,
|
|
242
495
|
})
|
|
243
496
|
|
|
244
497
|
expect(result).toMatchObject({
|
|
@@ -143,4 +143,18 @@ describe('viewer asset resolution', () => {
|
|
|
143
143
|
expect(stderr).toBe('')
|
|
144
144
|
expect(stdout.trim()).toBe(await realpath(viewer))
|
|
145
145
|
})
|
|
146
|
+
|
|
147
|
+
test('resolves assets shipped beside a Bun-compiled standalone executable', async () => {
|
|
148
|
+
const root = await mkdtemp(join(tmpdir(), 'astrale-view-standalone-'))
|
|
149
|
+
temporaryDirectories.push(root)
|
|
150
|
+
const executable = join(root, 'bin', 'astrale')
|
|
151
|
+
const viewer = join(root, 'bin', 'viewer', 'dist')
|
|
152
|
+
await mkdir(viewer, { recursive: true })
|
|
153
|
+
await writeFile(join(viewer, 'main.js'), '')
|
|
154
|
+
await writeFile(join(viewer, 'index.html'), '')
|
|
155
|
+
|
|
156
|
+
expect(viewerDistDir('file:///$bunfs/root/assets.ts', '/$bunfs/root/astrale', executable)).toBe(
|
|
157
|
+
viewer,
|
|
158
|
+
)
|
|
159
|
+
})
|
|
146
160
|
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ChildrenChannel, ExternalOpenRequest, Shell } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
import { capabilitiesMiddleware, createIntentPipeline, createIntentRouter } from '@astrale-os/shell'
|
|
4
|
+
import { describe, expect, mock, test } from 'bun:test'
|
|
5
|
+
|
|
6
|
+
import type { ExternalOpenIntentMessage } from '../view/external-open-intent'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
installExternalOpenIntentHandler,
|
|
10
|
+
openExternalBrowserWindow,
|
|
11
|
+
} from '../view/external-open-intent'
|
|
12
|
+
import { viewHostCapabilities } from '../view/host-capabilities'
|
|
13
|
+
|
|
14
|
+
describe('View external navigation host effect', () => {
|
|
15
|
+
test('the real Shell pipeline opens only an exact granted origin and replies to the physical child', async () => {
|
|
16
|
+
const sent: Array<{ windowId: string; message: unknown }> = []
|
|
17
|
+
const children = {
|
|
18
|
+
send: (windowId: string, value: unknown) => sent.push({ windowId, message: value }),
|
|
19
|
+
has: () => true,
|
|
20
|
+
on: () => () => undefined,
|
|
21
|
+
onClose: () => () => undefined,
|
|
22
|
+
} as ChildrenChannel
|
|
23
|
+
const capabilities = viewHostCapabilities(['https://connect.nango.dev'])
|
|
24
|
+
const pipeline = createIntentPipeline()
|
|
25
|
+
pipeline.use(
|
|
26
|
+
capabilitiesMiddleware({
|
|
27
|
+
lookup: (sender) => (sender === 'physical-child' ? capabilities : undefined),
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
const router = createIntentRouter({
|
|
31
|
+
selfWindowId: 'root',
|
|
32
|
+
pipeline,
|
|
33
|
+
parent: null,
|
|
34
|
+
children,
|
|
35
|
+
})
|
|
36
|
+
const open = mock((_request: ExternalOpenRequest) => true)
|
|
37
|
+
installExternalOpenIntentHandler(
|
|
38
|
+
{
|
|
39
|
+
children,
|
|
40
|
+
onIntent: router.onLocal.bind(router),
|
|
41
|
+
} as unknown as Shell,
|
|
42
|
+
{ open },
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
await router.handleInbound(
|
|
46
|
+
'child',
|
|
47
|
+
message('https://connect.nango.dev/session', 'forged-sibling'),
|
|
48
|
+
'physical-child',
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
expect(open).toHaveBeenCalledTimes(1)
|
|
52
|
+
expect(sent).toEqual([
|
|
53
|
+
{
|
|
54
|
+
windowId: 'physical-child',
|
|
55
|
+
message: expect.objectContaining({
|
|
56
|
+
envelope: expect.objectContaining({
|
|
57
|
+
payload: { correlationId: 'request-1', result: { outcome: 'opened' } },
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
for (const denied of [
|
|
64
|
+
'http://connect.nango.dev/session',
|
|
65
|
+
'https://sub.connect.nango.dev/session',
|
|
66
|
+
'https://connect.nango.dev:444/session',
|
|
67
|
+
'https://evil.example/session',
|
|
68
|
+
]) {
|
|
69
|
+
await router.handleInbound('child', message(denied), 'physical-child')
|
|
70
|
+
}
|
|
71
|
+
await router.handleInbound('child', message('https://connect.nango.dev/session'), 'ungranted')
|
|
72
|
+
expect(open).toHaveBeenCalledTimes(1)
|
|
73
|
+
expect(sent).toHaveLength(1)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('reports a popup block through the admitted pipeline', async () => {
|
|
77
|
+
const sent = mock((_windowId: string, _message: unknown) => undefined)
|
|
78
|
+
const children = {
|
|
79
|
+
send: sent,
|
|
80
|
+
has: () => true,
|
|
81
|
+
on: () => () => undefined,
|
|
82
|
+
onClose: () => () => undefined,
|
|
83
|
+
} as ChildrenChannel
|
|
84
|
+
const pipeline = createIntentPipeline()
|
|
85
|
+
pipeline.use(
|
|
86
|
+
capabilitiesMiddleware({
|
|
87
|
+
lookup: () => viewHostCapabilities(['https://connect.nango.dev']),
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
const router = createIntentRouter({ selfWindowId: 'root', pipeline, parent: null, children })
|
|
91
|
+
installExternalOpenIntentHandler(
|
|
92
|
+
{ children, onIntent: router.onLocal.bind(router) } as unknown as Shell,
|
|
93
|
+
{ open: () => false },
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
await router.handleInbound('child', message('https://connect.nango.dev/session'), 'child-1')
|
|
97
|
+
|
|
98
|
+
expect(sent).toHaveBeenCalledWith(
|
|
99
|
+
'child-1',
|
|
100
|
+
expect.objectContaining({
|
|
101
|
+
envelope: expect.objectContaining({
|
|
102
|
+
payload: { correlationId: 'request-1', result: { outcome: 'blocked' } },
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('isolates a fresh inert context before navigating and closes it when isolation fails', () => {
|
|
109
|
+
const replace = mock((_url: string) => undefined)
|
|
110
|
+
const opened = { opener: {}, location: { replace }, close: mock(() => undefined) }
|
|
111
|
+
const open = mock(() => opened)
|
|
112
|
+
|
|
113
|
+
expect(
|
|
114
|
+
openExternalBrowserWindow({ open } as never, {
|
|
115
|
+
url: 'https://connect.nango.dev/session',
|
|
116
|
+
mode: 'popup',
|
|
117
|
+
}),
|
|
118
|
+
).toBe(true)
|
|
119
|
+
expect(open).toHaveBeenCalledWith('', '_blank', 'popup,width=720,height=760')
|
|
120
|
+
expect(opened.opener).toBeNull()
|
|
121
|
+
expect(replace).toHaveBeenCalledWith('https://connect.nango.dev/session')
|
|
122
|
+
|
|
123
|
+
const close = mock(() => undefined)
|
|
124
|
+
const unsafe = Object.defineProperty({ close, location: { replace: mock() } }, 'opener', {
|
|
125
|
+
set: () => {
|
|
126
|
+
throw new Error('opener isolation refused')
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
expect(
|
|
130
|
+
openExternalBrowserWindow({ open: () => unsafe } as never, {
|
|
131
|
+
url: 'https://connect.nango.dev/session',
|
|
132
|
+
mode: 'tab',
|
|
133
|
+
}),
|
|
134
|
+
).toBe(false)
|
|
135
|
+
expect(close).toHaveBeenCalledTimes(1)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
function message(url: string, sender = 'child-1'): ExternalOpenIntentMessage {
|
|
140
|
+
return {
|
|
141
|
+
type: 'intent',
|
|
142
|
+
version: 1,
|
|
143
|
+
envelope: {
|
|
144
|
+
name: 'browser.openExternal',
|
|
145
|
+
payload: { url, mode: 'popup' },
|
|
146
|
+
sender: { windowId: sender },
|
|
147
|
+
correlationId: 'request-1',
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { admitExternalOpenOrigins } from '../view/external-open-origins'
|
|
4
|
+
|
|
5
|
+
describe('View external navigation grants', () => {
|
|
6
|
+
test('defaults to no authority and canonicalizes exact HTTPS origins', () => {
|
|
7
|
+
expect(admitExternalOpenOrigins(undefined)).toEqual([])
|
|
8
|
+
expect(
|
|
9
|
+
admitExternalOpenOrigins([
|
|
10
|
+
'https://connect.nango.dev',
|
|
11
|
+
'https://connect.nango.dev/',
|
|
12
|
+
'https://connect.composio.dev:443',
|
|
13
|
+
]),
|
|
14
|
+
).toEqual(['https://connect.nango.dev', 'https://connect.composio.dev'])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test.each([
|
|
18
|
+
'http://connect.nango.dev',
|
|
19
|
+
'https://user@connect.nango.dev',
|
|
20
|
+
'https://connect.nango.dev/path',
|
|
21
|
+
'https://connect.nango.dev?session=secret',
|
|
22
|
+
'https://*.example.com',
|
|
23
|
+
'not-a-url',
|
|
24
|
+
])('rejects a non-origin grant: %s', (candidate) => {
|
|
25
|
+
expect(() => admitExternalOpenOrigins([candidate])).toThrow('exact HTTPS origin')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -23,7 +23,6 @@ const profile: ResolvedView = {
|
|
|
23
23
|
kind: 'definition',
|
|
24
24
|
definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
|
|
25
25
|
},
|
|
26
|
-
auth: 'required',
|
|
27
26
|
},
|
|
28
27
|
href: 'https://shell.test/profile',
|
|
29
28
|
handshake: 'shell',
|
|
@@ -41,7 +40,6 @@ const card: ResolvedView = {
|
|
|
41
40
|
kind: 'definition',
|
|
42
41
|
definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
|
|
43
42
|
},
|
|
44
|
-
auth: 'public',
|
|
45
43
|
},
|
|
46
44
|
href: 'https://shell.test/card',
|
|
47
45
|
handshake: 'none',
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AuthApi } from '@astrale-os/sdk/auth'
|
|
2
|
+
|
|
3
|
+
import { describe, expect, mock, test } from 'bun:test'
|
|
2
4
|
import { once } from 'node:events'
|
|
3
5
|
|
|
4
6
|
import type { ViewServeConfig } from '../view/session'
|
|
5
7
|
|
|
6
8
|
import { findFreePort } from '../port'
|
|
7
|
-
import { startViewServer } from '../view/server'
|
|
9
|
+
import { mintViewCredential, startViewServer } from '../view/server'
|
|
8
10
|
|
|
9
11
|
const digest = (character: string) => `sha256:${character.repeat(64)}` as const
|
|
10
12
|
const target = (value: string) => value as ViewServeConfig['session']['view']['target']
|
|
@@ -13,6 +15,19 @@ const revision = (character: string) =>
|
|
|
13
15
|
digest(character) as ViewServeConfig['session']['view']['route']['revision']
|
|
14
16
|
|
|
15
17
|
describe('view session server credentials', () => {
|
|
18
|
+
test('mints a proof-bounded credential for the Kernel audience', async () => {
|
|
19
|
+
const mint = mock(async () => 'minted-credential')
|
|
20
|
+
const auth = { mint } as unknown as Pick<AuthApi, 'mint'>
|
|
21
|
+
|
|
22
|
+
await expect(mintViewCredential(auth, issuer('https://kernel.test'))).resolves.toBe(
|
|
23
|
+
'minted-credential',
|
|
24
|
+
)
|
|
25
|
+
expect(mint).toHaveBeenCalledWith({
|
|
26
|
+
audience: 'https://kernel.test',
|
|
27
|
+
ttlSeconds: 240,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
16
31
|
/** @evidence TEST-CLI-PLAIN-VIEW-RECEIVES-NO-CREDENTIAL */
|
|
17
32
|
test('refuses to mint a token for a handshake-none View', async () => {
|
|
18
33
|
const nonce = 'plain-view'
|
|
@@ -29,7 +44,7 @@ describe('view session server credentials', () => {
|
|
|
29
44
|
target: target('/:example.test'),
|
|
30
45
|
route: {
|
|
31
46
|
key: 'example.test:view.public',
|
|
32
|
-
declaration: { target: { kind: 'domain' }
|
|
47
|
+
declaration: { target: { kind: 'domain' } },
|
|
33
48
|
href: 'https://example.test/ui/public',
|
|
34
49
|
handshake: 'none',
|
|
35
50
|
issuer: issuer('https://example.test'),
|
|
@@ -45,12 +60,20 @@ describe('view session server credentials', () => {
|
|
|
45
60
|
issuer: 'https://kernel.test',
|
|
46
61
|
direct: true,
|
|
47
62
|
},
|
|
63
|
+
externalOrigins: ['https://connect.nango.dev'],
|
|
48
64
|
idleMs: 60_000,
|
|
49
65
|
} satisfies ViewServeConfig
|
|
50
66
|
const server = startViewServer(config)
|
|
51
67
|
await once(server, 'listening')
|
|
52
68
|
|
|
53
69
|
try {
|
|
70
|
+
const configResponse = await fetch(`http://127.0.0.1:${port}/s/${nonce}/config.json`)
|
|
71
|
+
expect(configResponse.status).toBe(200)
|
|
72
|
+
expect(await configResponse.json()).toMatchObject({
|
|
73
|
+
sessionId: 'v-plain',
|
|
74
|
+
externalOrigins: ['https://connect.nango.dev'],
|
|
75
|
+
})
|
|
76
|
+
|
|
54
77
|
const response = await fetch(`http://127.0.0.1:${port}/s/${nonce}/token`, {
|
|
55
78
|
method: 'POST',
|
|
56
79
|
})
|