@neuralnomads/codenomad-dev 0.20.0-dev-20260911-ea37f97b → 0.20.0-dev-20260917-aa51cbb9

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.
Files changed (191) hide show
  1. package/README.md +2 -2
  2. package/dist/api-types.js.map +1 -1
  3. package/dist/opencode/automation-plugin.test.js +32 -38
  4. package/dist/opencode/automation-plugin.test.js.map +1 -1
  5. package/dist/opencode/compatibility/contract-fixture.js +15 -0
  6. package/dist/opencode/compatibility/contract-fixture.js.map +1 -0
  7. package/dist/opencode/compatibility/events.js +9 -0
  8. package/dist/opencode/compatibility/events.js.map +1 -0
  9. package/dist/opencode/compatibility/location.js +89 -0
  10. package/dist/opencode/compatibility/location.js.map +1 -0
  11. package/dist/opencode/compatibility/location.test.js +92 -0
  12. package/dist/opencode/compatibility/location.test.js.map +1 -0
  13. package/dist/opencode/compatibility/negotiate.js +73 -0
  14. package/dist/opencode/compatibility/negotiate.js.map +1 -0
  15. package/dist/opencode/compatibility/negotiate.test.js +61 -0
  16. package/dist/opencode/compatibility/negotiate.test.js.map +1 -0
  17. package/dist/opencode/compatibility/proxy-locations.js +137 -0
  18. package/dist/opencode/compatibility/proxy-locations.js.map +1 -0
  19. package/dist/opencode/compatibility/proxy-locations.test.js +42 -0
  20. package/dist/opencode/compatibility/proxy-locations.test.js.map +1 -0
  21. package/dist/opencode/compatibility/proxy.js +60 -0
  22. package/dist/opencode/compatibility/proxy.js.map +1 -0
  23. package/dist/opencode/compatibility/proxy.test.js +47 -0
  24. package/dist/opencode/compatibility/proxy.test.js.map +1 -0
  25. package/dist/opencode/compatibility/requests.js +54 -0
  26. package/dist/opencode/compatibility/requests.js.map +1 -0
  27. package/dist/opencode/compatibility/runtime.js +26 -0
  28. package/dist/opencode/compatibility/runtime.js.map +1 -0
  29. package/dist/opencode/compatibility/transport.js +112 -0
  30. package/dist/opencode/compatibility/transport.js.map +1 -0
  31. package/dist/opencode/compatibility/transport.test.js +83 -0
  32. package/dist/opencode/compatibility/transport.test.js.map +1 -0
  33. package/dist/opencode/session-pruning/location.js +16 -0
  34. package/dist/opencode/session-pruning/location.js.map +1 -0
  35. package/dist/opencode/session-pruning/package.test.js +37 -0
  36. package/dist/opencode/session-pruning/package.test.js.map +1 -0
  37. package/dist/opencode/session-pruning/plugin.js +5 -2
  38. package/dist/opencode/session-pruning/plugin.js.map +1 -1
  39. package/dist/opencode/session-pruning/preview-store.js +3 -2
  40. package/dist/opencode/session-pruning/preview-store.js.map +1 -1
  41. package/dist/opencode/session-pruning/pruning.test.js +7 -5
  42. package/dist/opencode/session-pruning/pruning.test.js.map +1 -1
  43. package/dist/opencode/session-pruning/service.js +5 -4
  44. package/dist/opencode/session-pruning/service.js.map +1 -1
  45. package/dist/opencode/session-pruning/service.test.js +65 -53
  46. package/dist/opencode/session-pruning/service.test.js.map +1 -1
  47. package/dist/permissions/opencode-replier.js +2 -2
  48. package/dist/permissions/opencode-replier.js.map +1 -1
  49. package/dist/permissions/opencode-replier.test.js +1 -1
  50. package/dist/permissions/opencode-replier.test.js.map +1 -1
  51. package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
  52. package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
  53. package/dist/plugins/session-pruning/plugin.mjs +422 -271
  54. package/dist/server/__tests__/instance-proxy-legacy.test.js +111 -0
  55. package/dist/server/__tests__/instance-proxy-legacy.test.js.map +1 -0
  56. package/dist/server/__tests__/instance-proxy.test.js +95 -34
  57. package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
  58. package/dist/server/http-server.js +147 -237
  59. package/dist/server/http-server.js.map +1 -1
  60. package/dist/server/routes/session-pruning.js +4 -2
  61. package/dist/server/routes/session-pruning.js.map +1 -1
  62. package/dist/server/routes/session-pruning.test.js +16 -2
  63. package/dist/server/routes/session-pruning.test.js.map +1 -1
  64. package/dist/server/routes/worktrees.js +1 -0
  65. package/dist/server/routes/worktrees.js.map +1 -1
  66. package/dist/server/routes/worktrees.test.js +54 -1
  67. package/dist/server/routes/worktrees.test.js.map +1 -1
  68. package/dist/workspaces/host-opencode-service.test.js +56 -7
  69. package/dist/workspaces/host-opencode-service.test.js.map +1 -1
  70. package/dist/workspaces/instance-client.js.map +1 -1
  71. package/dist/workspaces/instance-events.js +71 -57
  72. package/dist/workspaces/instance-events.js.map +1 -1
  73. package/dist/workspaces/instance-events.test.js +122 -9
  74. package/dist/workspaces/instance-events.test.js.map +1 -1
  75. package/dist/workspaces/manager.js +24 -39
  76. package/dist/workspaces/manager.js.map +1 -1
  77. package/dist/workspaces/manager.test.js +43 -7
  78. package/dist/workspaces/manager.test.js.map +1 -1
  79. package/dist/workspaces/opencode-cli-service.js +56 -29
  80. package/dist/workspaces/opencode-cli-service.js.map +1 -1
  81. package/dist/workspaces/opencode-connection-generation.test.js +57 -0
  82. package/dist/workspaces/opencode-connection-generation.test.js.map +1 -0
  83. package/dist/workspaces/opencode-service.js +70 -16
  84. package/dist/workspaces/opencode-service.js.map +1 -1
  85. package/dist/workspaces/opencode-service.test.js +3 -4
  86. package/dist/workspaces/opencode-service.test.js.map +1 -1
  87. package/dist/workspaces/worktree-session-evacuation.js +31 -11
  88. package/dist/workspaces/worktree-session-evacuation.js.map +1 -1
  89. package/dist/workspaces/worktree-session-evacuation.test.js +211 -0
  90. package/dist/workspaces/worktree-session-evacuation.test.js.map +1 -1
  91. package/dist/workspaces/wsl-opencode-service.test.js +64 -24
  92. package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
  93. package/package.json +3 -3
  94. package/public/assets/App-BROhwPo9.js +54 -0
  95. package/public/assets/App-BROhwPo9.js.map +1 -0
  96. package/public/assets/{FilesTab-CRpGDs1B.js → FilesTab-DY3Vr-3r.js} +3 -3
  97. package/public/assets/{FilesTab-CRpGDs1B.js.map → FilesTab-DY3Vr-3r.js.map} +1 -1
  98. package/public/assets/{GitChangesTab-DxFebTiq.js → GitChangesTab-BD6CJngE.js} +3 -3
  99. package/public/assets/{GitChangesTab-DxFebTiq.js.map → GitChangesTab-BD6CJngE.js.map} +1 -1
  100. package/public/assets/{SplitFilePanel-DgTMrrO-.js → SplitFilePanel-BdY6d_6m.js} +2 -2
  101. package/public/assets/{SplitFilePanel-DgTMrrO-.js.map → SplitFilePanel-BdY6d_6m.js.map} +1 -1
  102. package/public/assets/StatusTab-C-_2yaPa.js +2 -0
  103. package/public/assets/StatusTab-C-_2yaPa.js.map +1 -0
  104. package/public/assets/{align-justify-v_yPXSys.js → align-justify-DI8VTZ4G.js} +2 -2
  105. package/public/assets/{align-justify-v_yPXSys.js.map → align-justify-DI8VTZ4G.js.map} +1 -1
  106. package/public/assets/{bundle-full-HzL_Jtqk.js → bundle-full-D_M65Eb_.js} +2 -2
  107. package/public/assets/{bundle-full-HzL_Jtqk.js.map → bundle-full-D_M65Eb_.js.map} +1 -1
  108. package/public/assets/{client-state-CF-PJ0GG.js → client-state-CWCzCOz6.js} +2 -2
  109. package/public/assets/{client-state-CF-PJ0GG.js.map → client-state-CWCzCOz6.js.map} +1 -1
  110. package/public/assets/{diff-viewer-BFDQBDVy.js → diff-viewer-CGPsYEr3.js} +2 -2
  111. package/public/assets/{diff-viewer-BFDQBDVy.js.map → diff-viewer-CGPsYEr3.js.map} +1 -1
  112. package/public/assets/index-B8-lAqjr.js +2 -0
  113. package/public/assets/index-B8-lAqjr.js.map +1 -0
  114. package/public/assets/index-BeUewQym.js +2 -0
  115. package/public/assets/index-BeUewQym.js.map +1 -0
  116. package/public/assets/index-BxRFHpJd.js +2 -0
  117. package/public/assets/{index-D2UHSOG5.js.map → index-BxRFHpJd.js.map} +1 -1
  118. package/public/assets/index-Bzv1kU-S.js +2 -0
  119. package/public/assets/{index-CTKYD_VJ.js.map → index-Bzv1kU-S.js.map} +1 -1
  120. package/public/assets/index-C9tI4CRG.js +2 -0
  121. package/public/assets/index-C9tI4CRG.js.map +1 -0
  122. package/public/assets/index-CGYHFZ6F.js +2 -0
  123. package/public/assets/index-CGYHFZ6F.js.map +1 -0
  124. package/public/assets/{index-CHYmKl10.js → index-CMEAjBFp.js} +2 -2
  125. package/public/assets/index-CMEAjBFp.js.map +1 -0
  126. package/public/assets/index-Cazypvyc.js +2 -0
  127. package/public/assets/index-Cazypvyc.js.map +1 -0
  128. package/public/assets/index-CwqzofWP.js +2 -0
  129. package/public/assets/index-CwqzofWP.js.map +1 -0
  130. package/public/assets/index-Da9yOa6q.js +2 -0
  131. package/public/assets/index-Da9yOa6q.js.map +1 -0
  132. package/public/assets/{index-RK9gf0uk.js → index-Ddjkirek.js} +2 -2
  133. package/public/assets/{index-RK9gf0uk.js.map → index-Ddjkirek.js.map} +1 -1
  134. package/public/assets/index-HHgvEOAH.js +3 -0
  135. package/public/assets/index-HHgvEOAH.js.map +1 -0
  136. package/public/assets/index-MeFffHqb.css +1 -0
  137. package/public/assets/loading-CdhHiSED.css +1 -0
  138. package/public/assets/{loading-BSB82snY.js → loading-Gqh6NPUr.js} +2 -2
  139. package/public/assets/{loading-BSB82snY.js.map → loading-Gqh6NPUr.js.map} +1 -1
  140. package/public/assets/main-Cbx7KdTr.js +38 -0
  141. package/public/assets/main-Cbx7KdTr.js.map +1 -0
  142. package/public/assets/{markdown-DtidoL2Z.js → markdown-BWnjyvnX.js} +4 -4
  143. package/public/assets/{markdown-DtidoL2Z.js.map → markdown-BWnjyvnX.js.map} +1 -1
  144. package/public/assets/monaco-viewer-BEJIiZWL.js +27 -0
  145. package/public/assets/monaco-viewer-BEJIiZWL.js.map +1 -0
  146. package/public/assets/{startup-state-settings-card-BY741Rt0.js → startup-state-settings-card-C4E2Itop.js} +2 -2
  147. package/public/assets/{startup-state-settings-card-BY741Rt0.js.map → startup-state-settings-card-C4E2Itop.js.map} +1 -1
  148. package/public/assets/{terminal-square-BdC_jwm3.js → terminal-square-C3wutdV0.js} +2 -2
  149. package/public/assets/{terminal-square-BdC_jwm3.js.map → terminal-square-C3wutdV0.js.map} +1 -1
  150. package/public/assets/tool-call-DNsAQ_m4.js +60 -0
  151. package/public/assets/tool-call-DNsAQ_m4.js.map +1 -0
  152. package/public/assets/{unified-picker-C0eo1chJ.js → unified-picker-tB7y17Qz.js} +2 -2
  153. package/public/assets/unified-picker-tB7y17Qz.js.map +1 -0
  154. package/public/assets/{wrap-text-BuMtdvMx.js → wrap-text-CkBxzt04.js} +2 -2
  155. package/public/assets/{wrap-text-BuMtdvMx.js.map → wrap-text-CkBxzt04.js.map} +1 -1
  156. package/public/index.html +4 -4
  157. package/public/loading.html +5 -5
  158. package/public/sw.js +1 -1
  159. package/public/sw.js.map +1 -1
  160. package/public/assets/App-0-FcHzPS.js +0 -44
  161. package/public/assets/App-0-FcHzPS.js.map +0 -1
  162. package/public/assets/StatusTab-LKUA9cbH.js +0 -2
  163. package/public/assets/StatusTab-LKUA9cbH.js.map +0 -1
  164. package/public/assets/index-8iPlm2r0.js +0 -2
  165. package/public/assets/index-8iPlm2r0.js.map +0 -1
  166. package/public/assets/index-B185mC7m.js +0 -2
  167. package/public/assets/index-B185mC7m.js.map +0 -1
  168. package/public/assets/index-Bbj1xF8i.js +0 -2
  169. package/public/assets/index-Bbj1xF8i.js.map +0 -1
  170. package/public/assets/index-BsmFN3k1.js +0 -2
  171. package/public/assets/index-BsmFN3k1.js.map +0 -1
  172. package/public/assets/index-Bz_OQJYA.css +0 -1
  173. package/public/assets/index-C52CfqK3.js +0 -2
  174. package/public/assets/index-C52CfqK3.js.map +0 -1
  175. package/public/assets/index-CHYmKl10.js.map +0 -1
  176. package/public/assets/index-CTKYD_VJ.js +0 -2
  177. package/public/assets/index-D2UHSOG5.js +0 -2
  178. package/public/assets/index-DDoB_Z36.js +0 -2
  179. package/public/assets/index-DDoB_Z36.js.map +0 -1
  180. package/public/assets/index-DcwAtZw-.js +0 -3
  181. package/public/assets/index-DcwAtZw-.js.map +0 -1
  182. package/public/assets/index-DxUD3RYr.js +0 -2
  183. package/public/assets/index-DxUD3RYr.js.map +0 -1
  184. package/public/assets/loading-CmEVQgyj.css +0 -1
  185. package/public/assets/main-CZztwrfv.js +0 -48
  186. package/public/assets/main-CZztwrfv.js.map +0 -1
  187. package/public/assets/monaco-viewer-CpxDfB-6.js +0 -27
  188. package/public/assets/monaco-viewer-CpxDfB-6.js.map +0 -1
  189. package/public/assets/tool-call-DbtCsJ4x.js +0 -60
  190. package/public/assets/tool-call-DbtCsJ4x.js.map +0 -1
  191. package/public/assets/unified-picker-C0eo1chJ.js.map +0 -1
@@ -0,0 +1,92 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { OpenCode } from "@opencode/client";
4
+ import { createRuntimeFetch } from "./transport";
5
+ import { rememberRuntime } from "./runtime";
6
+ import { LOCATION_CONTEXT_HEADER, applyLocationContext, locationRequestOptions, moveSessionToLocation, readLocationContext, readLocationRef, sameLocation, } from "./location";
7
+ test("identity context survives real modern generated move serialization", async () => {
8
+ const original = { directory: "/work/100% 工作", workspaceID: "workspace-1" };
9
+ let calls = 0;
10
+ const client = OpenCode.make({ baseUrl: "http://localhost:1234", fetch: async (input, init) => {
11
+ calls++;
12
+ const request = new Request(input, init);
13
+ const before = await request.json();
14
+ assert.deepEqual(before, { directory: original.directory });
15
+ const headers = new Headers(request.headers);
16
+ const after = applyLocationContext(new URL(request.url), request.method, before, headers, "legacy");
17
+ assert.deepEqual(after, original);
18
+ assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false);
19
+ return new Response(null, { status: 204 });
20
+ } });
21
+ await moveSessionToLocation(client, "s", original);
22
+ assert.equal(calls, 1);
23
+ });
24
+ test("context parser refuses malformed, duplicate, unknown and modern identity selectors", () => {
25
+ const encoded = locationRequestOptions({ directory: "/repo", workspaceID: "w" }).headers[LOCATION_CONTEXT_HEADER];
26
+ assert.deepEqual(readLocationContext(encoded, "legacy"), { directory: "/repo", workspaceID: "w" });
27
+ for (const profile of ["modern", "unknown"])
28
+ assert.throws(() => readLocationContext(encoded, profile), /Unsupported/);
29
+ assert.throws(() => readLocationContext([encoded, encoded], "legacy"));
30
+ assert.throws(() => readLocationContext("%", "legacy"));
31
+ assert.throws(() => readLocationContext(encodeURIComponent('{"directory":"/repo","workspace":"w"}'), "legacy"));
32
+ assert.throws(() => readLocationRef({ directory: "/repo", workspaceID: null }));
33
+ assert.equal(sameLocation({ directory: "/repo", workspaceID: "one" }, { directory: "/repo", workspaceID: "two" }), false);
34
+ });
35
+ test("global Forms and catalog requests preserve explicit identity without forwarding private headers", () => {
36
+ const ref = { directory: "/工作/100% ready", workspaceID: "worktree" };
37
+ const options = locationRequestOptions(ref);
38
+ const headers = new Headers({ ...options.headers, "x-opencode-directory": encodeURIComponent(ref.directory) });
39
+ applyLocationContext(new URL("http://localhost/api/session/global/form/f/reply"), "POST", { answer: {} }, headers, "legacy");
40
+ assert.equal(headers.get("x-opencode-workspace"), "worktree");
41
+ assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false);
42
+ const url = new URL("http://localhost/api/form");
43
+ url.searchParams.set("location[directory]", ref.directory);
44
+ applyLocationContext(url, "GET", undefined, new Headers(options.headers), "legacy");
45
+ assert.equal(url.searchParams.get("location[workspace]"), "worktree");
46
+ const foreign = new URL("http://localhost/api/form?location[directory]=/other");
47
+ assert.throws(() => applyLocationContext(foreign, "GET", undefined, new Headers(options.headers), "legacy"), /does not match/);
48
+ assert.throws(() => applyLocationContext(url, "GET", undefined, new Headers(options.headers), "modern"), /Unsupported/);
49
+ });
50
+ test("real generated list, RPC, eviction and credential calls use their distinct legacy wire locations", async () => {
51
+ const endpoint = { url: "http://localhost:1234" };
52
+ rememberRuntime(endpoint, { version: "2.0.3", pid: 1, discovery: "health" });
53
+ const location = { directory: "/repo", workspaceID: "wrk_one" };
54
+ const calls = [];
55
+ const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {
56
+ const request = new Request(input, init);
57
+ assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false);
58
+ const url = new URL(request.url);
59
+ calls.push(url);
60
+ if (url.pathname === "/api/session") {
61
+ assert.equal(url.searchParams.get("directory"), "/repo");
62
+ assert.equal(url.searchParams.get("workspace"), "wrk_one");
63
+ assert.equal(url.searchParams.has("location[directory]"), false);
64
+ return Response.json({ data: [], cursor: {} });
65
+ }
66
+ assert.equal(url.searchParams.get("location[directory]"), "/repo");
67
+ assert.equal(url.searchParams.get("location[workspace]"), "wrk_one");
68
+ return url.pathname.startsWith("/api/rpc/") ? Response.json({ output: true }) : new Response(null, { status: 204 });
69
+ }) });
70
+ const options = locationRequestOptions(location);
71
+ await client.session.list({ directory: location.directory }, options);
72
+ await client.rpc.call({ rpcID: "fixture", method: "check", input: {}, location: { directory: location.directory } }, options);
73
+ await client.debug.location.evict({ location: { directory: location.directory } }, options);
74
+ await client.credential.remove({ credentialID: "credential" }, options);
75
+ assert.equal(calls.length, 4);
76
+ await assert.rejects(client.session.list({ cursor: "native-cursor" }, options));
77
+ assert.equal(calls.length, 4, "conflicting cursor context never dispatches");
78
+ });
79
+ test("directory-only credential context is native-scoped on legacy and global on modern", async () => {
80
+ for (const version of ["2.0.3", "2.0.4"]) {
81
+ const endpoint = { url: "http://localhost:1234" };
82
+ rememberRuntime(endpoint, { version, pid: 1, discovery: version === "2.0.3" ? "health" : "status" });
83
+ const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async (input, init) => {
84
+ const request = new Request(input, init);
85
+ assert.equal(request.headers.has(LOCATION_CONTEXT_HEADER), false);
86
+ assert.equal(new URL(request.url).search, version === "2.0.3" ? "?location%5Bdirectory%5D=%2Frepo" : "");
87
+ return new Response(null, { status: 204 });
88
+ }) });
89
+ await client.credential.remove({ credentialID: "credential" }, locationRequestOptions({ directory: "/repo" }, { includeDirectory: true }));
90
+ }
91
+ });
92
+ //# sourceMappingURL=location.test.js.map
@@ -0,0 +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(\"identity context survives real modern generated move serialization\", async () => {\n const original = { directory: \"/work/100% 工作\", workspaceID: \"workspace-1\" }\n let calls = 0\n const client = OpenCode.make({ baseUrl: \"http://localhost:1234\", fetch: async (input, init) => {\n calls++\n const request = new Request(input, init)\n const before = await request.json()\n assert.deepEqual(before, { directory: original.directory })\n const headers = new Headers(request.headers)\n const after = applyLocationContext(new URL(request.url), request.method, before, headers, \"legacy\")\n assert.deepEqual(after, original)\n assert.equal(headers.has(LOCATION_CONTEXT_HEADER), false)\n return new Response(null, { status: 204 })\n } })\n await moveSessionToLocation(client, \"s\", original)\n assert.equal(calls, 1)\n})\n\ntest(\"context parser refuses malformed, duplicate, unknown and modern identity selectors\", () => {\n const encoded = locationRequestOptions({ directory: \"/repo\", workspaceID: \"w\" })!.headers[LOCATION_CONTEXT_HEADER]\n assert.deepEqual(readLocationContext(encoded, \"legacy\"), { directory: \"/repo\", workspaceID: \"w\" })\n for (const profile of [\"modern\", \"unknown\"] as const) assert.throws(() => readLocationContext(encoded, profile), /Unsupported/)\n assert.throws(() => readLocationContext([encoded, encoded], \"legacy\"))\n assert.throws(() => readLocationContext(\"%\", \"legacy\"))\n assert.throws(() => readLocationContext(encodeURIComponent('{\"directory\":\"/repo\",\"workspace\":\"w\"}'), \"legacy\"))\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 requests preserve explicit identity without forwarding private headers\", () => {\n const ref = { directory: \"/工作/100% ready\", workspaceID: \"worktree\" }\n const options = locationRequestOptions(ref)!\n const headers = new Headers({ ...options.headers, \"x-opencode-directory\": encodeURIComponent(ref.directory) })\n applyLocationContext(new URL(\"http://localhost/api/session/global/form/f/reply\"), \"POST\", { answer: {} }, headers, \"legacy\")\n assert.equal(headers.get(\"x-opencode-workspace\"), \"worktree\")\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), \"legacy\")\n assert.equal(url.searchParams.get(\"location[workspace]\"), \"worktree\")\n const foreign = new URL(\"http://localhost/api/form?location[directory]=/other\")\n assert.throws(() => applyLocationContext(foreign, \"GET\", undefined, new Headers(options.headers), \"legacy\"), /does not match/)\n assert.throws(() => applyLocationContext(url, \"GET\", undefined, new Headers(options.headers), \"modern\"), /Unsupported/)\n})\n\ntest(\"real generated list, RPC, eviction and credential calls use their distinct legacy wire locations\", async () => {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version: \"2.0.3\", pid: 1, discovery: \"health\" })\n const location = { directory: \"/repo\", workspaceID: \"wrk_one\" }\n const calls: URL[] = []\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 const url = new URL(request.url)\n calls.push(url)\n if (url.pathname === \"/api/session\") {\n assert.equal(url.searchParams.get(\"directory\"), \"/repo\")\n assert.equal(url.searchParams.get(\"workspace\"), \"wrk_one\")\n assert.equal(url.searchParams.has(\"location[directory]\"), false)\n return Response.json({ data: [], cursor: {} })\n }\n assert.equal(url.searchParams.get(\"location[directory]\"), \"/repo\")\n assert.equal(url.searchParams.get(\"location[workspace]\"), \"wrk_one\")\n return url.pathname.startsWith(\"/api/rpc/\") ? Response.json({ output: true }) : new Response(null, { status: 204 })\n }) })\n const options = locationRequestOptions(location)\n await client.session.list({ directory: location.directory }, options)\n await client.rpc.call({ rpcID: \"fixture\", method: \"check\", input: {}, location: { directory: location.directory } }, options)\n await client.debug.location.evict({ location: { directory: location.directory } }, options)\n await client.credential.remove({ credentialID: \"credential\" }, options)\n assert.equal(calls.length, 4)\n await assert.rejects(client.session.list({ cursor: \"native-cursor\" }, options))\n assert.equal(calls.length, 4, \"conflicting cursor context never dispatches\")\n})\n\ntest(\"directory-only credential context is native-scoped on legacy and global on modern\", async () => {\n for (const version of [\"2.0.3\", \"2.0.4\"]) {\n const endpoint = { url: \"http://localhost:1234\" }\n rememberRuntime(endpoint, { version, pid: 1, discovery: version === \"2.0.3\" ? \"health\" : \"status\" })\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, version === \"2.0.3\" ? \"?location%5Bdirectory%5D=%2Frepo\" : \"\")\n return new Response(null, { status: 204 })\n }) })\n await client.credential.remove({ credentialID: \"credential\" }, locationRequestOptions({ directory: \"/repo\" }, { includeDirectory: true }))\n }\n})\n"]}
@@ -0,0 +1,73 @@
1
+ import { Service } from "@opencode/client/service";
2
+ import { runtimeIdentity } from "./runtime";
3
+ function object(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
5
+ }
6
+ // Unknown release numbers are not a startup/version gate. Read their declared
7
+ // HTTP contract before selecting a serializer; never trial a write operation.
8
+ export async function negotiateRuntime(endpoint, fetcher, signal) {
9
+ const response = await fetcher(new URL("/openapi.json", endpoint.url), {
10
+ headers: Service.headers(endpoint), redirect: "error",
11
+ signal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),
12
+ });
13
+ if (!response.ok) {
14
+ await response.body?.cancel();
15
+ throw new Error(`Cannot negotiate OpenCode contract (HTTP ${response.status})`);
16
+ }
17
+ const reader = response.body?.getReader();
18
+ if (!reader)
19
+ throw new Error("Missing OpenCode contract");
20
+ const chunks = [];
21
+ let length = 0;
22
+ try {
23
+ while (true) {
24
+ const chunk = await reader.read();
25
+ if (chunk.done)
26
+ break;
27
+ length += chunk.value.byteLength;
28
+ if (length > 8 * 1024 * 1024)
29
+ throw new Error("OpenCode contract exceeds limit");
30
+ chunks.push(chunk.value);
31
+ }
32
+ }
33
+ catch (error) {
34
+ await reader.cancel().catch(() => { });
35
+ throw error;
36
+ }
37
+ finally {
38
+ reader.releaseLock();
39
+ }
40
+ const document = object(JSON.parse(Buffer.concat(chunks, length).toString("utf8")));
41
+ const paths = object(document.paths);
42
+ const resolve = (input) => {
43
+ const schema = object(input);
44
+ const ref = schema.$ref;
45
+ return typeof ref === "string" && ref.startsWith("#/components/schemas/")
46
+ ? object(document.components?.schemas?.[ref.slice("#/components/schemas/".length)]) : schema;
47
+ };
48
+ const has = (path, method) => Boolean(object(paths[path])[method]);
49
+ const body = (path, field) => {
50
+ const operation = object(object(paths[path]).post);
51
+ return field in object(resolve(operation.requestBody?.content?.["application/json"]?.schema).properties);
52
+ };
53
+ const session = "/api/session/{sessionID}";
54
+ const inbox = object(resolve(document.components?.schemas?.["Session.Inbox.User"]).properties);
55
+ const modern = has(session, "patch") && has("/api/form", "get")
56
+ && has(`${session}/form/{formID}`, "delete") && has(`/api/experimental/session/{sessionID}/wait`, "post")
57
+ && body(`${session}/fork`, "before") && body(`${session}/command`, "name")
58
+ && body(`${session}/permission/{requestID}/reply`, "decision")
59
+ && "time" in inbox && !("timeCreated" in inbox);
60
+ const legacy = has(`${session}/rename`, "post") && has("/api/form/request", "get")
61
+ && has(`${session}/form/{formID}/cancel`, "post") && has(`${session}/wait`, "post")
62
+ && body(`${session}/fork`, "boundary") && body(`${session}/command`, "command")
63
+ && body(`${session}/permission/{requestID}/reply`, "reply")
64
+ && "timeCreated" in inbox && !("time" in inbox);
65
+ if (modern === legacy)
66
+ throw new Error(`Unsupported OpenCode contract: ${runtimeIdentity(endpoint)?.version ?? "unknown"}`);
67
+ const profile = modern ? "modern" : "legacy";
68
+ const identity = runtimeIdentity(endpoint);
69
+ if (identity?.contract)
70
+ identity.contract.profile = profile;
71
+ return profile;
72
+ }
73
+ //# sourceMappingURL=negotiate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiate.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/negotiate.ts"],"names":[],"mappings":"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","sourcesContent":["import { Service, type Endpoint } from \"@opencode/client/service\"\nimport { runtimeIdentity, type ContractProfile } from \"./runtime\"\n\ntype ObjectValue = Record<string, any>\nfunction object(value: unknown): ObjectValue {\n return value && typeof value === \"object\" && !Array.isArray(value) ? value : {}\n}\n\n// Unknown release numbers are not a startup/version gate. Read their declared\n// HTTP contract before selecting a serializer; never trial a write operation.\nexport async function negotiateRuntime(endpoint: Endpoint, fetcher: typeof fetch, signal: AbortSignal): Promise<Exclude<ContractProfile, \"unknown\">> {\n const response = await fetcher(new URL(\"/openapi.json\", endpoint.url), {\n headers: Service.headers(endpoint), redirect: \"error\",\n signal: AbortSignal.any([signal, AbortSignal.timeout(5_000)]),\n })\n if (!response.ok) {\n await response.body?.cancel()\n throw new Error(`Cannot negotiate OpenCode contract (HTTP ${response.status})`)\n }\n const reader = response.body?.getReader()\n if (!reader) throw new Error(\"Missing OpenCode contract\")\n const chunks: Uint8Array[] = []\n let length = 0\n try {\n while (true) {\n const chunk = await reader.read()\n if (chunk.done) break\n length += chunk.value.byteLength\n if (length > 8 * 1024 * 1024) throw new Error(\"OpenCode contract exceeds limit\")\n chunks.push(chunk.value)\n }\n } catch (error) { await reader.cancel().catch(() => {}); throw error }\n finally { reader.releaseLock() }\n const document = object(JSON.parse(Buffer.concat(chunks, length).toString(\"utf8\")))\n const paths = object(document.paths)\n const resolve = (input: unknown) => {\n const schema = object(input)\n const ref = schema.$ref\n return typeof ref === \"string\" && ref.startsWith(\"#/components/schemas/\")\n ? object(document.components?.schemas?.[ref.slice(\"#/components/schemas/\".length)]) : schema\n }\n const has = (path: string, method: string) => Boolean(object(paths[path])[method])\n const body = (path: string, field: string) => {\n const operation = object(object(paths[path]).post)\n return field in object(resolve(operation.requestBody?.content?.[\"application/json\"]?.schema).properties)\n }\n const session = \"/api/session/{sessionID}\"\n const inbox = object(resolve(document.components?.schemas?.[\"Session.Inbox.User\"]).properties)\n const modern = has(session, \"patch\") && has(\"/api/form\", \"get\")\n && has(`${session}/form/{formID}`, \"delete\") && has(`/api/experimental/session/{sessionID}/wait`, \"post\")\n && body(`${session}/fork`, \"before\") && body(`${session}/command`, \"name\")\n && body(`${session}/permission/{requestID}/reply`, \"decision\")\n && \"time\" in inbox && !(\"timeCreated\" in inbox)\n const legacy = has(`${session}/rename`, \"post\") && has(\"/api/form/request\", \"get\")\n && has(`${session}/form/{formID}/cancel`, \"post\") && has(`${session}/wait`, \"post\")\n && body(`${session}/fork`, \"boundary\") && body(`${session}/command`, \"command\")\n && body(`${session}/permission/{requestID}/reply`, \"reply\")\n && \"timeCreated\" in inbox && !(\"time\" in inbox)\n if (modern === legacy) throw new Error(`Unsupported OpenCode contract: ${runtimeIdentity(endpoint)?.version ?? \"unknown\"}`)\n const profile = modern ? \"modern\" : \"legacy\"\n const identity = runtimeIdentity(endpoint)\n if (identity?.contract) identity.contract.profile = profile\n return profile\n}\n"]}
@@ -0,0 +1,61 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { createRuntimeFetch } from "./transport";
4
+ import { rememberRuntime } from "./runtime";
5
+ import { legacyContractFixture } from "./contract-fixture";
6
+ test("unknown contracts perform authenticated bounded read negotiation before any mutation", async () => {
7
+ for (const kind of ["unauthorized", "unrecognized", "oversized"]) {
8
+ const endpoint = { url: "http://127.0.0.1:4321", auth: { type: "basic", username: "opencode", password: "fixture" } };
9
+ rememberRuntime(endpoint, { version: "2.0.100", pid: 1, discovery: "status" });
10
+ let calls = 0, cancelled = false;
11
+ const transport = createRuntimeFetch(endpoint, async (input, init) => {
12
+ calls++;
13
+ assert.equal(new URL(String(input)).pathname, "/openapi.json");
14
+ assert.equal(init?.method ?? "GET", "GET");
15
+ assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:fixture").toString("base64")}`);
16
+ assert.equal(init?.redirect, "error");
17
+ if (kind === "unauthorized")
18
+ return new Response(null, { status: 401 });
19
+ if (kind === "unrecognized")
20
+ return Response.json({ paths: {} });
21
+ return new Response(new ReadableStream({
22
+ start(controller) { controller.enqueue(new Uint8Array(8 * 1024 * 1024 + 1)); },
23
+ cancel() { cancelled = true; },
24
+ }));
25
+ });
26
+ await assert.rejects(transport(`${endpoint.url}/api/session/s`, { method: "PATCH", body: '{"title":"fixture"}' }), /contract|HTTP 401/);
27
+ assert.equal(calls, 1, "negotiation failure must never dispatch a mutation");
28
+ if (kind === "oversized")
29
+ assert.equal(cancelled, true);
30
+ }
31
+ });
32
+ test("shared negotiation has independent subscriber cancellation and never retries a mutation", async () => {
33
+ const endpoint = { url: "http://127.0.0.1:4321" };
34
+ rememberRuntime(endpoint, { version: "next-contract", pid: 1, discovery: "health" });
35
+ let complete;
36
+ let negotiationSignal;
37
+ const calls = [];
38
+ const transport = createRuntimeFetch(endpoint, async (input, init) => {
39
+ const pathname = new URL(String(input)).pathname;
40
+ calls.push(pathname);
41
+ if (pathname === "/openapi.json") {
42
+ negotiationSignal = init?.signal;
43
+ return new Promise(resolve => { complete = resolve; });
44
+ }
45
+ assert.equal(pathname, "/api/session/s/rename");
46
+ return new Response(null, { status: 204 });
47
+ });
48
+ const first = new AbortController(), third = new AbortController();
49
+ const a = transport(`${endpoint.url}/api/location`, { signal: first.signal });
50
+ const b = transport(`${endpoint.url}/api/session/s`, { method: "PATCH", body: '{"title":"fixture"}' });
51
+ const c = transport(`${endpoint.url}/api/location`, { signal: third.signal });
52
+ first.abort();
53
+ third.abort();
54
+ await assert.rejects(a, { name: "AbortError" });
55
+ await assert.rejects(c, { name: "AbortError" });
56
+ assert.equal(negotiationSignal?.aborted, false);
57
+ complete(Response.json(legacyContractFixture));
58
+ assert.equal((await b).status, 204);
59
+ assert.deepEqual(calls, ["/openapi.json", "/api/session/s/rename"]);
60
+ });
61
+ //# sourceMappingURL=negotiate.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiate.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/negotiate.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { createRuntimeFetch } from \"./transport\"\nimport { rememberRuntime } from \"./runtime\"\nimport { legacyContractFixture } from \"./contract-fixture\"\n\ntest(\"unknown contracts perform authenticated bounded read negotiation before any mutation\", async () => {\n for (const kind of [\"unauthorized\", \"unrecognized\", \"oversized\"] as const) {\n const endpoint = { url: \"http://127.0.0.1:4321\", auth: { type: \"basic\" as const, username: \"opencode\", password: \"fixture\" } }\n rememberRuntime(endpoint, { version: \"2.0.100\", pid: 1, discovery: \"status\" })\n let calls = 0, cancelled = false\n const transport = createRuntimeFetch(endpoint, async (input, init) => {\n calls++\n assert.equal(new URL(String(input)).pathname, \"/openapi.json\")\n assert.equal(init?.method ?? \"GET\", \"GET\")\n assert.equal(new Headers(init?.headers).get(\"authorization\"), `Basic ${Buffer.from(\"opencode:fixture\").toString(\"base64\")}`)\n assert.equal(init?.redirect, \"error\")\n if (kind === \"unauthorized\") return new Response(null, { status: 401 })\n if (kind === \"unrecognized\") return Response.json({ paths: {} })\n return new Response(new ReadableStream({\n start(controller) { controller.enqueue(new Uint8Array(8 * 1024 * 1024 + 1)) },\n cancel() { cancelled = true },\n }))\n })\n await assert.rejects(transport(`${endpoint.url}/api/session/s`, { method: \"PATCH\", body: '{\"title\":\"fixture\"}' }), /contract|HTTP 401/)\n assert.equal(calls, 1, \"negotiation failure must never dispatch a mutation\")\n if (kind === \"oversized\") assert.equal(cancelled, true)\n }\n})\n\ntest(\"shared negotiation has independent subscriber cancellation and never retries a mutation\", async () => {\n const endpoint = { url: \"http://127.0.0.1:4321\" }\n rememberRuntime(endpoint, { version: \"next-contract\", pid: 1, discovery: \"health\" })\n let complete!: (response: Response) => void\n let negotiationSignal: AbortSignal | null | undefined\n const calls: string[] = []\n const transport = createRuntimeFetch(endpoint, async (input, init) => {\n const pathname = new URL(String(input)).pathname\n calls.push(pathname)\n if (pathname === \"/openapi.json\") {\n negotiationSignal = init?.signal\n return new Promise<Response>(resolve => { complete = resolve })\n }\n assert.equal(pathname, \"/api/session/s/rename\")\n return new Response(null, { status: 204 })\n })\n const first = new AbortController(), third = new AbortController()\n const a = transport(`${endpoint.url}/api/location`, { signal: first.signal })\n const b = transport(`${endpoint.url}/api/session/s`, { method: \"PATCH\", body: '{\"title\":\"fixture\"}' })\n const c = transport(`${endpoint.url}/api/location`, { signal: third.signal })\n first.abort()\n third.abort()\n await assert.rejects(a, { name: \"AbortError\" })\n await assert.rejects(c, { name: \"AbortError\" })\n assert.equal(negotiationSignal?.aborted, false)\n complete(Response.json(legacyContractFixture))\n assert.equal((await b).status, 204)\n assert.deepEqual(calls, [\"/openapi.json\", \"/api/session/s/rename\"])\n})\n"]}
@@ -0,0 +1,137 @@
1
+ import path from "node:path";
2
+ import { readLocationRef } from "./location";
3
+ function object(value) {
4
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value) && !Buffer.isBuffer(value);
5
+ }
6
+ // Call on the canonical request before route translation. Public legacy query
7
+ // selectors use "workspace"; stored/body locations use "workspaceID".
8
+ export function readRequestLocations(url, body, defaultDirectory, profile) {
9
+ const result = { directories: [], locations: [], invalid: false };
10
+ const add = (directory, workspaceID) => {
11
+ try {
12
+ const ref = readLocationRef({ directory, workspaceID });
13
+ if (workspaceID !== undefined && profile !== "legacy")
14
+ throw new Error("Unsupported workspace selector");
15
+ result.directories.push(ref.directory);
16
+ result.locations.push(ref);
17
+ }
18
+ catch {
19
+ result.invalid = true;
20
+ }
21
+ };
22
+ for (const key of ["directory", "location[directory]", "workspace", "location[workspace]"]) {
23
+ if (url.searchParams.getAll(key).length > 1)
24
+ result.invalid = true;
25
+ }
26
+ if (url.searchParams.has("workspaceID") || url.searchParams.has("location[workspaceID]"))
27
+ result.invalid = true;
28
+ for (const [directoryKey, workspaceKey] of [["directory", "workspace"], ["location[directory]", "location[workspace]"]]) {
29
+ const directory = url.searchParams.get(directoryKey) ?? undefined;
30
+ const workspaceID = url.searchParams.get(workspaceKey) ?? undefined;
31
+ if (workspaceID !== undefined && workspaceKey === "workspace" && url.pathname.replace(/\/$/, "") !== "/api/session")
32
+ result.invalid = true;
33
+ if (directory !== undefined || workspaceID !== undefined)
34
+ add(directory ?? defaultDirectory, workspaceID);
35
+ }
36
+ if (object(body)) {
37
+ if ("workspace" in body)
38
+ result.invalid = true;
39
+ if ("directory" in body || "workspaceID" in body)
40
+ add(body.directory ?? defaultDirectory, body.workspaceID);
41
+ if (body.location !== undefined && body.location !== null) {
42
+ if (!object(body.location) || "workspace" in body.location)
43
+ result.invalid = true;
44
+ else
45
+ add(body.location.directory ?? defaultDirectory, body.location.workspaceID);
46
+ }
47
+ }
48
+ return result;
49
+ }
50
+ // Preserve the export/history identities; never rewrite every historical
51
+ // location to the import destination. Each is independently authorized by the
52
+ // caller and subsequently directory-translated at the existing proxy boundary.
53
+ export function prepareLocationImport(body, directory, profile) {
54
+ const result = { body, directories: [], locations: [], invalid: false };
55
+ if (!object(body))
56
+ return { ...result, invalid: true };
57
+ const addLocation = (owner, key) => {
58
+ const input = owner[key] ?? { directory };
59
+ if (!object(input) || "workspace" in input) {
60
+ result.invalid = true;
61
+ return;
62
+ }
63
+ try {
64
+ const location = readLocationRef({ ...input, directory: input.directory ?? directory });
65
+ if (location.workspaceID !== undefined && profile !== "legacy")
66
+ throw new Error("Unsupported workspace selector");
67
+ owner[key] = location;
68
+ result.directories.push(location.directory);
69
+ result.locations.push(location);
70
+ }
71
+ catch {
72
+ result.invalid = true;
73
+ }
74
+ };
75
+ // A copy keeps failed authorization from mutating the incoming request.
76
+ const input = { ...body };
77
+ result.body = input;
78
+ addLocation(input, "location");
79
+ if (object(input.info)) {
80
+ input.info = { ...input.info };
81
+ addLocation(input.info, "location");
82
+ }
83
+ if (Array.isArray(input.messages))
84
+ input.messages = input.messages.map((message) => {
85
+ if (!object(message) || message.type !== "location-switched")
86
+ return message;
87
+ const next = { ...message };
88
+ addLocation(next, "location");
89
+ if (object(next.previous)) {
90
+ next.previous = { ...next.previous };
91
+ addLocation(next.previous, "location");
92
+ }
93
+ return next;
94
+ });
95
+ return result;
96
+ }
97
+ export function decodeSessionListScope(cursor, profile) {
98
+ if (!cursor || cursor.length > 65_536 || !/^[A-Za-z0-9_-]+$/.test(cursor))
99
+ return null;
100
+ try {
101
+ const value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
102
+ if (!object(value) || value.workspaceID !== undefined)
103
+ return null;
104
+ const workspaceID = value.workspace;
105
+ if (workspaceID !== undefined && (profile !== "legacy" || typeof workspaceID !== "string" || !workspaceID.trim()))
106
+ return null;
107
+ const anchor = value.anchor;
108
+ if (!object(anchor) || typeof anchor.id !== "string" || !anchor.id
109
+ || typeof anchor.time !== "number" || !Number.isFinite(anchor.time)
110
+ || (anchor.direction !== "previous" && anchor.direction !== "next"))
111
+ return null;
112
+ if (value.search !== undefined && typeof value.search !== "string")
113
+ return null;
114
+ if (value.order !== undefined && value.order !== "asc" && value.order !== "desc")
115
+ return null;
116
+ if (value.parentID !== undefined && value.parentID !== null && typeof value.parentID !== "string")
117
+ return null;
118
+ const legacy = workspaceID === undefined ? {} : { workspaceID: workspaceID };
119
+ if (typeof value.directory === "string" && value.directory.trim() && value.project === undefined && value.subpath === undefined) {
120
+ return { directory: value.directory, ...legacy };
121
+ }
122
+ if (typeof value.project === "string" && value.project.trim() && value.directory === undefined) {
123
+ if (value.subpath === undefined)
124
+ return { project: value.project, ...legacy };
125
+ if (typeof value.subpath === "string" && !path.posix.isAbsolute(value.subpath)
126
+ && !path.win32.isAbsolute(value.subpath) && !value.subpath.split(/[\\/]/).includes("..")) {
127
+ return { project: value.project, subpath: value.subpath, ...legacy };
128
+ }
129
+ }
130
+ // Workspace-only/unscoped tokens cannot prove directory authority.
131
+ return null;
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
137
+ //# sourceMappingURL=proxy-locations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-locations.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/proxy-locations.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\"\nimport type { LocationRef } from \"@opencode/client\"\nimport type { ContractProfile } from \"./runtime\"\nimport { readLocationRef } from \"./location\"\n\nexport interface RequestLocations {\n directories: string[]\n locations: LocationRef[]\n invalid: boolean\n}\n\nfunction object(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value) && !Buffer.isBuffer(value)\n}\n\n// Call on the canonical request before route translation. Public legacy query\n// selectors use \"workspace\"; stored/body locations use \"workspaceID\".\nexport function readRequestLocations(url: URL, body: unknown, defaultDirectory: string, profile: ContractProfile): RequestLocations {\n const result: RequestLocations = { directories: [], locations: [], invalid: false }\n const add = (directory: unknown, workspaceID?: unknown) => {\n try {\n const ref = readLocationRef({ directory, workspaceID })\n if (workspaceID !== undefined && profile !== \"legacy\") throw new Error(\"Unsupported workspace selector\")\n result.directories.push(ref.directory)\n result.locations.push(ref)\n } catch { result.invalid = true }\n }\n for (const key of [\"directory\", \"location[directory]\", \"workspace\", \"location[workspace]\"]) {\n if (url.searchParams.getAll(key).length > 1) result.invalid = true\n }\n if (url.searchParams.has(\"workspaceID\") || url.searchParams.has(\"location[workspaceID]\")) result.invalid = true\n for (const [directoryKey, workspaceKey] of [[\"directory\", \"workspace\"], [\"location[directory]\", \"location[workspace]\"]]) {\n const directory = url.searchParams.get(directoryKey) ?? undefined\n const workspaceID = url.searchParams.get(workspaceKey) ?? undefined\n if (workspaceID !== undefined && workspaceKey === \"workspace\" && url.pathname.replace(/\\/$/, \"\") !== \"/api/session\") result.invalid = true\n if (directory !== undefined || workspaceID !== undefined) add(directory ?? defaultDirectory, workspaceID)\n }\n if (object(body)) {\n if (\"workspace\" in body) result.invalid = true\n if (\"directory\" in body || \"workspaceID\" in body) add(body.directory ?? defaultDirectory, body.workspaceID)\n if (body.location !== undefined && body.location !== null) {\n if (!object(body.location) || \"workspace\" in body.location) result.invalid = true\n else add(body.location.directory ?? defaultDirectory, body.location.workspaceID)\n }\n }\n return result\n}\n\n// Preserve the export/history identities; never rewrite every historical\n// location to the import destination. Each is independently authorized by the\n// caller and subsequently directory-translated at the existing proxy boundary.\nexport function prepareLocationImport(body: unknown, directory: string, profile: ContractProfile): RequestLocations & { body: unknown } {\n const result: RequestLocations & { body: unknown } = { body, directories: [], locations: [], invalid: false }\n if (!object(body)) return { ...result, invalid: true }\n const addLocation = (owner: Record<string, unknown>, key: string) => {\n const input = owner[key] ?? { directory }\n if (!object(input) || \"workspace\" in input) { result.invalid = true; return }\n try {\n const location = readLocationRef({ ...input, directory: input.directory ?? directory })\n if (location.workspaceID !== undefined && profile !== \"legacy\") throw new Error(\"Unsupported workspace selector\")\n owner[key] = location\n result.directories.push(location.directory)\n result.locations.push(location)\n } catch { result.invalid = true }\n }\n // A copy keeps failed authorization from mutating the incoming request.\n const input = { ...body }\n result.body = input\n addLocation(input, \"location\")\n if (object(input.info)) { input.info = { ...input.info }; addLocation(input.info as Record<string, unknown>, \"location\") }\n if (Array.isArray(input.messages)) input.messages = input.messages.map((message) => {\n if (!object(message) || message.type !== \"location-switched\") return message\n const next = { ...message }\n addLocation(next, \"location\")\n if (object(next.previous)) { next.previous = { ...next.previous }; addLocation(next.previous as Record<string, unknown>, \"location\") }\n return next\n })\n return result\n}\n\nexport interface SessionListScope {\n directory?: string\n project?: string\n subpath?: string\n workspaceID?: string\n}\n\nexport function decodeSessionListScope(cursor: string, profile: ContractProfile): SessionListScope | null {\n if (!cursor || cursor.length > 65_536 || !/^[A-Za-z0-9_-]+$/.test(cursor)) return null\n try {\n const value: unknown = JSON.parse(Buffer.from(cursor, \"base64url\").toString(\"utf8\"))\n if (!object(value) || value.workspaceID !== undefined) return null\n const workspaceID = value.workspace\n if (workspaceID !== undefined && (profile !== \"legacy\" || typeof workspaceID !== \"string\" || !workspaceID.trim())) return null\n const anchor = value.anchor\n if (!object(anchor) || typeof anchor.id !== \"string\" || !anchor.id\n || typeof anchor.time !== \"number\" || !Number.isFinite(anchor.time)\n || (anchor.direction !== \"previous\" && anchor.direction !== \"next\")) return null\n if (value.search !== undefined && typeof value.search !== \"string\") return null\n if (value.order !== undefined && value.order !== \"asc\" && value.order !== \"desc\") return null\n if (value.parentID !== undefined && value.parentID !== null && typeof value.parentID !== \"string\") return null\n const legacy = workspaceID === undefined ? {} : { workspaceID: workspaceID as string }\n if (typeof value.directory === \"string\" && value.directory.trim() && value.project === undefined && value.subpath === undefined) {\n return { directory: value.directory, ...legacy }\n }\n if (typeof value.project === \"string\" && value.project.trim() && value.directory === undefined) {\n if (value.subpath === undefined) return { project: value.project, ...legacy }\n if (typeof value.subpath === \"string\" && !path.posix.isAbsolute(value.subpath)\n && !path.win32.isAbsolute(value.subpath) && !value.subpath.split(/[\\\\/]/).includes(\"..\")) {\n return { project: value.project, subpath: value.subpath, ...legacy }\n }\n }\n // Workspace-only/unscoped tokens cannot prove directory authority.\n return null\n } catch { return null }\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { decodeSessionListScope, prepareLocationImport, readRequestLocations } from "./proxy-locations";
4
+ test("request selectors preserve legacy authority and retain modern rejection", () => {
5
+ const url = new URL("http://localhost/api/form?location[directory]=/repo&location[workspace]=one");
6
+ assert.deepEqual(readRequestLocations(url, undefined, "/root", "legacy"), {
7
+ directories: ["/repo"], locations: [{ directory: "/repo", workspaceID: "one" }], invalid: false,
8
+ });
9
+ assert.equal(readRequestLocations(url, undefined, "/root", "modern").invalid, true);
10
+ url.searchParams.append("location[workspace]", "two");
11
+ assert.equal(readRequestLocations(url, undefined, "/root", "legacy").invalid, true);
12
+ for (const body of [{ location: { directory: "/repo", workspaceID: "one" } }, { directory: "/repo", workspaceID: "one" }]) {
13
+ const result = readRequestLocations(new URL("http://localhost/api/session"), body, "/root", "legacy");
14
+ assert.deepEqual(result.locations, [{ directory: "/repo", workspaceID: "one" }]);
15
+ assert.equal(readRequestLocations(new URL("http://localhost/api/session"), body, "/root", "modern").invalid, true);
16
+ }
17
+ });
18
+ test("imports authorize each full historical identity without rewriting it", () => {
19
+ const body = {
20
+ location: { directory: "/root", workspaceID: "root" },
21
+ info: { location: { directory: "/repo", workspaceID: "one" } },
22
+ messages: [{ type: "location-switched", location: { directory: "/repo", workspaceID: "two" }, previous: { location: { directory: "/repo", workspaceID: "one" } } }],
23
+ };
24
+ const result = prepareLocationImport(body, "/default", "legacy");
25
+ assert.equal(result.invalid, false);
26
+ assert.deepEqual(result.locations.map(location => location.workspaceID), ["root", "one", "two", "one"]);
27
+ assert.deepEqual(result.body, body);
28
+ assert.notEqual(result.body, body);
29
+ assert.equal(prepareLocationImport(body, "/default", "modern").invalid, true);
30
+ });
31
+ test("cursor parsing preserves native scope while rejecting foreign-schema authority", () => {
32
+ const value = { directory: "/repo", workspace: "one", anchor: { id: "s", time: 1, direction: "next" } };
33
+ const cursor = Buffer.from(JSON.stringify(value)).toString("base64url");
34
+ assert.deepEqual(decodeSessionListScope(cursor, "legacy"), { directory: "/repo", workspaceID: "one" });
35
+ assert.equal(decodeSessionListScope(cursor, "modern"), null);
36
+ const withFields = (fields) => Buffer.from(JSON.stringify({ ...value, ...fields })).toString("base64url");
37
+ assert.equal(decodeSessionListScope(withFields({ project: "other" }), "legacy"), null);
38
+ assert.equal(decodeSessionListScope(withFields({ workspaceID: "other" }), "legacy"), null);
39
+ assert.equal(decodeSessionListScope(withFields({ directory: undefined }), "legacy"), null);
40
+ assert.deepEqual(JSON.parse(Buffer.from(cursor, "base64url").toString()), value, "the native token is not rewritten");
41
+ });
42
+ //# sourceMappingURL=proxy-locations.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy-locations.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/proxy-locations.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { decodeSessionListScope, prepareLocationImport, readRequestLocations } from \"./proxy-locations\"\n\ntest(\"request selectors preserve legacy authority and retain modern rejection\", () => {\n const url = new URL(\"http://localhost/api/form?location[directory]=/repo&location[workspace]=one\")\n assert.deepEqual(readRequestLocations(url, undefined, \"/root\", \"legacy\"), {\n directories: [\"/repo\"], locations: [{ directory: \"/repo\", workspaceID: \"one\" }], invalid: false,\n })\n assert.equal(readRequestLocations(url, undefined, \"/root\", \"modern\").invalid, true)\n url.searchParams.append(\"location[workspace]\", \"two\")\n assert.equal(readRequestLocations(url, undefined, \"/root\", \"legacy\").invalid, true)\n for (const body of [{ location: { directory: \"/repo\", workspaceID: \"one\" } }, { directory: \"/repo\", workspaceID: \"one\" }]) {\n const result = readRequestLocations(new URL(\"http://localhost/api/session\"), body, \"/root\", \"legacy\")\n assert.deepEqual(result.locations, [{ directory: \"/repo\", workspaceID: \"one\" }])\n assert.equal(readRequestLocations(new URL(\"http://localhost/api/session\"), body, \"/root\", \"modern\").invalid, true)\n }\n})\n\ntest(\"imports authorize each full historical identity without rewriting it\", () => {\n const body = {\n location: { directory: \"/root\", workspaceID: \"root\" },\n info: { location: { directory: \"/repo\", workspaceID: \"one\" } },\n messages: [{ type: \"location-switched\", location: { directory: \"/repo\", workspaceID: \"two\" }, previous: { location: { directory: \"/repo\", workspaceID: \"one\" } } }],\n }\n const result = prepareLocationImport(body, \"/default\", \"legacy\")\n assert.equal(result.invalid, false)\n assert.deepEqual(result.locations.map(location => location.workspaceID), [\"root\", \"one\", \"two\", \"one\"])\n assert.deepEqual(result.body, body)\n assert.notEqual(result.body, body)\n assert.equal(prepareLocationImport(body, \"/default\", \"modern\").invalid, true)\n})\n\ntest(\"cursor parsing preserves native scope while rejecting foreign-schema authority\", () => {\n const value = { directory: \"/repo\", workspace: \"one\", anchor: { id: \"s\", time: 1, direction: \"next\" } }\n const cursor = Buffer.from(JSON.stringify(value)).toString(\"base64url\")\n assert.deepEqual(decodeSessionListScope(cursor, \"legacy\"), { directory: \"/repo\", workspaceID: \"one\" })\n assert.equal(decodeSessionListScope(cursor, \"modern\"), null)\n const withFields = (fields: object) => Buffer.from(JSON.stringify({ ...value, ...fields })).toString(\"base64url\")\n assert.equal(decodeSessionListScope(withFields({ project: \"other\" }), \"legacy\"), null)\n assert.equal(decodeSessionListScope(withFields({ workspaceID: \"other\" }), \"legacy\"), null)\n assert.equal(decodeSessionListScope(withFields({ directory: undefined }), \"legacy\"), null)\n assert.deepEqual(JSON.parse(Buffer.from(cursor, \"base64url\").toString()), value, \"the native token is not rewritten\")\n})\n"]}
@@ -0,0 +1,60 @@
1
+ import { Readable } from "node:stream";
2
+ export async function forwardRuntimeRequest(args) {
3
+ const controller = new AbortController();
4
+ let released = false;
5
+ let unauthorized = false;
6
+ const release = () => {
7
+ if (released)
8
+ return;
9
+ released = true;
10
+ args.release?.();
11
+ args.reply.raw.off("close", disconnect);
12
+ // Retiring the connection aborts its fetch bodies. Preserve the received
13
+ // native 401 envelope until its body has been consumed by the downstream.
14
+ if (unauthorized)
15
+ args.invalidate();
16
+ };
17
+ const disconnect = () => { controller.abort(); release(); };
18
+ args.reply.raw.once("close", disconnect);
19
+ if (args.reply.raw.destroyed || args.request.raw.aborted)
20
+ controller.abort();
21
+ try {
22
+ const headers = new Headers();
23
+ for (const [key, value] of Object.entries(args.headers)) {
24
+ if (typeof value === "string")
25
+ headers.set(key, value);
26
+ else if (Array.isArray(value))
27
+ for (const item of value)
28
+ headers.append(key, String(item));
29
+ }
30
+ headers.delete("content-length");
31
+ const body = args.body === undefined || args.body === null ? undefined
32
+ : typeof args.body === "string" ? args.body : Buffer.isBuffer(args.body) ? Uint8Array.from(args.body).buffer : JSON.stringify(args.body);
33
+ if (body === undefined)
34
+ headers.delete("content-type");
35
+ const response = await args.fetch(args.url, { method: args.request.method, headers, body, signal: controller.signal });
36
+ unauthorized = response.status === 401;
37
+ args.reply.code(response.status);
38
+ // Fetch has decompressed the body. Do not relay its original framing,
39
+ // cookies, redirect destinations or content-encoding/content-length.
40
+ for (const name of ["content-type", "cache-control", "etag", "last-modified"]) {
41
+ const value = response.headers.get(name);
42
+ if (value !== null)
43
+ args.reply.header(name, value);
44
+ }
45
+ if (!response.body) {
46
+ release();
47
+ return args.reply.send();
48
+ }
49
+ const stream = Readable.fromWeb(response.body);
50
+ stream.once("end", release).once("close", release).once("error", release);
51
+ return args.reply.send(stream);
52
+ }
53
+ catch (error) {
54
+ release();
55
+ if (!controller.signal.aborted)
56
+ args.invalidate();
57
+ throw error;
58
+ }
59
+ }
60
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/proxy.ts"],"names":[],"mappings":"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","sourcesContent":["import { Readable } from \"node:stream\"\nimport type { FastifyReply, FastifyRequest } from \"fastify\"\n\nexport async function forwardRuntimeRequest(args: {\n request: FastifyRequest\n reply: FastifyReply\n url: string\n body: unknown\n headers: Record<string, unknown>\n fetch: typeof fetch\n release?: () => void\n invalidate: () => void\n}) {\n const controller = new AbortController()\n let released = false\n let unauthorized = false\n const release = () => {\n if (released) return\n released = true\n args.release?.()\n args.reply.raw.off(\"close\", disconnect)\n // Retiring the connection aborts its fetch bodies. Preserve the received\n // native 401 envelope until its body has been consumed by the downstream.\n if (unauthorized) args.invalidate()\n }\n const disconnect = () => { controller.abort(); release() }\n args.reply.raw.once(\"close\", disconnect)\n if (args.reply.raw.destroyed || args.request.raw.aborted) controller.abort()\n try {\n const headers = new Headers()\n for (const [key, value] of Object.entries(args.headers)) {\n if (typeof value === \"string\") headers.set(key, value)\n else if (Array.isArray(value)) for (const item of value) headers.append(key, String(item))\n }\n headers.delete(\"content-length\")\n const body = args.body === undefined || args.body === null ? undefined\n : typeof args.body === \"string\" ? args.body : Buffer.isBuffer(args.body) ? Uint8Array.from(args.body).buffer : JSON.stringify(args.body)\n if (body === undefined) headers.delete(\"content-type\")\n const response = await args.fetch(args.url, { method: args.request.method, headers, body, signal: controller.signal })\n unauthorized = response.status === 401\n args.reply.code(response.status)\n // Fetch has decompressed the body. Do not relay its original framing,\n // cookies, redirect destinations or content-encoding/content-length.\n for (const name of [\"content-type\", \"cache-control\", \"etag\", \"last-modified\"]) {\n const value = response.headers.get(name)\n if (value !== null) args.reply.header(name, value)\n }\n if (!response.body) { release(); return args.reply.send() }\n const stream = Readable.fromWeb(response.body as import(\"node:stream/web\").ReadableStream)\n stream.once(\"end\", release).once(\"close\", release).once(\"error\", release)\n return args.reply.send(stream)\n } catch (error) {\n release()\n if (!controller.signal.aborted) args.invalidate()\n throw error\n }\n}\n"]}