@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.11
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 +23 -12
- package/dist/astrale.js +48048 -56798
- package/dist/public/connect-core.js +2011 -30765
- package/dist/public/keys/index.js +884 -29483
- package/dist/public/paths/index.js +13101 -41459
- package/dist/types/admin/binding.d.ts +19 -0
- package/dist/types/admin/instance/client.d.ts +22 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +5 -12
- 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 +26 -168
- 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/meta.d.ts +2 -2
- 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 +91 -0
- package/dist/types/lib/validation.d.ts +4 -1
- package/package.json +16 -19
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/__tests__/binding.test.ts +31 -0
- package/src/admin/__tests__/fixture.ts +101 -0
- package/src/admin/binding.ts +98 -0
- package/src/admin/catalog/.spec/api.d.ts +2 -2
- package/src/admin/catalog/__tests__/client.test.ts +20 -49
- package/src/admin/catalog/client.ts +36 -32
- package/src/admin/catalog/model.ts +3 -4
- 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 +2 -2
- package/src/admin/instance/__tests__/client.test.ts +39 -164
- package/src/admin/instance/client.ts +35 -124
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +11 -20
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
- package/src/commands/__tests__/domain-list.test.ts +63 -4
- package/src/commands/__tests__/install-identity-override.test.ts +19 -19
- 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__/read-commands.test.ts +35 -10
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +48 -19
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +43 -56
- package/src/commands/domain/install.ts +13 -17
- package/src/commands/domain/list.ts +18 -13
- 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 +9 -16
- package/src/commands/logs.ts +24 -25
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +27 -30
- package/src/commands/studio.ts +20 -0
- package/src/commands/token.ts +8 -7
- package/src/commands/update.ts +38 -14
- package/src/commands/view.ts +7 -6
- 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 +43 -32
- package/src/connection/__tests__/exchange.test.ts +36 -6
- 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 +52 -27
- 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__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/domain-publication.test.ts +138 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +36 -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__/instance.test.ts +6 -0
- package/src/lib/__tests__/log-errors.test.ts +64 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +113 -8
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +18 -3
- package/src/lib/__tests__/view-session.test.ts +1 -1
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/admin-target.ts +21 -2
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/domain-publication.ts +104 -0
- package/src/lib/failure-debug.ts +26 -0
- package/src/lib/idp.ts +39 -4
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/instance.ts +22 -12
- package/src/lib/log.ts +16 -9
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/provision-instance.ts +34 -57
- package/src/lib/update.ts +124 -30
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/resolve.ts +1 -1
- package/src/lib/view/server.ts +17 -3
- package/src/program/__tests__/program.test.ts +20 -4
- package/src/program/build.ts +2 -2
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- 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 +10 -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/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- 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 +6 -5
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +43 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +122 -0
- package/studio/server/cache.ts +162 -11
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +50 -40
- package/studio/server/domain.ts +83 -52
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/routes.ts +114 -0
- package/studio/server/introspect/anatomy/views.ts +53 -0
- package/studio/server/introspect/anatomy-extras.test.ts +106 -35
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +52 -28
- package/studio/server/introspect/bundle.ts +15 -9
- package/studio/server/introspect/canonical-schema.test.ts +93 -309
- package/studio/server/introspect/canonical-schema.ts +257 -425
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/core-extractor.ts +18 -127
- package/studio/server/introspect/core.ts +1 -2
- package/studio/server/introspect/diff.test.ts +18 -18
- package/studio/server/introspect/diff.ts +44 -90
- package/studio/server/introspect/extractor.ts +23 -116
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/overlay.test.ts +26 -54
- package/studio/server/introspect/overlay.ts +10 -37
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +83 -143
- package/studio/server/introspect/runtime.ts +40 -11
- package/studio/server/introspect/schema-ir-json.test.ts +70 -0
- package/studio/server/introspect/schema-ir-json.ts +151 -0
- 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/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +161 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +330 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +223 -7
- package/studio/server/state/baseline.ts +148 -34
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -11
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/views/target.test.ts +78 -0
- package/studio/server/views/target.ts +222 -0
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +9 -5
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +393 -0
- package/studio/shared/contracts/surface.test.ts +317 -0
- package/studio/shared/contracts/workspace.ts +265 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +34 -0
- package/studio/shared/schema/identity.ts +68 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +40 -61
- package/.check-workspace.cjs +0 -40
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
- package/studio/client/dist/assets/elk-api-D2xgMJvi.js +0 -1
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.test.ts +0 -286
- package/studio/server/state/views.ts +0 -535
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { handleApi } from './api'
|
|
7
|
+
import { registerDomain, unregisterDomain } from './domain'
|
|
8
|
+
|
|
9
|
+
const roots: string[] = []
|
|
10
|
+
const domainIds: string[] = []
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
while (domainIds.length) unregisterDomain(domainIds.pop()!)
|
|
14
|
+
while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true })
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function fixture() {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), 'studio-api-contract-'))
|
|
19
|
+
roots.push(root)
|
|
20
|
+
mkdirSync(join(root, 'schema'))
|
|
21
|
+
writeFileSync(join(root, 'astrale.config.ts'), 'export default {}\n')
|
|
22
|
+
writeFileSync(join(root, 'application.ts'), 'export default {}\n')
|
|
23
|
+
writeFileSync(join(root, 'schema/index.ts'), 'export const Test = {}\n')
|
|
24
|
+
const handle = registerDomain(root)!
|
|
25
|
+
domainIds.push(handle.id)
|
|
26
|
+
return handle
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function route(path: string, init?: RequestInit) {
|
|
30
|
+
const url = new URL(`http://127.0.0.1${path}`)
|
|
31
|
+
return handleApi(new Request(url, init), url, () => {})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('router ignores non-api paths and returns the stable JSON 404 for unknown API paths', async () => {
|
|
35
|
+
expect(await route('/assets/app.js')).toBeNull()
|
|
36
|
+
|
|
37
|
+
const response = await route('/api/unknown')
|
|
38
|
+
expect(response?.status).toBe(404)
|
|
39
|
+
expect(response?.headers.get('content-type')).toBe('application/json')
|
|
40
|
+
expect(await response?.json()).toEqual({ error: 'not found' })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('router blocks cross-site mutations before route dispatch but permits same-origin requests', async () => {
|
|
44
|
+
const blocked = await route('/api/unknown', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { origin: 'https://malicious.example' },
|
|
47
|
+
})
|
|
48
|
+
expect(blocked?.status).toBe(403)
|
|
49
|
+
expect(await blocked?.json()).toEqual({ error: 'cross-site request blocked' })
|
|
50
|
+
|
|
51
|
+
const sameOrigin = await route('/api/unknown', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { origin: 'http://127.0.0.1' },
|
|
54
|
+
})
|
|
55
|
+
expect(sameOrigin?.status).toBe(404)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('cross-site protection compares the complete origin and leaves mutation state unchanged', async () => {
|
|
59
|
+
const handle = fixture()
|
|
60
|
+
const contextPath = `/api/domain/${encodeURIComponent(handle.id)}/context`
|
|
61
|
+
const mutation = {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'content-type': 'application/json',
|
|
65
|
+
// The host matches Studio, but the scheme does not: this is not same-origin.
|
|
66
|
+
origin: 'https://127.0.0.1',
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({ action: 'add', title: 'Injected', body: 'Must not persist' }),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const blocked = await route(contextPath, mutation)
|
|
72
|
+
expect(blocked?.status).toBe(403)
|
|
73
|
+
expect(await blocked?.json()).toEqual({ error: 'cross-site request blocked' })
|
|
74
|
+
|
|
75
|
+
const wrongPort = await route(contextPath, {
|
|
76
|
+
...mutation,
|
|
77
|
+
headers: { ...mutation.headers, origin: 'http://127.0.0.1:3000' },
|
|
78
|
+
})
|
|
79
|
+
expect(wrongPort?.status).toBe(403)
|
|
80
|
+
|
|
81
|
+
const unchanged = await route(contextPath)
|
|
82
|
+
expect(await unchanged?.json()).toEqual({ user: [], auto: [] })
|
|
83
|
+
|
|
84
|
+
const allowed = await route(contextPath, {
|
|
85
|
+
...mutation,
|
|
86
|
+
headers: { ...mutation.headers, origin: 'http://127.0.0.1' },
|
|
87
|
+
})
|
|
88
|
+
expect(allowed?.status).toBe(200)
|
|
89
|
+
const stored = (await (await route(contextPath))?.json()) as { user: unknown[] }
|
|
90
|
+
expect(stored.user).toHaveLength(1)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('domain dispatch preserves context success, validation, and unknown-domain responses', async () => {
|
|
94
|
+
const handle = fixture()
|
|
95
|
+
const base = `/api/domain/${encodeURIComponent(handle.id)}`
|
|
96
|
+
|
|
97
|
+
const context = await route(`${base}/context`)
|
|
98
|
+
expect(context?.status).toBe(200)
|
|
99
|
+
expect(await context?.json()).toEqual({ user: [], auto: [] })
|
|
100
|
+
|
|
101
|
+
const invalid = await route(`${base}/context`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'content-type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ action: 'invalid' }),
|
|
105
|
+
})
|
|
106
|
+
expect(invalid?.status).toBe(400)
|
|
107
|
+
expect(await invalid?.json()).toEqual({ error: 'unknown context action' })
|
|
108
|
+
|
|
109
|
+
const missing = await route('/api/domain/not-registered/context')
|
|
110
|
+
expect(missing?.status).toBe(404)
|
|
111
|
+
expect(await missing?.json()).toEqual({ error: 'not found' })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('workspace dispatch preserves catalog responses and instance request validation', async () => {
|
|
115
|
+
const handle = fixture()
|
|
116
|
+
|
|
117
|
+
const catalog = await route('/api/catalog')
|
|
118
|
+
expect(catalog?.status).toBe(200)
|
|
119
|
+
const origins = (await catalog?.json()).map((entry: { origin: string }) => entry.origin)
|
|
120
|
+
expect(origins[0]).toBe('kernel.astrale.ai')
|
|
121
|
+
expect(origins).toContain(handle.id)
|
|
122
|
+
|
|
123
|
+
const invalidInstance = await route('/api/instances/use', {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'content-type': 'application/json' },
|
|
126
|
+
body: JSON.stringify({ name: ' ' }),
|
|
127
|
+
})
|
|
128
|
+
expect(invalidInstance?.status).toBe(400)
|
|
129
|
+
expect(await invalidInstance?.json()).toEqual({ error: 'name is required' })
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('multipart document upload remains ahead of JSON parsing and raw download keeps its media type', async () => {
|
|
133
|
+
const handle = fixture()
|
|
134
|
+
const base = `/api/domain/${encodeURIComponent(handle.id)}`
|
|
135
|
+
const form = new FormData()
|
|
136
|
+
form.append('files', new File(['contract body'], 'contract.txt', { type: 'text/plain' }))
|
|
137
|
+
|
|
138
|
+
const uploaded = await route(`${base}/context/documents`, { method: 'POST', body: form })
|
|
139
|
+
expect(uploaded?.status).toBe(200)
|
|
140
|
+
const documents = (await uploaded?.json()) as Array<{ id: string; type: string }>
|
|
141
|
+
expect(documents).toHaveLength(1)
|
|
142
|
+
|
|
143
|
+
const raw = await route(`${base}/context/documents/${encodeURIComponent(documents[0]!.id)}/raw`)
|
|
144
|
+
expect(raw?.status).toBe(200)
|
|
145
|
+
expect(raw?.headers.get('content-type')).toBe('text/plain;charset=utf-8')
|
|
146
|
+
expect(await raw?.text()).toBe('contract body')
|
|
147
|
+
})
|
package/studio/server/api.ts
CHANGED
|
@@ -1,436 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Studio JSON API composition root. It owns only the public `/api/` boundary,
|
|
3
|
+
* cross-site mutation guard, workspace-vs-domain addressing, and final 404.
|
|
4
|
+
* Capability modules under `api/` own the route implementations.
|
|
4
5
|
*/
|
|
5
|
-
import
|
|
6
|
+
import { handleDomainRoute } from './api/domain'
|
|
7
|
+
import { crossSiteBlocked, json, notFound, type Notify } from './api/http'
|
|
8
|
+
import { handleWorkspaceRoute } from './api/workspace'
|
|
9
|
+
import { getDomain } from './domain'
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
import { getBundle, getAnatomy, getCore, invalidate } from './cache'
|
|
9
|
-
import { type DomainHandle, allDomains, depsInstalled, getDomain } from './domain'
|
|
10
|
-
import { schemaRefs } from './introspect/schema-refs'
|
|
11
|
-
import { buildCatalog } from './state/catalog'
|
|
12
|
-
import {
|
|
13
|
-
addThreadEntry,
|
|
14
|
-
deleteComment,
|
|
15
|
-
editThreadEntry,
|
|
16
|
-
markOrphans,
|
|
17
|
-
mergeReply,
|
|
18
|
-
readComments,
|
|
19
|
-
setStatus,
|
|
20
|
-
upsertComment,
|
|
21
|
-
} from './state/comments'
|
|
22
|
-
import {
|
|
23
|
-
addUserContext,
|
|
24
|
-
deleteContext,
|
|
25
|
-
readContext,
|
|
26
|
-
setAutoInclude,
|
|
27
|
-
updateContext,
|
|
28
|
-
} from './state/context'
|
|
29
|
-
import { buildCopyMarkdown } from './state/copy'
|
|
30
|
-
import { createDomain } from './state/create'
|
|
31
|
-
import {
|
|
32
|
-
addDocument,
|
|
33
|
-
deleteDocument,
|
|
34
|
-
listDocuments,
|
|
35
|
-
readDocument,
|
|
36
|
-
updateDocument,
|
|
37
|
-
} from './state/documents'
|
|
38
|
-
import { isEnvName, readEnvModel, writeEnvUpdates } from './state/env'
|
|
39
|
-
import { detectGit } from './state/git'
|
|
40
|
-
import { refreshAuto } from './state/handoff'
|
|
41
|
-
import {
|
|
42
|
-
activeInstanceName,
|
|
43
|
-
instanceStatus,
|
|
44
|
-
listInstances,
|
|
45
|
-
runDeploy,
|
|
46
|
-
setActiveInstance,
|
|
47
|
-
} from './state/instance'
|
|
48
|
-
import { deleteIntegration, readIntegrations, upsertIntegration } from './state/integrations'
|
|
49
|
-
import { readLayout, resetLayout, saveLayout, setNodePositions } from './state/layout'
|
|
50
|
-
import { readSettings, updateSettings } from './state/settings'
|
|
51
|
-
import { applyUpdates, getUpdates } from './state/updates'
|
|
52
|
-
import { closeViewSession, getViewRuntime, launchViewSession } from './state/views'
|
|
53
|
-
import { readVisibility, resetVisibility, saveVisibility } from './state/visibility'
|
|
54
|
-
|
|
55
|
-
function json(data: unknown, status = 200): Response {
|
|
56
|
-
return new Response(JSON.stringify(data), {
|
|
57
|
-
status,
|
|
58
|
-
headers: { 'content-type': 'application/json' },
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
const notFound = () => json({ error: 'not found' }, 404)
|
|
62
|
-
const badReq = (m: string) => json({ error: m }, 400)
|
|
63
|
-
|
|
64
|
-
/** The deploy-target INSTANCE name, or null for route-based / unset targets (which have no instance to query). */
|
|
65
|
-
function instanceTarget(prodTarget?: string): string | null {
|
|
66
|
-
const m = prodTarget?.match(/^instance:\s*(.+)$/)
|
|
67
|
-
return m ? m[1].trim() || null : null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Block cross-site state-changing requests (CSRF defense-in-depth on top of the
|
|
72
|
-
* loopback bind). A mutation triggers the local agent (file edits + shell), so a
|
|
73
|
-
* malicious page the user visits must NOT be able to POST here. The studio's own
|
|
74
|
-
* SPA is same-origin; curl/CLI/the MCP bridge send no Origin/Sec-Fetch-Site.
|
|
75
|
-
*/
|
|
76
|
-
function crossSiteBlocked(req: Request, url: URL): boolean {
|
|
77
|
-
if (req.method === 'GET' || req.method === 'HEAD') return false
|
|
78
|
-
const sfs = req.headers.get('sec-fetch-site')
|
|
79
|
-
if (sfs) return !(sfs === 'same-origin' || sfs === 'none')
|
|
80
|
-
const origin = req.headers.get('origin')
|
|
81
|
-
if (origin) {
|
|
82
|
-
try {
|
|
83
|
-
return new URL(origin).host !== url.host
|
|
84
|
-
} catch {
|
|
85
|
-
return true
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return false // no Origin + no Sec-Fetch-Site → a non-browser client (curl/CLI/MCP)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export type Notify = (e: StudioEvent) => void
|
|
11
|
+
export type { Notify } from './api/http'
|
|
92
12
|
|
|
93
13
|
export async function handleApi(req: Request, url: URL, notify: Notify): Promise<Response | null> {
|
|
94
14
|
const path = url.pathname
|
|
95
15
|
if (!path.startsWith('/api/')) return null
|
|
96
16
|
if (crossSiteBlocked(req, url)) return json({ error: 'cross-site request blocked' }, 403)
|
|
97
17
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
allDomains().map(async (h) => {
|
|
101
|
-
const bundle = await getBundle(h.id)
|
|
102
|
-
return {
|
|
103
|
-
id: h.id,
|
|
104
|
-
origin: bundle?.overlay.origin || h.origin || h.id,
|
|
105
|
-
path: h.root,
|
|
106
|
-
schemaDir: h.schemaDirName,
|
|
107
|
-
depsInstalled: depsInstalled(h.root),
|
|
108
|
-
hasGit: detectGit(h.root).hasGit,
|
|
109
|
-
configFile: h.configFile,
|
|
110
|
-
}
|
|
111
|
-
}),
|
|
112
|
-
)
|
|
113
|
-
return json(out)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Scaffold a brand-new domain into the workspace (the one write that ADDS a domain).
|
|
117
|
-
if (path === '/api/workspace/create' && req.method === 'POST') {
|
|
118
|
-
const body = await req.json().catch(() => ({}))
|
|
119
|
-
const name = String(body.name ?? '')
|
|
120
|
-
const result = await createDomain(name, await activeInstanceName())
|
|
121
|
-
if (result.ok) notify({ type: 'workspace', domains: allDomains().map((d) => d.id) })
|
|
122
|
-
return json(result) // 200 with {ok:false,error} on failure — the SPA reads the result shape
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (path === '/api/catalog') {
|
|
126
|
-
return json(buildCatalog(allDomains().map((h) => ({ origin: h.origin ?? h.id, id: h.id }))))
|
|
127
|
-
}
|
|
18
|
+
const workspace = await handleWorkspaceRoute(req, path, notify)
|
|
19
|
+
if (workspace) return workspace
|
|
128
20
|
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
const body = await req.json().catch(() => ({}))
|
|
133
|
-
const name = String(body.name ?? '').trim()
|
|
134
|
-
if (!name) return badReq('name is required')
|
|
135
|
-
return json(await setActiveInstance(name))
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// /api/domain/:id/...
|
|
139
|
-
const m = path.match(/^\/api\/domain\/([^/]+)(\/.*)?$/)
|
|
140
|
-
if (!m) return notFound()
|
|
141
|
-
const id = decodeURIComponent(m[1])
|
|
142
|
-
const rest = m[2] ?? ''
|
|
21
|
+
const match = path.match(/^\/api\/domain\/([^/]+)(\/.*)?$/)
|
|
22
|
+
if (!match) return notFound()
|
|
23
|
+
const id = decodeURIComponent(match[1])
|
|
143
24
|
const handle = getDomain(id)
|
|
144
25
|
if (!handle) return notFound()
|
|
145
|
-
const root = handle.root
|
|
146
|
-
|
|
147
|
-
// ── context documents (handled BEFORE the json body parse — uploads are multipart) ──
|
|
148
|
-
if (rest === '/context/documents' && req.method === 'GET') return json(listDocuments(root))
|
|
149
|
-
if (rest === '/context/documents' && req.method === 'POST') {
|
|
150
|
-
const form = await req.formData()
|
|
151
|
-
const added = []
|
|
152
|
-
for (const value of form.getAll('files')) {
|
|
153
|
-
if (value instanceof File)
|
|
154
|
-
added.push(
|
|
155
|
-
addDocument(root, value.name, value.type, new Uint8Array(await value.arrayBuffer())),
|
|
156
|
-
)
|
|
157
|
-
}
|
|
158
|
-
return json(added)
|
|
159
|
-
}
|
|
160
|
-
const rawDoc = rest.match(/^\/context\/documents\/([^/]+)\/raw$/)
|
|
161
|
-
if (rawDoc && req.method === 'GET') {
|
|
162
|
-
const d = readDocument(root, decodeURIComponent(rawDoc[1]))
|
|
163
|
-
if (!d) return notFound()
|
|
164
|
-
return new Response(Bun.file(d.abs), { headers: { 'content-type': d.meta.type } })
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const body = req.method === 'POST' ? await req.json().catch(() => ({})) : {}
|
|
168
|
-
|
|
169
|
-
if (rest === '/context/documents/delete' && req.method === 'POST')
|
|
170
|
-
return json({ ok: deleteDocument(root, body.id) })
|
|
171
|
-
if (rest === '/context/documents/update' && req.method === 'POST') {
|
|
172
|
-
const meta = updateDocument(root, body.id, new TextEncoder().encode(String(body.content ?? '')))
|
|
173
|
-
return meta ? json(meta) : notFound()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── schema bundle ──
|
|
177
|
-
if (rest === '/bundle') return json(await getBundle(id, url.searchParams.has('fresh')))
|
|
178
|
-
if (rest === '/anatomy') return json(await getAnatomy(id, url.searchParams.has('fresh')))
|
|
179
|
-
|
|
180
|
-
// ── update staleness (CLI + SDK deps, via `astrale update --check --json`) ──
|
|
181
|
-
if (rest === '/updates' && req.method === 'GET') return json(await getUpdates(root))
|
|
182
|
-
// ── apply the update (the "Update now" button) — runs `astrale update --yes` ──
|
|
183
|
-
if (rest === '/updates/apply' && req.method === 'POST') return json(await applyUpdates(root))
|
|
184
26
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const viewRuntimeM = rest.match(/^\/views\/([^/]+)\/runtime$/)
|
|
194
|
-
if (viewRuntimeM && req.method === 'GET') {
|
|
195
|
-
const slug = decodeURIComponent(viewRuntimeM[1])
|
|
196
|
-
if (!/^[a-z][a-z0-9-]*$/.test(slug)) return badReq('invalid view slug')
|
|
197
|
-
const anatomy = await getAnatomy(id)
|
|
198
|
-
const view = anatomy?.views.find((candidate) => candidate.slug === slug)
|
|
199
|
-
if (!view) return notFound()
|
|
200
|
-
const bundle = await getBundle(id)
|
|
201
|
-
const origin = bundle?.overlay.origin || anatomy?.overview.origin
|
|
202
|
-
if (!origin) return badReq('domain origin is unavailable')
|
|
203
|
-
return json(
|
|
204
|
-
await getViewRuntime(root, origin, view, bundle, readSettings(root).viewProbeTimeoutMs),
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const viewSessionM = rest.match(/^\/views\/([^/]+)\/session$/)
|
|
209
|
-
if (viewSessionM && req.method === 'POST') {
|
|
210
|
-
const slug = decodeURIComponent(viewSessionM[1])
|
|
211
|
-
if (!/^[a-z][a-z0-9-]*$/.test(slug)) return badReq('invalid view slug')
|
|
212
|
-
const anatomy = await getAnatomy(id)
|
|
213
|
-
const view = anatomy?.views.find((candidate) => candidate.slug === slug)
|
|
214
|
-
if (!view) return notFound()
|
|
215
|
-
const bundle = await getBundle(id)
|
|
216
|
-
const origin = bundle?.overlay.origin || anatomy?.overview.origin
|
|
217
|
-
if (!origin) return badReq('domain origin is unavailable')
|
|
218
|
-
return json(
|
|
219
|
-
await launchViewSession(
|
|
220
|
-
root,
|
|
221
|
-
origin,
|
|
222
|
-
view,
|
|
223
|
-
bundle,
|
|
224
|
-
body,
|
|
225
|
-
readSettings(root).viewProbeTimeoutMs,
|
|
226
|
-
),
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── instance / deploy ──
|
|
231
|
-
if (rest === '/instance') {
|
|
232
|
-
const target = instanceTarget((await getAnatomy(id))?.overview.prodTarget)
|
|
233
|
-
const bundle = await getBundle(id)
|
|
234
|
-
return json(
|
|
235
|
-
await instanceStatus(
|
|
236
|
-
handle,
|
|
237
|
-
target,
|
|
238
|
-
bundle?.overlay.origin ?? null,
|
|
239
|
-
bundle?.schemaHash ?? null,
|
|
240
|
-
),
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
if (rest === '/instance/deploy' && req.method === 'POST') {
|
|
244
|
-
const bundle = await getBundle(id)
|
|
245
|
-
const result = await runDeploy(handle, bundle?.schemaHash ?? null)
|
|
246
|
-
return json(result)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// ── comments ──
|
|
250
|
-
if (rest === '/comments') {
|
|
251
|
-
if (req.method === 'GET') {
|
|
252
|
-
const bundle = await getBundle(id)
|
|
253
|
-
// Valid schema targets come from the IR (authoritative), not source spans —
|
|
254
|
-
// so inherited / span-less members are never falsely orphaned. Spans are
|
|
255
|
-
// unioned in only as a belt-and-suspenders for refs the IR walk might miss.
|
|
256
|
-
const valid = new Set<string>(bundle ? schemaRefs(bundle) : [])
|
|
257
|
-
for (const k of Object.keys(bundle?.overlay.sourceSpans ?? {})) valid.add(k)
|
|
258
|
-
const store = readComments(root)
|
|
259
|
-
for (const c of store.comments)
|
|
260
|
-
for (const a of c.anchorRefs) if (a.kind !== 'schema') valid.add(a.ref)
|
|
261
|
-
return json(markOrphans(root, [...valid]))
|
|
262
|
-
}
|
|
263
|
-
if (body.action === 'create') {
|
|
264
|
-
const bundle = await getBundle(id)
|
|
265
|
-
const c = upsertComment(root, {
|
|
266
|
-
anchors: body.anchors ?? [],
|
|
267
|
-
anchorRefs: body.anchorRefs ?? [],
|
|
268
|
-
text: body.text,
|
|
269
|
-
firstRole: body.firstRole,
|
|
270
|
-
type: body.type,
|
|
271
|
-
options: body.options,
|
|
272
|
-
schemaVersion: bundle?.schemaHash,
|
|
273
|
-
})
|
|
274
|
-
notify({ type: 'comments', domainId: id })
|
|
275
|
-
return json(c)
|
|
276
|
-
}
|
|
277
|
-
if (body.action === 'reply') {
|
|
278
|
-
const c = addThreadEntry(root, body.id, body.entry)
|
|
279
|
-
notify({ type: 'comments', domainId: id })
|
|
280
|
-
return c ? json(c) : notFound()
|
|
281
|
-
}
|
|
282
|
-
if (body.action === 'edit') {
|
|
283
|
-
const c = editThreadEntry(root, body.id, body.entryId, String(body.text ?? ''))
|
|
284
|
-
notify({ type: 'comments', domainId: id })
|
|
285
|
-
return c ? json(c) : notFound()
|
|
286
|
-
}
|
|
287
|
-
if (body.action === 'status') {
|
|
288
|
-
const c = setStatus(root, body.id, body.status, body.closeNote)
|
|
289
|
-
notify({ type: 'comments', domainId: id })
|
|
290
|
-
return c ? json(c) : notFound()
|
|
291
|
-
}
|
|
292
|
-
if (body.action === 'delete') {
|
|
293
|
-
const ok = deleteComment(root, body.id)
|
|
294
|
-
notify({ type: 'comments', domainId: id })
|
|
295
|
-
return json({ ok })
|
|
296
|
-
}
|
|
297
|
-
return badReq('unknown comments action')
|
|
298
|
-
}
|
|
299
|
-
if (rest === '/comments/merge' && req.method === 'POST') {
|
|
300
|
-
const bundle = await getBundle(id)
|
|
301
|
-
try {
|
|
302
|
-
const result = mergeReply(root, bundle?.schemaHash ?? '', String(body.text ?? ''), {
|
|
303
|
-
dedupeAuthorText: true,
|
|
304
|
-
})
|
|
305
|
-
notify({ type: 'comments', domainId: id })
|
|
306
|
-
return json(result)
|
|
307
|
-
} catch (e: any) {
|
|
308
|
-
return badReq(String(e?.message ?? e))
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const agentResponse = await handleAgentRoute({ req, url, rest, body, handle, notify })
|
|
313
|
-
if (agentResponse) return agentResponse
|
|
314
|
-
|
|
315
|
-
// ── context ──
|
|
316
|
-
if (rest === '/context') {
|
|
317
|
-
if (req.method === 'GET') return json(readContext(root))
|
|
318
|
-
if (body.action === 'add') return notifyJson(notify, id, 'comments', addUserContext(root, body))
|
|
319
|
-
if (body.action === 'update') {
|
|
320
|
-
const it = updateContext(root, body.id, body)
|
|
321
|
-
return it ? json(it) : notFound()
|
|
322
|
-
}
|
|
323
|
-
if (body.action === 'delete') return json({ ok: deleteContext(root, body.id) })
|
|
324
|
-
if (body.action === 'include') {
|
|
325
|
-
const it = setAutoInclude(root, body.id, body.include)
|
|
326
|
-
return it ? json(it) : notFound()
|
|
327
|
-
}
|
|
328
|
-
return badReq('unknown context action')
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// ── integrations ──
|
|
332
|
-
if (rest === '/integrations') {
|
|
333
|
-
const detected = (await getAnatomy(id))?.detectedIntegrations ?? []
|
|
334
|
-
if (req.method === 'GET') return json(readIntegrations(root, detected))
|
|
335
|
-
if (body.action === 'upsert') return json(upsertIntegration(root, body))
|
|
336
|
-
if (body.action === 'delete') return json({ ok: deleteIntegration(root, body.id) })
|
|
337
|
-
return badReq('unknown integrations action')
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ── settings (per-domain power-user overrides) ──
|
|
341
|
-
if (rest === '/settings') {
|
|
342
|
-
if (req.method === 'GET') return json(readSettings(root))
|
|
343
|
-
if (body.action === 'update') {
|
|
344
|
-
const next = updateSettings(root, body.settings ?? {})
|
|
345
|
-
invalidate(id, 'anatomy') // integrationsDir changes what detection scans
|
|
346
|
-
notify({ type: 'anatomy-diff', domainId: id })
|
|
347
|
-
return json(next)
|
|
348
|
-
}
|
|
349
|
-
return badReq('unknown settings action')
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ── env vars editor: read/parse + WRITE `.env.<env>` (the read-only studio's one
|
|
353
|
-
// sanctioned domain-file writer), reconciled against env.ts's Env contract ──
|
|
354
|
-
if (rest === '/env') {
|
|
355
|
-
const envName = req.method === 'GET' ? url.searchParams.get('env') : body.env
|
|
356
|
-
if (!isEnvName(envName)) return badReq('env must be "dev" or "prod"')
|
|
357
|
-
if (req.method === 'GET') return json(readEnvModel(root, envName))
|
|
358
|
-
if (req.method === 'POST') {
|
|
359
|
-
if (!body.updates || typeof body.updates !== 'object')
|
|
360
|
-
return badReq('updates object required')
|
|
361
|
-
const updates: Record<string, string | null> = {}
|
|
362
|
-
for (const [k, v] of Object.entries(body.updates as Record<string, unknown>)) {
|
|
363
|
-
if (!/^[A-Za-z_]\w*$/.test(k)) continue // ignore bad keys
|
|
364
|
-
updates[k] = v === null ? null : String(v)
|
|
365
|
-
}
|
|
366
|
-
try {
|
|
367
|
-
const model = writeEnvUpdates(root, envName, updates)
|
|
368
|
-
notify({ type: 'anatomy-diff', domainId: id }) // env.ts/.env changed — nudge consumers
|
|
369
|
-
return json(model)
|
|
370
|
-
} catch (e: any) {
|
|
371
|
-
return badReq(String(e?.message ?? e))
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return badReq('GET or POST')
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// ── graph layout (persisted manual positions) ──
|
|
378
|
-
if (rest === '/layout') {
|
|
379
|
-
const bundle = await getBundle(id)
|
|
380
|
-
if (req.method === 'GET') return json(readLayout(root))
|
|
381
|
-
if (body.action === 'set')
|
|
382
|
-
return json(setNodePositions(root, body.positions ?? {}, bundle?.schemaHash))
|
|
383
|
-
if (body.action === 'save')
|
|
384
|
-
return json(saveLayout(root, body.positions ?? {}, bundle?.schemaHash))
|
|
385
|
-
if (body.action === 'reset') {
|
|
386
|
-
resetLayout(root)
|
|
387
|
-
return json({ ok: true })
|
|
388
|
-
}
|
|
389
|
-
return badReq('unknown layout action')
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ── canvas visibility (persisted hide-set, inherited-edge toggle, interface nodes) ──
|
|
393
|
-
if (rest === '/visibility') {
|
|
394
|
-
if (req.method === 'GET') return json(readVisibility(root))
|
|
395
|
-
if (body.action === 'set')
|
|
396
|
-
return json(
|
|
397
|
-
saveVisibility(root, {
|
|
398
|
-
hidden: body.hidden ?? {},
|
|
399
|
-
showInheritedEdges: body.showInheritedEdges ?? true,
|
|
400
|
-
materializedInterfaces: body.materializedInterfaces ?? {},
|
|
401
|
-
}),
|
|
402
|
-
)
|
|
403
|
-
if (body.action === 'reset') return json(resetVisibility(root))
|
|
404
|
-
return badReq('unknown visibility action')
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// ── copy payload ──
|
|
408
|
-
if (rest === '/copy-payload' && req.method === 'POST') {
|
|
409
|
-
await refreshAuto(handle)
|
|
410
|
-
const bundle = await getBundle(id)
|
|
411
|
-
const ctx = readContext(root)
|
|
412
|
-
const open = readComments(root).comments.filter((c) => c.status === 'open')
|
|
413
|
-
const md = buildCopyMarkdown({
|
|
414
|
-
origin: bundle?.overlay.origin ?? id,
|
|
415
|
-
root,
|
|
416
|
-
schemaHash: bundle?.schemaHash ?? '',
|
|
417
|
-
openComments: open,
|
|
418
|
-
userContext: ctx.user,
|
|
419
|
-
autoContext: body.includeAuto ? ctx.auto.filter((a) => a.includeInHandoff) : [],
|
|
420
|
-
documents: listDocuments(root),
|
|
421
|
-
})
|
|
422
|
-
return json({ markdown: md, openComments: open.length })
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
return notFound()
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function notifyJson(
|
|
429
|
-
notify: Notify,
|
|
430
|
-
id: string,
|
|
431
|
-
type: StudioEvent['type'],
|
|
432
|
-
data: unknown,
|
|
433
|
-
): Response {
|
|
434
|
-
notify({ type, domainId: id } as StudioEvent)
|
|
435
|
-
return json(data)
|
|
27
|
+
return handleDomainRoute({
|
|
28
|
+
req,
|
|
29
|
+
url,
|
|
30
|
+
rest: match[2] ?? '',
|
|
31
|
+
handle,
|
|
32
|
+
notify,
|
|
33
|
+
})
|
|
436
34
|
}
|