@neuralnomads/codenomad-dev 0.20.0-dev-20260922-e68ac524 → 0.20.0-dev-20260922-0a31a8b3
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/opencode/compatibility/location.js +11 -0
- package/dist/opencode/compatibility/location.js.map +1 -1
- package/dist/opencode/compatibility/location.test.js +21 -0
- package/dist/opencode/compatibility/location.test.js.map +1 -1
- package/dist/server/__tests__/instance-proxy.test.js +27 -0
- package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
- package/dist/server/http-server.js +1 -0
- package/dist/server/http-server.js.map +1 -1
- package/dist/workspaces/__tests__/git-common-directory.test.js +11 -0
- package/dist/workspaces/__tests__/git-common-directory.test.js.map +1 -1
- package/dist/workspaces/git-process.js +16 -6
- package/dist/workspaces/git-process.js.map +1 -1
- package/dist/workspaces/git-status.js +32 -38
- package/dist/workspaces/git-status.js.map +1 -1
- package/dist/workspaces/git-status.test.js +28 -1
- package/dist/workspaces/git-status.test.js.map +1 -1
- package/package.json +1 -1
- package/public/assets/App-CkXPZUpS.js +51 -0
- package/public/assets/App-CkXPZUpS.js.map +1 -0
- package/public/assets/{FilesTab-CznSFRW_.js → FilesTab-C4o3_OHJ.js} +3 -3
- package/public/assets/{FilesTab-CznSFRW_.js.map → FilesTab-C4o3_OHJ.js.map} +1 -1
- package/public/assets/{GitChangesTab-DLtyzpX0.js → GitChangesTab-ClsL5QrZ.js} +3 -3
- package/public/assets/{GitChangesTab-DLtyzpX0.js.map → GitChangesTab-ClsL5QrZ.js.map} +1 -1
- package/public/assets/{SplitFilePanel-BHkyIER4.js → SplitFilePanel-C_dI-kjb.js} +2 -2
- package/public/assets/{SplitFilePanel-BHkyIER4.js.map → SplitFilePanel-C_dI-kjb.js.map} +1 -1
- package/public/assets/{StatusTab-BB3L4UEF.js → StatusTab-_gvxV-qq.js} +2 -2
- package/public/assets/{StatusTab-BB3L4UEF.js.map → StatusTab-_gvxV-qq.js.map} +1 -1
- package/public/assets/{align-justify-BGcr8Y3G.js → align-justify-CboH-c3n.js} +2 -2
- package/public/assets/{align-justify-BGcr8Y3G.js.map → align-justify-CboH-c3n.js.map} +1 -1
- package/public/assets/{bundle-full-CwHbX2Tz.js → bundle-full-CL2fEEcE.js} +2 -2
- package/public/assets/{bundle-full-CwHbX2Tz.js.map → bundle-full-CL2fEEcE.js.map} +1 -1
- package/public/assets/{client-state-CjVIIkWh.js → client-state-BB0go4Xe.js} +2 -2
- package/public/assets/{client-state-CjVIIkWh.js.map → client-state-BB0go4Xe.js.map} +1 -1
- package/public/assets/{diff-viewer-FNBCogHl.js → diff-viewer-C-B3vwne.js} +2 -2
- package/public/assets/{diff-viewer-FNBCogHl.js.map → diff-viewer-C-B3vwne.js.map} +1 -1
- package/public/assets/index-4LEntpq0.js +2 -0
- package/public/assets/index-4LEntpq0.js.map +1 -0
- package/public/assets/index-4mzZPz2j.css +1 -0
- package/public/assets/index-B2wje_CF.js +2 -0
- package/public/assets/index-B2wje_CF.js.map +1 -0
- package/public/assets/index-BDbkD8FO.js +2 -0
- package/public/assets/index-BDbkD8FO.js.map +1 -0
- package/public/assets/index-BbUJLdzG.js +2 -0
- package/public/assets/index-BbUJLdzG.js.map +1 -0
- package/public/assets/index-Bdh_-CtJ.js +2 -0
- package/public/assets/index-Bdh_-CtJ.js.map +1 -0
- package/public/assets/index-C9Jqb-tF.js +2 -0
- package/public/assets/index-C9Jqb-tF.js.map +1 -0
- package/public/assets/index-CIT8ANJK.js +3 -0
- package/public/assets/index-CIT8ANJK.js.map +1 -0
- package/public/assets/index-CheaKG9m.js +2 -0
- package/public/assets/{index-C2jBtFkI.js.map → index-CheaKG9m.js.map} +1 -1
- package/public/assets/index-DbnGqvtc.js +2 -0
- package/public/assets/index-DbnGqvtc.js.map +1 -0
- package/public/assets/index-DhOvQD1j.js +2 -0
- package/public/assets/index-DhOvQD1j.js.map +1 -0
- package/public/assets/{index-CLgpBTF6.js → index-DrTKEExa.js} +2 -2
- package/public/assets/{index-CLgpBTF6.js.map → index-DrTKEExa.js.map} +1 -1
- package/public/assets/index-Gk3Kwcb2.js +2 -0
- package/public/assets/index-Gk3Kwcb2.js.map +1 -0
- package/public/assets/{loading-D8BcUQaM.js → loading-Cs2CSqHx.js} +2 -2
- package/public/assets/{loading-D8BcUQaM.js.map → loading-Cs2CSqHx.js.map} +1 -1
- package/public/assets/main-DAwgVcRN.js +42 -0
- package/public/assets/main-DAwgVcRN.js.map +1 -0
- package/public/assets/{markdown-CRPEbAqL.js → markdown-DheZTu4V.js} +4 -4
- package/public/assets/{markdown-CRPEbAqL.js.map → markdown-DheZTu4V.js.map} +1 -1
- package/public/assets/monaco-viewer-BtzCmjFO.js +27 -0
- package/public/assets/monaco-viewer-BtzCmjFO.js.map +1 -0
- package/public/assets/{startup-state-settings-card-CHqS2Pmg.js → startup-state-settings-card-CmMXcnR-.js} +2 -2
- package/public/assets/{startup-state-settings-card-CHqS2Pmg.js.map → startup-state-settings-card-CmMXcnR-.js.map} +1 -1
- package/public/assets/{terminal-square-DhmNJqNG.js → terminal-square-ypyAq6Ni.js} +2 -2
- package/public/assets/{terminal-square-DhmNJqNG.js.map → terminal-square-ypyAq6Ni.js.map} +1 -1
- package/public/assets/{todo-DXJ5kP_Z.js → todo-Cgy3-kyM.js} +2 -2
- package/public/assets/{todo-DXJ5kP_Z.js.map → todo-Cgy3-kyM.js.map} +1 -1
- package/public/assets/{tool-call-Bjo9DalZ.js → tool-call-0UMa6igS.js} +17 -17
- package/public/assets/{tool-call-Bjo9DalZ.js.map → tool-call-0UMa6igS.js.map} +1 -1
- package/public/assets/{unified-picker-CxrTM_tY.js → unified-picker-C-lhbMQ0.js} +2 -2
- package/public/assets/{unified-picker-CxrTM_tY.js.map → unified-picker-C-lhbMQ0.js.map} +1 -1
- package/public/assets/{wrap-text-BznZeD4J.js → wrap-text-BjODGwnM.js} +2 -2
- package/public/assets/{wrap-text-BznZeD4J.js.map → wrap-text-BjODGwnM.js.map} +1 -1
- package/public/index.html +4 -4
- package/public/loading.html +4 -4
- package/public/sw.js +1 -1
- package/public/sw.js.map +1 -1
- package/public/assets/App-BSkmbNUC.js +0 -51
- package/public/assets/App-BSkmbNUC.js.map +0 -1
- package/public/assets/index-AeE49W2c.js +0 -2
- package/public/assets/index-AeE49W2c.js.map +0 -1
- package/public/assets/index-B875QQLi.js +0 -2
- package/public/assets/index-B875QQLi.js.map +0 -1
- package/public/assets/index-BhInW6hR.js +0 -3
- package/public/assets/index-BhInW6hR.js.map +0 -1
- package/public/assets/index-C2jBtFkI.js +0 -2
- package/public/assets/index-C8zCyB6Z.js +0 -2
- package/public/assets/index-C8zCyB6Z.js.map +0 -1
- package/public/assets/index-C9vJvaFa.js +0 -2
- package/public/assets/index-C9vJvaFa.js.map +0 -1
- package/public/assets/index-CLdQSSfU.js +0 -2
- package/public/assets/index-CLdQSSfU.js.map +0 -1
- package/public/assets/index-CaYFZAN4.js +0 -2
- package/public/assets/index-CaYFZAN4.js.map +0 -1
- package/public/assets/index-ChudAs0f.js +0 -2
- package/public/assets/index-ChudAs0f.js.map +0 -1
- package/public/assets/index-D292-iMl.js +0 -2
- package/public/assets/index-D292-iMl.js.map +0 -1
- package/public/assets/index-DYBqUC9X.css +0 -1
- package/public/assets/index-Dk3HFhZs.js +0 -2
- package/public/assets/index-Dk3HFhZs.js.map +0 -1
- package/public/assets/main-DRbjgAo8.js +0 -42
- package/public/assets/main-DRbjgAo8.js.map +0 -1
- package/public/assets/monaco-viewer-CG0aPZ-r.js +0 -27
- package/public/assets/monaco-viewer-CG0aPZ-r.js.map +0 -1
|
@@ -74,6 +74,17 @@ export function applyLocationContext(url, method, body, headers) {
|
|
|
74
74
|
// Credential mutations are global; context must not create location scope.
|
|
75
75
|
return body;
|
|
76
76
|
}
|
|
77
|
+
const sessionRoute = url.pathname.replace(/\/+$/, "").match(/^\/api\/(?:experimental\/)?session\/([^/]+)(?:\/.*)?$/);
|
|
78
|
+
if (sessionRoute && sessionRoute[1] !== "active" && sessionRoute[1] !== "import") {
|
|
79
|
+
// A specific session's routes carry authority in the session identity,
|
|
80
|
+
// already ownership-checked by the proxy via session.get (which excludes
|
|
81
|
+
// the active/import pseudo-identities the same way). These native routes
|
|
82
|
+
// accept no location slot, so an ambient location header must not fail
|
|
83
|
+
// them here (for example instructions entries on every send). This check
|
|
84
|
+
// stays after the move/create/import/form/list handlers above, which do
|
|
85
|
+
// validate their own location slots.
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
77
88
|
const directory = url.searchParams.get("location[directory]");
|
|
78
89
|
assertDirectory(directory);
|
|
79
90
|
return body;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"location.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/location.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LocationRef, OpenCodeClient } from \"@opencode/client\"\nimport { readLocationRef } from \"../session-pruning/location\"\nexport { readLocationRef, sameLocation } from \"../session-pruning/location\"\n\n// An explicit CodeNomad context channel survives the generated modern client's\n// field selection. Never forward this header to OpenCode unchanged. The guarded\n// proxy must authorize its complete location before transport serialization.\nexport const LOCATION_CONTEXT_HEADER = \"x-codenomad-location\"\n\nexport function locationRequestOptions(location: LocationRef, options?: { includeDirectory?: boolean }): { headers: Record<string, string> } | undefined {\n const resolved = readLocationRef(location)\n return resolved.workspaceID === undefined && !options?.includeDirectory ? undefined : {\n headers: { [LOCATION_CONTEXT_HEADER]: encodeURIComponent(JSON.stringify(resolved)) },\n }\n}\n\nexport function readLocationContext(value: string | string[] | undefined): LocationRef | undefined {\n if (value === undefined) return undefined\n if (typeof value !== \"string\" || value.length > 16_384) throw new Error(\"Invalid location context\")\n const input: unknown = JSON.parse(decodeURIComponent(value))\n if (!input || typeof input !== \"object\" || Object.keys(input).some(key => key !== \"directory\" && key !== \"workspaceID\")) {\n throw new Error(\"Invalid location context\")\n }\n const location = readLocationRef(input)\n if (location.workspaceID !== undefined) throw new Error(\"Unsupported workspace selector\")\n return location\n}\n\nexport async function moveSessionToLocation(client: OpenCodeClient, sessionID: string, location: LocationRef): Promise<void> {\n await client.session.move({ sessionID, directory: location.directory }, locationRequestOptions(location))\n}\n\n// The transport calls this AFTER authorization. Validate the directory context\n// against its public wire slot without injecting obsolete identity selectors.\nexport function applyLocationContext(url: URL, method: string, body: unknown, headers: Headers): unknown {\n const location = readLocationContext(headers.get(LOCATION_CONTEXT_HEADER) ?? undefined)\n headers.delete(LOCATION_CONTEXT_HEADER)\n if (!location) return body\n if ([\"workspace\", \"location[workspace]\", \"workspaceID\", \"location[workspaceID]\"].some(key => url.searchParams.has(key))\n || headers.has(\"x-opencode-workspace\")) throw new Error(\"Unsupported workspace selector\")\n const input = body && typeof body === \"object\" && !Array.isArray(body) ? body as Record<string, unknown> : undefined\n if ((url.pathname === \"/api/worktree\" && [\"GET\", \"POST\", \"DELETE\"].includes(method))\n || (method === \"POST\" && url.pathname === \"/api/worktree/refresh\")) {\n return body\n }\n const assertDirectory = (directory: unknown) => {\n if (directory !== location.directory) throw new Error(\"Location context does not match request\")\n }\n if (method === \"POST\" && /^\\/api\\/session\\/[^/]+\\/move\\/?$/.test(url.pathname)) {\n assertDirectory(input?.directory)\n if (input?.workspaceID !== undefined) throw new Error(\"Unsupported workspace selector\")\n return body\n }\n if (method === \"POST\" && /^\\/api\\/(?:session|experimental\\/session\\/import)\\/?$/.test(url.pathname)) {\n const destination = readLocationRef(input?.location)\n assertDirectory(destination.directory)\n if (destination.workspaceID !== undefined) throw new Error(\"Unsupported workspace selector\")\n return body\n }\n if (/^\\/api\\/session\\/global\\/form(?:\\/[^/]+(?:\\/reply)?)?\\/?$/.test(url.pathname)) {\n assertDirectory(decodeURIComponent(headers.get(\"x-opencode-directory\") ?? \"\"))\n return body\n }\n if (method === \"GET\" && /^\\/api\\/session\\/?$/.test(url.pathname)) {\n // The cursor is the complete native query, not a location selector to amend.\n if (url.searchParams.has(\"cursor\")) throw new Error(\"Cursor already carries session list scope\")\n assertDirectory(url.searchParams.get(\"directory\"))\n return body\n }\n if (/^\\/api\\/credential\\/[^/]+(?:\\/activate)?\\/?$/.test(url.pathname)) {\n // Credential mutations are global; context must not create location scope.\n return body\n }\n const directory = url.searchParams.get(\"location[directory]\")\n assertDirectory(directory)\n return body\n}\n"]}
|
|
1
|
+
{"version":3,"file":"location.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/location.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LocationRef, OpenCodeClient } from \"@opencode/client\"\nimport { readLocationRef } from \"../session-pruning/location\"\nexport { readLocationRef, sameLocation } from \"../session-pruning/location\"\n\n// An explicit CodeNomad context channel survives the generated modern client's\n// field selection. Never forward this header to OpenCode unchanged. The guarded\n// proxy must authorize its complete location before transport serialization.\nexport const LOCATION_CONTEXT_HEADER = \"x-codenomad-location\"\n\nexport function locationRequestOptions(location: LocationRef, options?: { includeDirectory?: boolean }): { headers: Record<string, string> } | undefined {\n const resolved = readLocationRef(location)\n return resolved.workspaceID === undefined && !options?.includeDirectory ? undefined : {\n headers: { [LOCATION_CONTEXT_HEADER]: encodeURIComponent(JSON.stringify(resolved)) },\n }\n}\n\nexport function readLocationContext(value: string | string[] | undefined): LocationRef | undefined {\n if (value === undefined) return undefined\n if (typeof value !== \"string\" || value.length > 16_384) throw new Error(\"Invalid location context\")\n const input: unknown = JSON.parse(decodeURIComponent(value))\n if (!input || typeof input !== \"object\" || Object.keys(input).some(key => key !== \"directory\" && key !== \"workspaceID\")) {\n throw new Error(\"Invalid location context\")\n }\n const location = readLocationRef(input)\n if (location.workspaceID !== undefined) throw new Error(\"Unsupported workspace selector\")\n return location\n}\n\nexport async function moveSessionToLocation(client: OpenCodeClient, sessionID: string, location: LocationRef): Promise<void> {\n await client.session.move({ sessionID, directory: location.directory }, locationRequestOptions(location))\n}\n\n// The transport calls this AFTER authorization. Validate the directory context\n// against its public wire slot without injecting obsolete identity selectors.\nexport function applyLocationContext(url: URL, method: string, body: unknown, headers: Headers): unknown {\n const location = readLocationContext(headers.get(LOCATION_CONTEXT_HEADER) ?? undefined)\n headers.delete(LOCATION_CONTEXT_HEADER)\n if (!location) return body\n if ([\"workspace\", \"location[workspace]\", \"workspaceID\", \"location[workspaceID]\"].some(key => url.searchParams.has(key))\n || headers.has(\"x-opencode-workspace\")) throw new Error(\"Unsupported workspace selector\")\n const input = body && typeof body === \"object\" && !Array.isArray(body) ? body as Record<string, unknown> : undefined\n if ((url.pathname === \"/api/worktree\" && [\"GET\", \"POST\", \"DELETE\"].includes(method))\n || (method === \"POST\" && url.pathname === \"/api/worktree/refresh\")) {\n return body\n }\n const assertDirectory = (directory: unknown) => {\n if (directory !== location.directory) throw new Error(\"Location context does not match request\")\n }\n if (method === \"POST\" && /^\\/api\\/session\\/[^/]+\\/move\\/?$/.test(url.pathname)) {\n assertDirectory(input?.directory)\n if (input?.workspaceID !== undefined) throw new Error(\"Unsupported workspace selector\")\n return body\n }\n if (method === \"POST\" && /^\\/api\\/(?:session|experimental\\/session\\/import)\\/?$/.test(url.pathname)) {\n const destination = readLocationRef(input?.location)\n assertDirectory(destination.directory)\n if (destination.workspaceID !== undefined) throw new Error(\"Unsupported workspace selector\")\n return body\n }\n if (/^\\/api\\/session\\/global\\/form(?:\\/[^/]+(?:\\/reply)?)?\\/?$/.test(url.pathname)) {\n assertDirectory(decodeURIComponent(headers.get(\"x-opencode-directory\") ?? \"\"))\n return body\n }\n if (method === \"GET\" && /^\\/api\\/session\\/?$/.test(url.pathname)) {\n // The cursor is the complete native query, not a location selector to amend.\n if (url.searchParams.has(\"cursor\")) throw new Error(\"Cursor already carries session list scope\")\n assertDirectory(url.searchParams.get(\"directory\"))\n return body\n }\n if (/^\\/api\\/credential\\/[^/]+(?:\\/activate)?\\/?$/.test(url.pathname)) {\n // Credential mutations are global; context must not create location scope.\n return body\n }\n const sessionRoute = url.pathname.replace(/\\/+$/, \"\").match(/^\\/api\\/(?:experimental\\/)?session\\/([^/]+)(?:\\/.*)?$/)\n if (sessionRoute && sessionRoute[1] !== \"active\" && sessionRoute[1] !== \"import\") {\n // A specific session's routes carry authority in the session identity,\n // already ownership-checked by the proxy via session.get (which excludes\n // the active/import pseudo-identities the same way). These native routes\n // accept no location slot, so an ambient location header must not fail\n // them here (for example instructions entries on every send). This check\n // stays after the move/create/import/form/list handlers above, which do\n // validate their own location slots.\n return body\n }\n const directory = url.searchParams.get(\"location[directory]\")\n assertDirectory(directory)\n return body\n}\n"]}
|
|
@@ -67,6 +67,27 @@ test("move, creation and import validate context without reconstructing legacy b
|
|
|
67
67
|
const cursorUrl = new URL("http://localhost/api/session?cursor=native&directory=/repo");
|
|
68
68
|
assert.throws(() => applyLocationContext(cursorUrl, "GET", undefined, headers()), /Cursor already carries/);
|
|
69
69
|
});
|
|
70
|
+
test("session routes ignore ambient location context without a native slot", () => {
|
|
71
|
+
const headers = () => new Headers(locationRequestOptions({ directory: "/repo" }, { includeDirectory: true }).headers);
|
|
72
|
+
for (const [method, pathname, body] of [
|
|
73
|
+
["DELETE", "/api/experimental/session/ses_1/instructions/entries/codenomad.voice-mode", undefined],
|
|
74
|
+
["PUT", "/api/experimental/session/ses_1/instructions/entries/codenomad.voice-mode", { value: "x" }],
|
|
75
|
+
["GET", "/api/experimental/session/ses_1/instructions/entries", undefined],
|
|
76
|
+
["POST", "/api/session/ses_1/prompt", { text: "hi" }],
|
|
77
|
+
["GET", "/api/session/ses_1", undefined],
|
|
78
|
+
["GET", "/api/session/ses_1/", undefined],
|
|
79
|
+
["DELETE", "/api/session/ses_1", undefined],
|
|
80
|
+
["PATCH", "/api/session/ses_1/", { title: "renamed" }],
|
|
81
|
+
]) {
|
|
82
|
+
const requestHeaders = headers();
|
|
83
|
+
assert.equal(applyLocationContext(new URL(`http://localhost${pathname}`), method, body, requestHeaders), body);
|
|
84
|
+
assert.equal(requestHeaders.has(LOCATION_CONTEXT_HEADER), false);
|
|
85
|
+
}
|
|
86
|
+
// Pseudo-identities and session-list routes keep the strict directory check.
|
|
87
|
+
for (const pathname of ["/api/session/active/", "/api/experimental/session/import/", "/api/session"]) {
|
|
88
|
+
assert.throws(() => applyLocationContext(new URL(`http://localhost${pathname}`), "GET", undefined, headers()), /does not match/);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
70
91
|
test("directory-only credential context remains global", async () => {
|
|
71
92
|
const endpoint = { url: "http://localhost:1234" };
|
|
72
93
|
rememberRuntime(endpoint, { version: "2.0.11", pid: 1, discovery: "info" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"location.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/location.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { OpenCode } from \"@opencode/client\"\nimport { createRuntimeFetch } from \"./transport\"\nimport { rememberRuntime } from \"./runtime\"\nimport {\n LOCATION_CONTEXT_HEADER, applyLocationContext, locationRequestOptions,\n moveSessionToLocation, readLocationContext, readLocationRef, sameLocation,\n} from \"./location\"\n\ntest(\"generated calls preserve obsolete identity for rejection before upstream dispatch\", async () => {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version: \"2.0.11\", pid: 1, discovery: \"info\" })\n const location = { directory: \"/work/100% 工作\", workspaceID: \"workspace-1\" }\n let calls = 0\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async () => {\n calls++\n return new Response(null, { status: 204 })\n }) })\n const options = locationRequestOptions(location)\n await assert.rejects(moveSessionToLocation(client, \"s\", location))\n await assert.rejects(client.session.list({ directory: location.directory }, options))\n await assert.rejects(client.rpc.call({ rpcID: \"fixture\", method: \"check\", input: {}, location: { directory: location.directory } }, options))\n await assert.rejects(client.debug.location.evict({ location: { directory: location.directory } }, options))\n await assert.rejects(client.credential.remove({ credentialID: \"credential\" }, options))\n assert.equal(calls, 0)\n})\n\ntest(\"context parser accepts explicit directories and refuses malformed or obsolete selectors\", () => {\n const location = { directory: \"/repo\" }\n const encoded = locationRequestOptions(location, { includeDirectory: true })!.headers[LOCATION_CONTEXT_HEADER]\n assert.deepEqual(readLocationContext(encoded), location)\n assert.equal(locationRequestOptions(location), undefined)\n const obsolete = locationRequestOptions({ ...location, workspaceID: \"w\" })!.headers[LOCATION_CONTEXT_HEADER]\n assert.throws(() => readLocationContext(obsolete), /Unsupported/)\n assert.throws(() => readLocationContext([encoded, encoded]))\n assert.throws(() => readLocationContext(\"%\"))\n assert.throws(() => readLocationContext(encodeURIComponent('{\"directory\":\"/repo\",\"workspace\":\"w\"}')))\n assert.throws(() => readLocationRef({ directory: \"/repo\", workspaceID: null }))\n assert.equal(sameLocation({ directory: \"/repo\", workspaceID: \"one\" }, { directory: \"/repo\", workspaceID: \"two\" }), false)\n})\n\ntest(\"global Forms and catalog contexts validate directory scope and strip private headers\", () => {\n const ref = { directory: \"/工作/100% ready\" }\n const options = locationRequestOptions(ref, { includeDirectory: true })!\n const headers = new Headers({ ...options.headers, \"x-opencode-directory\": encodeURIComponent(ref.directory) })\n const answer = { answer: {} }\n assert.equal(applyLocationContext(new URL(\"http://localhost/api/session/global/form/f/reply\"), \"POST\", answer, headers), answer)\n assert.equal(headers.has(\"x-opencode-workspace\"), false)\n assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false)\n const url = new URL(\"http://localhost/api/form\")\n url.searchParams.set(\"location[directory]\", ref.directory)\n applyLocationContext(url, \"GET\", undefined, new Headers(options.headers))\n assert.equal(url.searchParams.has(\"location[workspace]\"), false)\n const foreign = new URL(\"http://localhost/api/form?location[directory]=/other\")\n assert.throws(() => applyLocationContext(foreign, \"GET\", undefined, new Headers(options.headers)), /does not match/)\n const foreignForm = new Headers({ ...options.headers, \"x-opencode-directory\": \"/other\" })\n assert.throws(() => applyLocationContext(new URL(\"http://localhost/api/session/global/form/f\"), \"DELETE\", undefined, foreignForm), /does not match/)\n url.searchParams.set(\"location[workspace]\", \"old\")\n assert.throws(() => applyLocationContext(url, \"GET\", undefined, new Headers(options.headers)), /Unsupported/)\n})\n\ntest(\"move, creation and import validate context without reconstructing legacy body fields\", () => {\n const headers = () => new Headers(locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true })!.headers)\n for (const [pathname, body] of [\n [\"/api/session/s/move\", { directory: \"/repo\" }],\n [\"/api/session\", { location: { directory: \"/repo\" } }],\n [\"/api/experimental/session/import\", { location: { directory: \"/repo\" }, messages: [] }],\n ] as const) {\n assert.equal(applyLocationContext(new URL(`http://localhost${pathname}`), \"POST\", body, headers()), body)\n const obsolete = \"location\" in body ? { ...body, location: { directory: \"/repo\", workspaceID: \"old\" } } : { ...body, workspaceID: \"old\" }\n assert.throws(() => applyLocationContext(new URL(`http://localhost${pathname}`), \"POST\", obsolete, headers()), /Unsupported/)\n }\n const cursorUrl = new URL(\"http://localhost/api/session?cursor=native&directory=/repo\")\n assert.throws(() => applyLocationContext(cursorUrl, \"GET\", undefined, headers()), /Cursor already carries/)\n})\n\ntest(\"directory-only credential context remains global\", async () => {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version: \"2.0.11\", pid: 1, discovery: \"info\" })\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {\n const request = new Request(input, init)\n assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false)\n assert.equal(new URL(request.url).search, \"\")\n return new Response(null, { status: 204 })\n }) })\n await client.credential.remove({ credentialID: \"credential\" }, locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true }))\n})\n\ntest(\"generated worktree methods preserve project payloads without location serialization\", async () => {\n const endpoint = { url: \"http://localhost:4321\", auth: { type: \"basic\" as const, username: \"fixture\", password: \"fixture\" } }\n rememberRuntime(endpoint, { version: \"2.0.11\", pid: 1, discovery: \"info\" })\n const requests: Array<{ method: string; url: URL; body: any }> = []\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {\n const request = new Request(input, init)\n assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false)\n requests.push({ method: request.method, url: new URL(request.url), body: request.body ? await request.json() : undefined })\n if (request.method === \"GET\") return Response.json([{ directory: \"/repo\", strategy: \"git\" }])\n if (request.method === \"POST\" && !request.url.endsWith(\"/refresh\")) return Response.json({ directory: \"/repo/.codenomad/worktrees/new\" })\n return new Response(null, { status: 204 })\n }) })\n const options = locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true })\n await client.worktree.list({ projectID: \"project\" }, options)\n await client.worktree.create({ projectID: \"project\", from: \"/source\", branch: \"revision\", directory: \"/repo/.codenomad/worktrees\", name: \"new\" }, options)\n await client.worktree.remove({ projectID: \"project\", directory: \"/repo/.codenomad/worktrees/new\", force: false }, options)\n await client.worktree.refresh({ projectID: \"project\" }, options)\n for (const request of requests) {\n assert.equal(request.url.searchParams.has(\"location[directory]\"), false)\n assert.equal(request.url.searchParams.has(\"location[workspace]\"), false)\n if (request.method === \"GET\") assert.equal(request.url.searchParams.get(\"projectID\"), \"project\")\n else assert.equal(request.body.projectID, \"project\")\n }\n assert.equal(requests[1].body.branch, \"revision\")\n assert.equal(requests[1].body.from, \"/source\")\n assert.equal(requests[2].body.directory, \"/repo/.codenomad/worktrees/new\")\n assert.equal(requests[2].body.force, false)\n})\n"]}
|
|
1
|
+
{"version":3,"file":"location.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/location.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { OpenCode } from \"@opencode/client\"\nimport { createRuntimeFetch } from \"./transport\"\nimport { rememberRuntime } from \"./runtime\"\nimport {\n LOCATION_CONTEXT_HEADER, applyLocationContext, locationRequestOptions,\n moveSessionToLocation, readLocationContext, readLocationRef, sameLocation,\n} from \"./location\"\n\ntest(\"generated calls preserve obsolete identity for rejection before upstream dispatch\", async () => {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version: \"2.0.11\", pid: 1, discovery: \"info\" })\n const location = { directory: \"/work/100% 工作\", workspaceID: \"workspace-1\" }\n let calls = 0\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async () => {\n calls++\n return new Response(null, { status: 204 })\n }) })\n const options = locationRequestOptions(location)\n await assert.rejects(moveSessionToLocation(client, \"s\", location))\n await assert.rejects(client.session.list({ directory: location.directory }, options))\n await assert.rejects(client.rpc.call({ rpcID: \"fixture\", method: \"check\", input: {}, location: { directory: location.directory } }, options))\n await assert.rejects(client.debug.location.evict({ location: { directory: location.directory } }, options))\n await assert.rejects(client.credential.remove({ credentialID: \"credential\" }, options))\n assert.equal(calls, 0)\n})\n\ntest(\"context parser accepts explicit directories and refuses malformed or obsolete selectors\", () => {\n const location = { directory: \"/repo\" }\n const encoded = locationRequestOptions(location, { includeDirectory: true })!.headers[LOCATION_CONTEXT_HEADER]\n assert.deepEqual(readLocationContext(encoded), location)\n assert.equal(locationRequestOptions(location), undefined)\n const obsolete = locationRequestOptions({ ...location, workspaceID: \"w\" })!.headers[LOCATION_CONTEXT_HEADER]\n assert.throws(() => readLocationContext(obsolete), /Unsupported/)\n assert.throws(() => readLocationContext([encoded, encoded]))\n assert.throws(() => readLocationContext(\"%\"))\n assert.throws(() => readLocationContext(encodeURIComponent('{\"directory\":\"/repo\",\"workspace\":\"w\"}')))\n assert.throws(() => readLocationRef({ directory: \"/repo\", workspaceID: null }))\n assert.equal(sameLocation({ directory: \"/repo\", workspaceID: \"one\" }, { directory: \"/repo\", workspaceID: \"two\" }), false)\n})\n\ntest(\"global Forms and catalog contexts validate directory scope and strip private headers\", () => {\n const ref = { directory: \"/工作/100% ready\" }\n const options = locationRequestOptions(ref, { includeDirectory: true })!\n const headers = new Headers({ ...options.headers, \"x-opencode-directory\": encodeURIComponent(ref.directory) })\n const answer = { answer: {} }\n assert.equal(applyLocationContext(new URL(\"http://localhost/api/session/global/form/f/reply\"), \"POST\", answer, headers), answer)\n assert.equal(headers.has(\"x-opencode-workspace\"), false)\n assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false)\n const url = new URL(\"http://localhost/api/form\")\n url.searchParams.set(\"location[directory]\", ref.directory)\n applyLocationContext(url, \"GET\", undefined, new Headers(options.headers))\n assert.equal(url.searchParams.has(\"location[workspace]\"), false)\n const foreign = new URL(\"http://localhost/api/form?location[directory]=/other\")\n assert.throws(() => applyLocationContext(foreign, \"GET\", undefined, new Headers(options.headers)), /does not match/)\n const foreignForm = new Headers({ ...options.headers, \"x-opencode-directory\": \"/other\" })\n assert.throws(() => applyLocationContext(new URL(\"http://localhost/api/session/global/form/f\"), \"DELETE\", undefined, foreignForm), /does not match/)\n url.searchParams.set(\"location[workspace]\", \"old\")\n assert.throws(() => applyLocationContext(url, \"GET\", undefined, new Headers(options.headers)), /Unsupported/)\n})\n\ntest(\"move, creation and import validate context without reconstructing legacy body fields\", () => {\n const headers = () => new Headers(locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true })!.headers)\n for (const [pathname, body] of [\n [\"/api/session/s/move\", { directory: \"/repo\" }],\n [\"/api/session\", { location: { directory: \"/repo\" } }],\n [\"/api/experimental/session/import\", { location: { directory: \"/repo\" }, messages: [] }],\n ] as const) {\n assert.equal(applyLocationContext(new URL(`http://localhost${pathname}`), \"POST\", body, headers()), body)\n const obsolete = \"location\" in body ? { ...body, location: { directory: \"/repo\", workspaceID: \"old\" } } : { ...body, workspaceID: \"old\" }\n assert.throws(() => applyLocationContext(new URL(`http://localhost${pathname}`), \"POST\", obsolete, headers()), /Unsupported/)\n }\n const cursorUrl = new URL(\"http://localhost/api/session?cursor=native&directory=/repo\")\n assert.throws(() => applyLocationContext(cursorUrl, \"GET\", undefined, headers()), /Cursor already carries/)\n})\n\ntest(\"session routes ignore ambient location context without a native slot\", () => {\n const headers = () => new Headers(locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true })!.headers)\n for (const [method, pathname, body] of [\n [\"DELETE\", \"/api/experimental/session/ses_1/instructions/entries/codenomad.voice-mode\", undefined],\n [\"PUT\", \"/api/experimental/session/ses_1/instructions/entries/codenomad.voice-mode\", { value: \"x\" }],\n [\"GET\", \"/api/experimental/session/ses_1/instructions/entries\", undefined],\n [\"POST\", \"/api/session/ses_1/prompt\", { text: \"hi\" }],\n [\"GET\", \"/api/session/ses_1\", undefined],\n [\"GET\", \"/api/session/ses_1/\", undefined],\n [\"DELETE\", \"/api/session/ses_1\", undefined],\n [\"PATCH\", \"/api/session/ses_1/\", { title: \"renamed\" }],\n ] as const) {\n const requestHeaders = headers()\n assert.equal(applyLocationContext(new URL(`http://localhost${pathname}`), method, body, requestHeaders), body)\n assert.equal(requestHeaders.has(LOCATION_CONTEXT_HEADER), false)\n }\n // Pseudo-identities and session-list routes keep the strict directory check.\n for (const pathname of [\"/api/session/active/\", \"/api/experimental/session/import/\", \"/api/session\"]) {\n assert.throws(() => applyLocationContext(\n new URL(`http://localhost${pathname}`), \"GET\", undefined, headers(),\n ), /does not match/)\n }\n})\n\ntest(\"directory-only credential context remains global\", async () => {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version: \"2.0.11\", pid: 1, discovery: \"info\" })\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {\n const request = new Request(input, init)\n assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false)\n assert.equal(new URL(request.url).search, \"\")\n return new Response(null, { status: 204 })\n }) })\n await client.credential.remove({ credentialID: \"credential\" }, locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true }))\n})\n\ntest(\"generated worktree methods preserve project payloads without location serialization\", async () => {\n const endpoint = { url: \"http://localhost:4321\", auth: { type: \"basic\" as const, username: \"fixture\", password: \"fixture\" } }\n rememberRuntime(endpoint, { version: \"2.0.11\", pid: 1, discovery: \"info\" })\n const requests: Array<{ method: string; url: URL; body: any }> = []\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {\n const request = new Request(input, init)\n assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false)\n requests.push({ method: request.method, url: new URL(request.url), body: request.body ? await request.json() : undefined })\n if (request.method === \"GET\") return Response.json([{ directory: \"/repo\", strategy: \"git\" }])\n if (request.method === \"POST\" && !request.url.endsWith(\"/refresh\")) return Response.json({ directory: \"/repo/.codenomad/worktrees/new\" })\n return new Response(null, { status: 204 })\n }) })\n const options = locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true })\n await client.worktree.list({ projectID: \"project\" }, options)\n await client.worktree.create({ projectID: \"project\", from: \"/source\", branch: \"revision\", directory: \"/repo/.codenomad/worktrees\", name: \"new\" }, options)\n await client.worktree.remove({ projectID: \"project\", directory: \"/repo/.codenomad/worktrees/new\", force: false }, options)\n await client.worktree.refresh({ projectID: \"project\" }, options)\n for (const request of requests) {\n assert.equal(request.url.searchParams.has(\"location[directory]\"), false)\n assert.equal(request.url.searchParams.has(\"location[workspace]\"), false)\n if (request.method === \"GET\") assert.equal(request.url.searchParams.get(\"projectID\"), \"project\")\n else assert.equal(request.body.projectID, \"project\")\n }\n assert.equal(requests[1].body.branch, \"revision\")\n assert.equal(requests[1].body.from, \"/source\")\n assert.equal(requests[2].body.directory, \"/repo/.codenomad/worktrees/new\")\n assert.equal(requests[2].body.force, false)\n})\n"]}
|
|
@@ -216,6 +216,33 @@ describe("instance proxy location enforcement", () => {
|
|
|
216
216
|
assert.equal(response.statusCode, 403);
|
|
217
217
|
assert.equal(requestCount(), 0);
|
|
218
218
|
});
|
|
219
|
+
it("proxies session instruction entries with or without ambient location context", async () => {
|
|
220
|
+
const { app, requestCount, sessionGets } = await harness();
|
|
221
|
+
const ambient = { "x-codenomad-location": encodeURIComponent(JSON.stringify({ directory: "/repo/worktree" })) };
|
|
222
|
+
const remove = await app.inject({ method: "DELETE",
|
|
223
|
+
url: "/workspaces/workspace/instance/api/experimental/session/session/instructions/entries/codenomad.voice-mode",
|
|
224
|
+
headers: ambient });
|
|
225
|
+
assert.equal(remove.statusCode, 200);
|
|
226
|
+
assert.equal(JSON.parse(remove.body).url, "/api/experimental/session/session/instructions/entries/codenomad.voice-mode");
|
|
227
|
+
assert.equal(JSON.parse(remove.body).headers["x-codenomad-location"], undefined);
|
|
228
|
+
const put = await app.inject({ method: "PUT",
|
|
229
|
+
url: "/workspaces/workspace/instance/api/experimental/session/session/instructions/entries/codenomad.voice-mode",
|
|
230
|
+
payload: { value: "x" }, headers: ambient });
|
|
231
|
+
assert.equal(put.statusCode, 200);
|
|
232
|
+
// The generated list route must be reachable through the workspace as well.
|
|
233
|
+
const list = await app.inject({ method: "GET",
|
|
234
|
+
url: "/workspaces/workspace/instance/api/experimental/session/session/instructions/entries" });
|
|
235
|
+
assert.equal(list.statusCode, 200);
|
|
236
|
+
assert.deepEqual(sessionGets, ["session", "session", "session"]);
|
|
237
|
+
assert.equal(requestCount(), 3);
|
|
238
|
+
});
|
|
239
|
+
it("rejects instruction entries for sessions belonging to another workspace", async () => {
|
|
240
|
+
const { app, requestCount } = await harness("/repo/worktree", {}, { session: "/other" });
|
|
241
|
+
const response = await app.inject({ method: "DELETE",
|
|
242
|
+
url: "/workspaces/workspace/instance/api/experimental/session/session/instructions/entries/codenomad.voice-mode" });
|
|
243
|
+
assert.equal(response.statusCode, 403);
|
|
244
|
+
assert.equal(requestCount(), 0);
|
|
245
|
+
});
|
|
219
246
|
it("filters the project list and its sandboxes to the workspace", async () => {
|
|
220
247
|
const { app, requestCount } = await harness();
|
|
221
248
|
const response = await app.inject({ method: "GET", url: "/workspaces/workspace/instance/api/project" });
|