@astrale-os/cli 1.0.0-beta.3 → 1.0.0-beta.30
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 +80 -6
- package/dist/astrale.js +55471 -58674
- package/dist/public/connect-core.js +2158 -30706
- package/dist/public/keys/index.js +1016 -29541
- package/dist/public/paths/index.js +370 -28654
- 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 +13 -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/dist/types/state/exchange-credentials.d.ts +6 -2
- package/dist/types/state/files.d.ts +2 -0
- package/dist/types/state/index.d.ts +3 -2
- package/dist/types/state/paths.d.ts +2 -0
- package/dist/types/state/session-routes.d.ts +10 -0
- package/package.json +16 -19
- 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 +47 -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 +30 -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 +6 -17
- 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 +118 -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 +50 -2
- package/src/commands/auth/logout.ts +2 -1
- package/src/commands/browser.ts +29 -0
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +71 -59
- package/src/commands/domain/install.ts +73 -46
- 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 +15 -16
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +8 -8
- 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 +53 -41
- package/src/commands/logs.ts +123 -36
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +35 -32
- package/src/commands/session/analyze.ts +26 -4
- 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 +21 -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 +89 -48
- package/src/commands/view.ts +82 -37
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +14 -5
- 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__/auth.test.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +21 -1
- package/src/connection/__tests__/errors.test.ts +27 -20
- package/src/connection/__tests__/exchange.test.ts +64 -11
- 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 +8 -4
- package/src/connection/__tests__/target.test.ts +2 -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 +110 -61
- 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 +27 -9
- 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__/fixtures/registry-journey.ts +13 -1
- package/src/identity/__tests__/registration.test.ts +1 -1
- package/src/identity/__tests__/registry.test.ts +4 -0
- package/src/identity/registration.ts +3 -3
- package/src/identity/registry.ts +2 -0
- 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__/browser-retention.test.ts +212 -0
- package/src/lib/__tests__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/config.test.ts +27 -0
- 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__/skills.test.ts +809 -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 +25 -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/browser-retention.ts +210 -0
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/config.ts +12 -1
- 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/skills/lock.ts +117 -0
- package/src/lib/skills/sync.ts +814 -0
- package/src/lib/skills.ts +1 -52
- 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/setup/steps/skills.ts +40 -38
- package/src/state/.spec/api.d.ts +21 -2
- package/src/state/.spec/architecture.md +18 -4
- package/src/state/.spec/layout.ts +1 -0
- package/src/state/__tests__/exchange-credentials.test.ts +88 -42
- package/src/state/__tests__/files.test.ts +24 -1
- package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
- package/src/state/__tests__/paths.test.ts +1 -0
- package/src/state/__tests__/session-routes.test.ts +138 -0
- package/src/state/exchange-credentials.ts +22 -9
- package/src/state/files.ts +41 -0
- package/src/state/index.ts +3 -1
- package/src/state/paths.ts +3 -0
- package/src/state/session-routes.ts +34 -0
- package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
- package/src/telemetry/__tests__/recorder.test.ts +1 -0
- package/src/telemetry/__tests__/retention.test.ts +267 -0
- package/src/telemetry/__tests__/settings.test.ts +102 -0
- package/src/telemetry/__tests__/store-scan.test.ts +128 -0
- package/src/telemetry/__tests__/trigger.test.ts +33 -4
- package/src/telemetry/analyze.ts +24 -2
- package/src/telemetry/recorder.ts +15 -4
- package/src/telemetry/retention.ts +176 -0
- package/src/telemetry/session.ts +18 -12
- package/src/telemetry/settings.ts +64 -11
- package/src/telemetry/store.ts +92 -9
- package/src/telemetry/trigger.ts +16 -28
- package/src/ui/.spec/api.d.ts +14 -0
- package/src/ui/.spec/architecture.md +14 -0
- package/src/ui/.spec/laws.ts +50 -0
- package/src/ui/.spec/layout.ts +16 -0
- package/src/ui/__tests__/ui.test.ts +1742 -0
- package/src/ui/index.ts +13 -0
- package/src/ui/lock.ts +89 -0
- package/src/ui/model.ts +83 -0
- package/src/ui/operations.ts +1044 -0
- package/src/ui/project.ts +333 -0
- package/src/ui/release.ts +398 -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-BFVFs_8x.js +81 -0
- package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
- package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
- package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
- package/studio/client/dist/index.html +4 -3
- package/studio/package.json +8 -14
- 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 +41 -32
- package/studio/server/agent/prompts/anchors.ts +23 -51
- package/studio/server/agent/prompts/system.test.ts +4 -4
- package/studio/server/agent/prompts/system.ts +5 -5
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/agent/run/preparation.ts +1 -1
- package/studio/server/api/canvas.ts +0 -1
- package/studio/server/api/context.ts +1 -1
- package/studio/server/api/deployment.ts +1 -1
- package/studio/server/api/views.ts +2 -2
- package/studio/server/api/workspace.ts +1 -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 +24 -18
- package/studio/server/cache.ts +10 -54
- package/studio/server/cli-consumers.test.ts +31 -3
- package/studio/server/domain.test.ts +50 -68
- package/studio/server/domain.ts +82 -63
- package/studio/server/handoff/copy.ts +2 -2
- 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 +10 -16
- package/studio/server/introspect/anatomy-extras.test.ts +29 -56
- 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 +155 -424
- package/studio/server/introspect/canonical-schema.ts +248 -601
- package/studio/server/introspect/core.ts +14 -22
- package/studio/server/introspect/diff.test.ts +21 -9
- package/studio/server/introspect/diff.ts +19 -64
- package/studio/server/introspect/extractor.ts +23 -122
- package/studio/server/introspect/overlay-tsmorph.test.ts +116 -241
- package/studio/server/introspect/overlay-tsmorph.ts +0 -1
- package/studio/server/introspect/overlay.ts +3 -51
- package/studio/server/introspect/revision.test.ts +24 -28
- package/studio/server/introspect/revision.ts +11 -22
- package/studio/server/introspect/runtime.test.ts +77 -195
- package/studio/server/introspect/runtime.ts +5 -67
- package/studio/server/introspect/schema-ir-json.test.ts +81 -0
- package/studio/server/introspect/schema-ir-json.ts +39 -69
- 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/lifecycle.ts +4 -1
- package/studio/server/state/baseline.test.ts +14 -9
- package/studio/server/state/baseline.ts +3 -5
- package/studio/server/state/documents.test.ts +69 -0
- package/studio/server/state/documents.ts +57 -10
- package/studio/server/state/visibility.ts +1 -5
- package/studio/server/views/model.ts +3 -3
- package/studio/server/views/target.test.ts +18 -76
- 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/updates.ts +23 -1
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/runtime.ts +4 -0
- package/studio/shared/contracts/schema.ts +44 -131
- package/studio/shared/contracts/surface.test.ts +15 -17
- package/studio/shared/contracts/workspace.ts +5 -11
- package/studio/shared/schema/identity.test.ts +20 -44
- package/studio/shared/schema/identity.ts +9 -24
- package/studio/tsconfig.json +2 -1
- package/tsconfig.json +1 -1
- package/viewer/dist/index.html +21 -0
- package/viewer/dist/main.js +41 -62
- 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
- package/studio/server/introspect/core-extractor.ts +0 -139
- package/studio/server/introspect/overlay.test.ts +0 -72
- package/studio/server/introspect/source-overlay/annotations.ts +0 -14
package/src/lib/skills.ts
CHANGED
|
@@ -3,62 +3,11 @@ import { homedir } from 'node:os'
|
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { findAgentBrowser } from './browser'
|
|
6
|
-
import { run } from './proc'
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
* Agent skills and the agent-browser tool are owned by the coding-agent harness
|
|
10
|
-
* (Claude Code et al.) and by npm — not by this CLI. `astrale setup` and
|
|
11
|
-
* `astrale update` only *detect* them and delegate installation to their real
|
|
12
|
-
* installers (`npx skills add`, `npm i -g agent-browser`). This module is that
|
|
13
|
-
* detection + delegation layer.
|
|
14
|
-
*/
|
|
7
|
+
export * from './skills/sync'
|
|
15
8
|
|
|
16
|
-
/** The skill name the agent harness looks up under `<dir>/<name>/SKILL.md`. */
|
|
17
|
-
export const ASTRALE_CLI_SKILL = 'astrale-cli'
|
|
18
|
-
export const ASTRALE_DOMAIN_SKILL = 'astrale-domain'
|
|
19
9
|
export const AGENT_BROWSER_SKILL = 'agent-browser'
|
|
20
10
|
|
|
21
|
-
/**
|
|
22
|
-
* Published skill source consumed by `npx skills add`. The public `astrale-os/cli`
|
|
23
|
-
* repo hosts BOTH the astrale-cli and astrale-domain skills (under `skills/`), so
|
|
24
|
-
* one `npx skills add astrale-os/cli` installs both; address one with
|
|
25
|
-
* `astrale-os/cli@astrale-cli` or `astrale-os/cli@astrale-domain`.
|
|
26
|
-
*/
|
|
27
|
-
export const ASTRALE_CLI_SKILL_SOURCE = 'astrale-os/cli'
|
|
28
|
-
|
|
29
|
-
/** Human-facing command that installs (or refreshes) both astrale skills, globally. */
|
|
30
|
-
export const SKILL_INSTALL_HINT = `npx skills add ${ASTRALE_CLI_SKILL_SOURCE} -g`
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Install or refresh the astrale agent skills by delegating to the skill package
|
|
34
|
-
* manager (`npx skills add`). We install GLOBALLY (`-g`, user-level): one run
|
|
35
|
-
* equips every project on the machine — the harness resolves the skills from the
|
|
36
|
-
* user Claude dir (see {@link detectSkill}) regardless of cwd, and installs land
|
|
37
|
-
* in `~/.agents/skills` rather than clobbering any project's own `.agents/skills`
|
|
38
|
-
* (notably this repo's symlinked source). We don't reimplement skill installation
|
|
39
|
-
* — re-running the real installer is also how an existing install updates to the
|
|
40
|
-
* latest published SKILL.md, so this doubles as the update path.
|
|
41
|
-
*
|
|
42
|
-
* We CAPTURE the installer's output rather than stream it, and surface it only on
|
|
43
|
-
* failure. A global install reports a benign per-agent note for any agent format
|
|
44
|
-
* that can't go user-level (e.g. PromptScript: "does not support global skill
|
|
45
|
-
* installation") as a "Failed to install N" banner — even though `npx skills`
|
|
46
|
-
* still exits 0 and the skills land for every other agent. Streaming that made a
|
|
47
|
-
* successful refresh look broken; suppressing it on success lets the caller print
|
|
48
|
-
* a clean line. Resolves true on exit 0. Requires Node/`npx` on PATH + network.
|
|
49
|
-
*/
|
|
50
|
-
export async function installSkills(): Promise<boolean> {
|
|
51
|
-
const { code, stdout, stderr } = await run('npx', [
|
|
52
|
-
'skills',
|
|
53
|
-
'add',
|
|
54
|
-
ASTRALE_CLI_SKILL_SOURCE,
|
|
55
|
-
'-g',
|
|
56
|
-
'-y',
|
|
57
|
-
])
|
|
58
|
-
if (code !== 0) process.stderr.write(stdout + stderr)
|
|
59
|
-
return code === 0
|
|
60
|
-
}
|
|
61
|
-
|
|
62
11
|
/**
|
|
63
12
|
* The directories the agent harness ACTUALLY loads skills from, in resolution
|
|
64
13
|
* order: a project `.claude/skills` found by walking UP from cwd (the harness
|
package/src/lib/update.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
chmod,
|
|
4
|
+
copyFile,
|
|
5
|
+
mkdir,
|
|
6
|
+
mkdtemp,
|
|
7
|
+
readFile,
|
|
8
|
+
realpath,
|
|
9
|
+
rename,
|
|
10
|
+
rm,
|
|
11
|
+
writeFile,
|
|
12
|
+
} from 'node:fs/promises'
|
|
3
13
|
import { tmpdir } from 'node:os'
|
|
4
14
|
import { dirname, join } from 'node:path'
|
|
5
15
|
import { z } from 'zod'
|
|
@@ -61,9 +71,26 @@ export type UpdateRequest = {
|
|
|
61
71
|
currentVersion: string
|
|
62
72
|
platform?: Platform
|
|
63
73
|
installPath?: string
|
|
74
|
+
execution?: UpdateExecution
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
export type UpdateExecution =
|
|
78
|
+
| { kind: 'standalone'; executable: string }
|
|
79
|
+
| { kind: 'package-managed'; executable: string }
|
|
80
|
+
|
|
81
|
+
const admittedScriptInstall = Symbol('admittedScriptInstall')
|
|
82
|
+
export type AdmittedScriptInstall = Readonly<{
|
|
83
|
+
metadata: InstallMetadata
|
|
84
|
+
executable: string
|
|
85
|
+
[admittedScriptInstall]: true
|
|
86
|
+
}>
|
|
87
|
+
|
|
66
88
|
export type UpdateResult =
|
|
89
|
+
| {
|
|
90
|
+
status: 'managed'
|
|
91
|
+
currentVersion: string
|
|
92
|
+
executable: string
|
|
93
|
+
}
|
|
67
94
|
| {
|
|
68
95
|
status: 'up-to-date'
|
|
69
96
|
currentVersion: string
|
|
@@ -106,50 +133,138 @@ export function platformKey(platform: Platform): string {
|
|
|
106
133
|
return `${platform.os}-${platform.arch}`
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
136
|
+
export function classifyUpdateExecution(input: {
|
|
137
|
+
bunVersion?: string
|
|
138
|
+
executable: string
|
|
139
|
+
entry?: string
|
|
140
|
+
}): UpdateExecution {
|
|
141
|
+
return input.bunVersion && input.entry?.startsWith('/$bunfs/')
|
|
142
|
+
? { kind: 'standalone', executable: input.executable }
|
|
143
|
+
: { kind: 'package-managed', executable: input.executable }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function detectUpdateExecution(): UpdateExecution {
|
|
147
|
+
return classifyUpdateExecution({
|
|
148
|
+
bunVersion: (process.versions as { bun?: string }).bun,
|
|
149
|
+
executable: process.execPath,
|
|
150
|
+
entry: process.argv[1],
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function packageManagedUpdateError(executable: string): AstraleError {
|
|
155
|
+
return new AstraleError(
|
|
156
|
+
'UPDATE_PACKAGE_MANAGED',
|
|
157
|
+
'This Astrale build is managed by your package manager.',
|
|
158
|
+
`Active binary: ${executable}. Update it with npm, pnpm, or bun; or put the official script installation first on PATH.`,
|
|
159
|
+
)
|
|
116
160
|
}
|
|
117
161
|
|
|
118
162
|
export async function readInstallMetadata(path = INSTALL_PATH): Promise<InstallMetadata> {
|
|
119
163
|
let raw: string
|
|
120
164
|
try {
|
|
121
165
|
raw = await readFile(path, 'utf8')
|
|
122
|
-
} catch {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
: new AstraleError(
|
|
130
|
-
'UPDATE_PACKAGE_MANAGED',
|
|
131
|
-
'This Astrale build is managed by your package manager.',
|
|
132
|
-
'Update with: npm install -g @astrale-os/cli@latest (or pnpm/bun)',
|
|
133
|
-
)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!isMissingFile(error)) throw error
|
|
168
|
+
throw new AstraleError(
|
|
169
|
+
'UPDATE_NOT_SCRIPT_INSTALLED',
|
|
170
|
+
'Astrale was not installed by the official install script.',
|
|
171
|
+
'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
|
|
172
|
+
)
|
|
134
173
|
}
|
|
135
174
|
|
|
136
|
-
|
|
175
|
+
let decoded: unknown
|
|
176
|
+
try {
|
|
177
|
+
decoded = JSON.parse(raw)
|
|
178
|
+
} catch {
|
|
179
|
+
throw badInstallMetadata(path)
|
|
180
|
+
}
|
|
181
|
+
const parsed = InstallMetadataSchema.safeParse(decoded)
|
|
137
182
|
if (!parsed.success) {
|
|
183
|
+
throw badInstallMetadata(path)
|
|
184
|
+
}
|
|
185
|
+
return parsed.data
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isMissingFile(error: unknown): error is NodeJS.ErrnoException {
|
|
189
|
+
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function badInstallMetadata(path: string): AstraleError {
|
|
193
|
+
return new AstraleError(
|
|
194
|
+
'UPDATE_BAD_INSTALL_METADATA',
|
|
195
|
+
`Invalid install metadata at ${path}.`,
|
|
196
|
+
'Reinstall with: curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh',
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function admitScriptInstall(
|
|
201
|
+
meta: InstallMetadata,
|
|
202
|
+
execution: Extract<UpdateExecution, { kind: 'standalone' }>,
|
|
203
|
+
): Promise<AdmittedScriptInstall> {
|
|
204
|
+
const [running, recorded] = await Promise.all([
|
|
205
|
+
realpathIfExists(execution.executable),
|
|
206
|
+
realpathIfExists(meta.bin),
|
|
207
|
+
])
|
|
208
|
+
if (running === undefined || recorded === undefined || running !== recorded) {
|
|
138
209
|
throw new AstraleError(
|
|
139
|
-
'
|
|
140
|
-
|
|
141
|
-
|
|
210
|
+
'UPDATE_INSTALL_MISMATCH',
|
|
211
|
+
'The running Astrale binary does not own the recorded script installation.',
|
|
212
|
+
`Running binary: ${execution.executable}; recorded binary: ${meta.bin}. Refusing to replace a different installation.`,
|
|
142
213
|
)
|
|
143
214
|
}
|
|
144
|
-
return
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
metadata: meta,
|
|
217
|
+
executable: running,
|
|
218
|
+
[admittedScriptInstall]: true as const,
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function realpathIfExists(path: string): Promise<string | undefined> {
|
|
223
|
+
try {
|
|
224
|
+
return await realpath(path)
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (isMissingFile(error)) return undefined
|
|
227
|
+
throw error
|
|
228
|
+
}
|
|
145
229
|
}
|
|
146
230
|
|
|
147
231
|
export async function writeInstallMetadata(
|
|
148
232
|
meta: InstallMetadata,
|
|
149
233
|
path = INSTALL_PATH,
|
|
234
|
+
filesystem: Pick<CohortFilesystem, 'mkdir' | 'rename' | 'rm'> &
|
|
235
|
+
Pick<typeof import('node:fs/promises'), 'writeFile'> = { mkdir, rename, rm, writeFile },
|
|
150
236
|
): Promise<void> {
|
|
151
|
-
|
|
152
|
-
|
|
237
|
+
const staged = `${path}.next`
|
|
238
|
+
const previous = `${path}.previous`
|
|
239
|
+
await filesystem.mkdir(dirname(path), { recursive: true })
|
|
240
|
+
await filesystem.rm(staged, { force: true })
|
|
241
|
+
await filesystem.rm(previous, { force: true })
|
|
242
|
+
await filesystem.writeFile(staged, JSON.stringify(meta, null, 2) + '\n')
|
|
243
|
+
|
|
244
|
+
let backedUp = false
|
|
245
|
+
try {
|
|
246
|
+
try {
|
|
247
|
+
await filesystem.rename(path, previous)
|
|
248
|
+
backedUp = true
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (!isMissingFile(error)) throw error
|
|
251
|
+
}
|
|
252
|
+
await filesystem.rename(staged, path)
|
|
253
|
+
await filesystem.rm(previous, { force: true })
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const rollback: unknown[] = []
|
|
256
|
+
if (backedUp) {
|
|
257
|
+
await filesystem.rename(previous, path).catch((failure) => rollback.push(failure))
|
|
258
|
+
}
|
|
259
|
+
await filesystem.rm(staged, { force: true }).catch((failure) => rollback.push(failure))
|
|
260
|
+
if (rollback.length > 0) {
|
|
261
|
+
throw new AggregateError(
|
|
262
|
+
[error, ...rollback],
|
|
263
|
+
'Install metadata update and rollback both failed.',
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
throw error
|
|
267
|
+
}
|
|
153
268
|
}
|
|
154
269
|
|
|
155
270
|
export function releaseBase(
|
|
@@ -174,8 +289,138 @@ export function shouldUpdate(currentVersion: string, manifestVersion: string): b
|
|
|
174
289
|
return currentVersion !== manifestVersion
|
|
175
290
|
}
|
|
176
291
|
|
|
177
|
-
|
|
178
|
-
|
|
292
|
+
interface CohortFilesystem {
|
|
293
|
+
readonly chmod: typeof chmod
|
|
294
|
+
readonly copyFile: typeof copyFile
|
|
295
|
+
readonly mkdir: typeof mkdir
|
|
296
|
+
readonly rename: typeof rename
|
|
297
|
+
readonly rm: typeof rm
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const defaultCohortFilesystem: CohortFilesystem = { chmod, copyFile, mkdir, rename, rm }
|
|
301
|
+
|
|
302
|
+
export interface StandaloneCohortReplacement {
|
|
303
|
+
readonly finalize: () => Promise<void>
|
|
304
|
+
readonly rollback: () => Promise<void>
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Replace the standalone executable and its Viewer as one rollback-safe cohort. */
|
|
308
|
+
export async function replaceStandaloneCohort(
|
|
309
|
+
installedBinary: string,
|
|
310
|
+
nextBinary: string,
|
|
311
|
+
nextViewerDist: string,
|
|
312
|
+
filesystem: Partial<CohortFilesystem> = {},
|
|
313
|
+
): Promise<StandaloneCohortReplacement> {
|
|
314
|
+
const fs = { ...defaultCohortFilesystem, ...filesystem }
|
|
315
|
+
const binDirectory = dirname(installedBinary)
|
|
316
|
+
const previousBinary = `${installedBinary}.previous`
|
|
317
|
+
const stagedBinary = `${installedBinary}.next`
|
|
318
|
+
const viewer = join(binDirectory, 'viewer')
|
|
319
|
+
const previousViewer = join(binDirectory, 'viewer.previous')
|
|
320
|
+
const stagedViewer = join(binDirectory, 'viewer.next')
|
|
321
|
+
const stagedViewerDist = join(stagedViewer, 'dist')
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await fs.rm(stagedViewer, { recursive: true, force: true })
|
|
325
|
+
await fs.mkdir(stagedViewerDist, { recursive: true })
|
|
326
|
+
await fs.copyFile(join(nextViewerDist, 'main.js'), join(stagedViewerDist, 'main.js'))
|
|
327
|
+
await fs.copyFile(join(nextViewerDist, 'index.html'), join(stagedViewerDist, 'index.html'))
|
|
328
|
+
await fs.copyFile(installedBinary, previousBinary)
|
|
329
|
+
await fs.copyFile(nextBinary, stagedBinary)
|
|
330
|
+
await fs.chmod(stagedBinary, 0o755)
|
|
331
|
+
} catch (error) {
|
|
332
|
+
await fs.rm(stagedViewer, { recursive: true, force: true }).catch(() => undefined)
|
|
333
|
+
await fs.rm(stagedBinary, { force: true }).catch(() => undefined)
|
|
334
|
+
throw error
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await fs.rm(previousViewer, { recursive: true, force: true })
|
|
338
|
+
let viewerBackedUp = false
|
|
339
|
+
let viewerCommitted = false
|
|
340
|
+
let binaryCommitted = false
|
|
341
|
+
try {
|
|
342
|
+
try {
|
|
343
|
+
await fs.rename(viewer, previousViewer)
|
|
344
|
+
viewerBackedUp = true
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
347
|
+
}
|
|
348
|
+
await fs.rename(stagedViewer, viewer)
|
|
349
|
+
viewerCommitted = true
|
|
350
|
+
await fs.rename(stagedBinary, installedBinary)
|
|
351
|
+
binaryCommitted = true
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const rollback: unknown[] = []
|
|
354
|
+
if (binaryCommitted) {
|
|
355
|
+
await fs.copyFile(previousBinary, installedBinary).catch((failure) => rollback.push(failure))
|
|
356
|
+
await fs.chmod(installedBinary, 0o755).catch((failure) => rollback.push(failure))
|
|
357
|
+
}
|
|
358
|
+
if (viewerCommitted) {
|
|
359
|
+
await fs
|
|
360
|
+
.rm(viewer, { recursive: true, force: true })
|
|
361
|
+
.catch((failure) => rollback.push(failure))
|
|
362
|
+
}
|
|
363
|
+
if (viewerBackedUp) {
|
|
364
|
+
await fs.rename(previousViewer, viewer).catch((failure) => rollback.push(failure))
|
|
365
|
+
}
|
|
366
|
+
if (rollback.length > 0) {
|
|
367
|
+
throw new AggregateError([error, ...rollback], 'Standalone update and rollback both failed.')
|
|
368
|
+
}
|
|
369
|
+
throw error
|
|
370
|
+
} finally {
|
|
371
|
+
await fs.rm(stagedViewer, { recursive: true, force: true }).catch(() => undefined)
|
|
372
|
+
await fs.rm(stagedBinary, { force: true }).catch(() => undefined)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let settled = false
|
|
376
|
+
return Object.freeze({
|
|
377
|
+
finalize: async () => {
|
|
378
|
+
if (settled) return
|
|
379
|
+
settled = true
|
|
380
|
+
await fs.rm(previousViewer, { recursive: true, force: true }).catch(() => undefined)
|
|
381
|
+
},
|
|
382
|
+
rollback: async () => {
|
|
383
|
+
if (settled) return
|
|
384
|
+
settled = true
|
|
385
|
+
const rollback: unknown[] = []
|
|
386
|
+
await fs.copyFile(previousBinary, installedBinary).catch((failure) => rollback.push(failure))
|
|
387
|
+
await fs.chmod(installedBinary, 0o755).catch((failure) => rollback.push(failure))
|
|
388
|
+
await fs
|
|
389
|
+
.rm(viewer, { recursive: true, force: true })
|
|
390
|
+
.catch((failure) => rollback.push(failure))
|
|
391
|
+
if (viewerBackedUp) {
|
|
392
|
+
await fs.rename(previousViewer, viewer).catch((failure) => rollback.push(failure))
|
|
393
|
+
}
|
|
394
|
+
if (rollback.length > 0) {
|
|
395
|
+
throw new AggregateError(rollback, 'Standalone update rollback failed.')
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
interface UpdateDependencies {
|
|
402
|
+
readonly replaceStandaloneCohort: typeof replaceStandaloneCohort
|
|
403
|
+
readonly writeInstallMetadata: typeof writeInstallMetadata
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const defaultUpdateDependencies = Object.freeze({ replaceStandaloneCohort, writeInstallMetadata })
|
|
407
|
+
|
|
408
|
+
export async function updateAstrale(
|
|
409
|
+
req: UpdateRequest,
|
|
410
|
+
dependencies: Partial<UpdateDependencies> = {},
|
|
411
|
+
): Promise<UpdateResult> {
|
|
412
|
+
const update = { ...defaultUpdateDependencies, ...dependencies }
|
|
413
|
+
const execution = req.execution ?? detectUpdateExecution()
|
|
414
|
+
if (execution.kind === 'package-managed') {
|
|
415
|
+
return {
|
|
416
|
+
status: 'managed',
|
|
417
|
+
currentVersion: req.currentVersion,
|
|
418
|
+
executable: execution.executable,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const install = await admitScriptInstall(await readInstallMetadata(req.installPath), execution)
|
|
423
|
+
const meta = install.metadata
|
|
179
424
|
const currentVersion = meta.version ?? req.currentVersion
|
|
180
425
|
const channel = req.channel ?? meta.channel ?? DEFAULT_UPDATE_CHANNEL
|
|
181
426
|
const platform = req.platform ?? detectPlatform()
|
|
@@ -226,25 +471,33 @@ export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
|
|
|
226
471
|
|
|
227
472
|
await extractTarGz(archive, tmp)
|
|
228
473
|
const nextBin = join(tmp, 'astrale')
|
|
474
|
+
const nextViewer = join(tmp, 'viewer', 'dist')
|
|
229
475
|
await chmod(nextBin, 0o755)
|
|
230
476
|
await smokeVersion(nextBin, manifest.binaryVersion ?? manifest.version)
|
|
231
477
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
478
|
+
const replacement = await update.replaceStandaloneCohort(meta.bin, nextBin, nextViewer)
|
|
479
|
+
try {
|
|
480
|
+
await update.writeInstallMetadata(
|
|
481
|
+
{
|
|
482
|
+
...meta,
|
|
483
|
+
channel: manifest.channel,
|
|
484
|
+
version: manifest.version,
|
|
485
|
+
installedAt: new Date().toISOString(),
|
|
486
|
+
},
|
|
487
|
+
req.installPath,
|
|
488
|
+
)
|
|
489
|
+
} catch (error) {
|
|
490
|
+
try {
|
|
491
|
+
await replacement.rollback()
|
|
492
|
+
} catch (rollbackError) {
|
|
493
|
+
throw new AggregateError(
|
|
494
|
+
[error, rollbackError],
|
|
495
|
+
'Standalone update metadata commit and cohort rollback both failed.',
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
throw error
|
|
499
|
+
}
|
|
500
|
+
await replacement.finalize()
|
|
248
501
|
|
|
249
502
|
return {
|
|
250
503
|
status: 'updated',
|
package/src/lib/validation.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDnsLabel } from '@astrale-os/sdk/
|
|
1
|
+
import { isDnsLabel } from '@astrale-os/sdk/value'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
|
|
4
4
|
import { ReservedSlugError } from '../errors'
|
|
@@ -21,7 +21,7 @@ export function validateName(name: string, entity: string): void {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function validateSlug(slug: string): void {
|
|
24
|
-
// Canonical DNS-label rule
|
|
24
|
+
// Canonical Core DNS-label rule: a slug becomes a
|
|
25
25
|
// hostname label, so reject anything not DNS-safe (§4.7).
|
|
26
26
|
if (!slug || !isDnsLabel(slug)) {
|
|
27
27
|
throw new Error(
|
package/src/lib/view/assets.ts
CHANGED
|
@@ -8,7 +8,11 @@ import { fileURLToPath } from 'node:url'
|
|
|
8
8
|
* (`<pkg>/viewer/dist`). The module URL is authoritative because npm global
|
|
9
9
|
* installs expose the CLI through a bin symlink outside the package root.
|
|
10
10
|
*/
|
|
11
|
-
export function viewerDistDir(
|
|
11
|
+
export function viewerDistDir(
|
|
12
|
+
moduleUrl = import.meta.url,
|
|
13
|
+
entry = process.argv[1] ?? '.',
|
|
14
|
+
executable = process.execPath,
|
|
15
|
+
): string {
|
|
12
16
|
const override = process.env.ASTRALE_VIEWER_DIR
|
|
13
17
|
if (override) return override
|
|
14
18
|
|
|
@@ -16,7 +20,12 @@ export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[
|
|
|
16
20
|
const published = join(moduleDirectory, '..', 'viewer', 'dist')
|
|
17
21
|
const source = join(moduleDirectory, '..', '..', '..', 'viewer', 'dist')
|
|
18
22
|
const legacy = join(dirname(entry), '..', 'viewer', 'dist')
|
|
19
|
-
const
|
|
23
|
+
const standalone = entry.startsWith('/$bunfs/')
|
|
24
|
+
? join(dirname(executable), 'viewer', 'dist')
|
|
25
|
+
: undefined
|
|
26
|
+
const complete = [standalone, published, source, legacy].find(
|
|
27
|
+
(candidate): candidate is string => candidate !== undefined && hasViewerBundle(candidate),
|
|
28
|
+
)
|
|
20
29
|
if (complete) return complete
|
|
21
30
|
|
|
22
31
|
// A source checkout may intentionally omit generated dist assets. Keep the
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExternalOpenRequest, IntentMessage, Shell } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
import { replyToIntent } from '@astrale-os/shell'
|
|
4
|
+
|
|
5
|
+
export interface ExternalOpenIntentHost {
|
|
6
|
+
open(request: ExternalOpenRequest): boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Register the root host's browser-owned external navigation effect. */
|
|
10
|
+
export function installExternalOpenIntentHandler(
|
|
11
|
+
shell: Shell,
|
|
12
|
+
host: ExternalOpenIntentHost,
|
|
13
|
+
): () => void {
|
|
14
|
+
return shell.onIntent('browser.openExternal', (message) => {
|
|
15
|
+
const opened = host.open(message.envelope.payload)
|
|
16
|
+
replyToIntent(shell.children, message.envelope.sender.windowId, message, {
|
|
17
|
+
outcome: opened ? 'opened' : 'blocked',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Open an external document without retaining a cross-origin opener capability. */
|
|
23
|
+
export function openExternalBrowserWindow(
|
|
24
|
+
browser: Pick<Window, 'open'>,
|
|
25
|
+
request: ExternalOpenRequest,
|
|
26
|
+
): boolean {
|
|
27
|
+
const popup = request.mode === 'popup'
|
|
28
|
+
const opened = browser.open('', '_blank', popup ? 'popup,width=720,height=760' : undefined)
|
|
29
|
+
if (opened === null) return false
|
|
30
|
+
try {
|
|
31
|
+
opened.opener = null
|
|
32
|
+
if (opened.opener !== null) throw new Error('Browser retained the opener capability.')
|
|
33
|
+
opened.location.replace(request.url)
|
|
34
|
+
} catch {
|
|
35
|
+
opened.close()
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ExternalOpenIntentMessage = IntentMessage<'browser.openExternal'>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AstraleError } from '../../errors.js'
|
|
2
|
+
|
|
3
|
+
/** Admit exact HTTPS origins explicitly granted by the CLI operator. */
|
|
4
|
+
export function admitExternalOpenOrigins(input: readonly string[] | undefined): readonly string[] {
|
|
5
|
+
if (input === undefined) return Object.freeze([])
|
|
6
|
+
const admitted = new Set<string>()
|
|
7
|
+
for (const candidate of input) {
|
|
8
|
+
let url: URL
|
|
9
|
+
try {
|
|
10
|
+
url = new URL(candidate)
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
throw invalidExternalOrigin(candidate, cause)
|
|
13
|
+
}
|
|
14
|
+
if (
|
|
15
|
+
url.protocol !== 'https:' ||
|
|
16
|
+
url.hostname.includes('*') ||
|
|
17
|
+
url.username !== '' ||
|
|
18
|
+
url.password !== '' ||
|
|
19
|
+
url.pathname !== '/' ||
|
|
20
|
+
url.search !== '' ||
|
|
21
|
+
url.hash !== ''
|
|
22
|
+
) {
|
|
23
|
+
throw invalidExternalOrigin(candidate)
|
|
24
|
+
}
|
|
25
|
+
admitted.add(url.origin)
|
|
26
|
+
}
|
|
27
|
+
return Object.freeze([...admitted])
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function invalidExternalOrigin(candidate: string, cause?: unknown): AstraleError {
|
|
31
|
+
return new AstraleError(
|
|
32
|
+
'INVALID_EXTERNAL_ORIGIN',
|
|
33
|
+
`External navigation grant "${candidate}" is not an exact HTTPS origin.`,
|
|
34
|
+
'Pass an origin such as https://connect.example.com with no credentials, path, query, or fragment.',
|
|
35
|
+
cause === undefined ? undefined : { cause },
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HostCapabilities } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
import { admitHostCapabilities } from '@astrale-os/shell'
|
|
4
|
+
|
|
5
|
+
/** Build the exact host grant handed to a CLI-hosted View session. */
|
|
6
|
+
export function viewHostCapabilities(externalOrigins: readonly string[]): HostCapabilities {
|
|
7
|
+
return admitHostCapabilities({
|
|
8
|
+
version: 1,
|
|
9
|
+
navigation: {
|
|
10
|
+
openView: {},
|
|
11
|
+
...(externalOrigins.length === 0 ? {} : { external: { origins: externalOrigins } }),
|
|
12
|
+
},
|
|
13
|
+
actions: {},
|
|
14
|
+
browser: {},
|
|
15
|
+
access: {},
|
|
16
|
+
})
|
|
17
|
+
}
|
package/src/lib/view/resolve.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedView as SessionResolvedView } from '@astrale-os/
|
|
1
|
+
import type { ResolvedView as SessionResolvedView } from '@astrale-os/sdk/client/session'
|
|
2
2
|
import type { ResolvedView } from '@astrale-os/shell'
|
|
3
3
|
|
|
4
4
|
import { Path } from '@astrale-os/sdk/graph/path'
|
package/src/lib/view/server.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { AuthApi } from '@astrale-os/sdk/auth'
|
|
2
|
+
import type { IssuerId } from '@astrale-os/sdk/auth'
|
|
1
3
|
import type { ReadableStream as WebReadableStream } from 'node:stream/web'
|
|
2
4
|
|
|
3
5
|
import { readFile } from 'node:fs/promises'
|
|
@@ -23,8 +25,9 @@ export { ensureViewerAssets, viewerDistDir } from './assets'
|
|
|
23
25
|
* heartbeat keeps a live session alive.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
+
const VIEW_TOKEN_TTL_SECONDS = 4 * 60
|
|
29
|
+
const TOKEN_REFRESH_MARGIN_MS = 60_000
|
|
30
|
+
const FALLBACK_TOKEN_TTL_MS = VIEW_TOKEN_TTL_SECONDS * 1_000
|
|
28
31
|
const IDLE_SWEEP_MS = 60_000
|
|
29
32
|
|
|
30
33
|
export type PageStatus = { state: string; error?: string; at: string }
|
|
@@ -44,7 +47,7 @@ export function startViewServer(config: ViewServeConfig): Server {
|
|
|
44
47
|
async function freshGrant(): Promise<TokenGrant> {
|
|
45
48
|
if (grant && grant.expiresAt - Date.now() > TOKEN_REFRESH_MARGIN_MS) return grant
|
|
46
49
|
grant = await withClientSession(config.kernel, async ({ auth, target }) => {
|
|
47
|
-
const token = await auth
|
|
50
|
+
const token = await mintViewCredential(auth, target.kernelIssuer)
|
|
48
51
|
return {
|
|
49
52
|
token,
|
|
50
53
|
expiresAt: jwtExpiry(token) ?? Date.now() + FALLBACK_TOKEN_TTL_MS,
|
|
@@ -98,6 +101,7 @@ export function startViewServer(config: ViewServeConfig): Server {
|
|
|
98
101
|
identity: session.identity ?? null,
|
|
99
102
|
instance: session.instance ?? null,
|
|
100
103
|
sessionId: session.id,
|
|
104
|
+
externalOrigins: config.externalOrigins,
|
|
101
105
|
})
|
|
102
106
|
return
|
|
103
107
|
}
|
|
@@ -182,6 +186,17 @@ export function startViewServer(config: ViewServeConfig): Server {
|
|
|
182
186
|
return server
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
/** Mint a short-lived credential from the selected CLI identity proof. */
|
|
190
|
+
export async function mintViewCredential(
|
|
191
|
+
auth: Pick<AuthApi, 'mint'>,
|
|
192
|
+
audience: IssuerId,
|
|
193
|
+
): Promise<string> {
|
|
194
|
+
return auth.mint({
|
|
195
|
+
audience,
|
|
196
|
+
ttlSeconds: VIEW_TOKEN_TTL_SECONDS,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
function jwtExpiry(token: string): number | null {
|
|
186
201
|
try {
|
|
187
202
|
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8')) as {
|
package/src/lib/view/session.ts
CHANGED
|
@@ -41,6 +41,8 @@ export type ViewServeConfig = {
|
|
|
41
41
|
* handles CORS and self-signed local CAs.
|
|
42
42
|
*/
|
|
43
43
|
proxy: { kernelUrl: string; issuer: string; caFile?: string; direct: boolean }
|
|
44
|
+
/** Exact HTTPS origins the operator consented to open from this View session. */
|
|
45
|
+
externalOrigins: readonly string[]
|
|
44
46
|
idleMs: number
|
|
45
47
|
}
|
|
46
48
|
|