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