@zuplo/editor 1.0.30627199824 → 1.0.30628139828

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 (77) hide show
  1. package/dist/adapters/memory-fs.d.ts +44 -0
  2. package/dist/adapters/memory-fs.d.ts.map +1 -0
  3. package/dist/adapters/memory-fs.js +204 -0
  4. package/dist/adapters/memory-fs.js.map +1 -0
  5. package/dist/adapters/memory-fs.spec.d.ts +2 -0
  6. package/dist/adapters/memory-fs.spec.d.ts.map +1 -0
  7. package/dist/adapters/memory-fs.spec.js +74 -0
  8. package/dist/adapters/memory-fs.spec.js.map +1 -0
  9. package/dist/adapters/memory-preferences.d.ts +10 -0
  10. package/dist/adapters/memory-preferences.d.ts.map +1 -0
  11. package/dist/adapters/memory-preferences.js +18 -0
  12. package/dist/adapters/memory-preferences.js.map +1 -0
  13. package/dist/adapters/memory.d.ts +10 -0
  14. package/dist/adapters/memory.d.ts.map +1 -0
  15. package/dist/adapters/memory.js +10 -0
  16. package/dist/adapters/memory.js.map +1 -0
  17. package/dist/adapters/node-fs.d.ts +11 -0
  18. package/dist/adapters/node-fs.d.ts.map +1 -0
  19. package/dist/adapters/node-fs.js +133 -0
  20. package/dist/adapters/node-fs.js.map +1 -0
  21. package/dist/adapters/node-preferences.d.ts +10 -0
  22. package/dist/adapters/node-preferences.d.ts.map +1 -0
  23. package/dist/adapters/node-preferences.js +38 -0
  24. package/dist/adapters/node-preferences.js.map +1 -0
  25. package/dist/app.d.ts +67 -0
  26. package/dist/app.d.ts.map +1 -0
  27. package/dist/app.js +116 -0
  28. package/dist/app.js.map +1 -0
  29. package/dist/client/favicon-local.ico +0 -0
  30. package/dist/config.d.ts +16 -2
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +5 -5
  33. package/dist/config.js.map +1 -1
  34. package/dist/fixtures/preview-project.d.ts +6 -0
  35. package/dist/fixtures/preview-project.d.ts.map +1 -0
  36. package/dist/fixtures/preview-project.js +21 -0
  37. package/dist/fixtures/preview-project.js.map +1 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/ports.d.ts +123 -0
  43. package/dist/ports.d.ts.map +1 -0
  44. package/dist/ports.js +71 -0
  45. package/dist/ports.js.map +1 -0
  46. package/dist/routes/api.d.ts +5 -0
  47. package/dist/routes/api.d.ts.map +1 -1
  48. package/dist/routes/api.js +10 -7
  49. package/dist/routes/api.js.map +1 -1
  50. package/dist/routes/api.spec.js +4 -0
  51. package/dist/routes/api.spec.js.map +1 -1
  52. package/dist/routes/editor.d.ts +10 -0
  53. package/dist/routes/editor.d.ts.map +1 -1
  54. package/dist/routes/editor.js +17 -36
  55. package/dist/routes/editor.js.map +1 -1
  56. package/dist/routes/editor.spec.js +7 -1
  57. package/dist/routes/editor.spec.js.map +1 -1
  58. package/dist/routes/fs-race-condition.spec.js +2 -1
  59. package/dist/routes/fs-race-condition.spec.js.map +1 -1
  60. package/dist/routes/fs.d.ts +2 -1
  61. package/dist/routes/fs.d.ts.map +1 -1
  62. package/dist/routes/fs.js +144 -191
  63. package/dist/routes/fs.js.map +1 -1
  64. package/dist/routes/fs.spec.js +2 -1
  65. package/dist/routes/fs.spec.js.map +1 -1
  66. package/dist/server.d.ts +7 -3
  67. package/dist/server.d.ts.map +1 -1
  68. package/dist/server.js +86 -116
  69. package/dist/server.js.map +1 -1
  70. package/dist/utils/path.d.ts +0 -13
  71. package/dist/utils/path.d.ts.map +1 -1
  72. package/dist/utils/path.js +0 -18
  73. package/dist/utils/path.js.map +1 -1
  74. package/dist/validation.d.ts.map +1 -1
  75. package/dist/validation.js +8 -5
  76. package/dist/validation.js.map +1 -1
  77. package/package.json +18 -1
package/dist/app.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { type Context, Hono } from "hono";
2
+ import { type LocalEditorLabels } from "./config.js";
3
+ import type { CommonLogger } from "./interfaces.js";
4
+ import type { ChangeNotifier, EditorPreferencesPort, FileSystemPort } from "./ports.js";
5
+ /**
6
+ * The built SPA, however the platform happens to hold it.
7
+ *
8
+ * `serve` is handed the live Hono context rather than a path, because both
9
+ * implementations are thin wrappers over something that already expects one —
10
+ * `serveStatic` on Node, Cloudflare's assets binding in the Worker — and
11
+ * synthesising a context for them is what breaks.
12
+ */
13
+ export interface AssetSource {
14
+ /**
15
+ * The `index.html` template, or `undefined` when there's no client build.
16
+ *
17
+ * Returned as a string because the config injection rewrites it per request.
18
+ */
19
+ readDocument(): Promise<string | undefined>;
20
+ /** The built asset the request is for, or `undefined` if there isn't one. */
21
+ serve(context: Context): Promise<Response | undefined>;
22
+ }
23
+ export interface EditorAppOptions {
24
+ /** The project the editor reads and writes. */
25
+ fs: FileSystemPort;
26
+ /** Where the user's external-editor choice is kept. */
27
+ preferences: EditorPreferencesPort;
28
+ /**
29
+ * The built SPA. Omit to run without one — Vite serves the client in
30
+ * development, and this app only answers `/api` in that case.
31
+ */
32
+ assets?: AssetSource;
33
+ /**
34
+ * Reports files changing underneath the editor, so the SPA can reload them.
35
+ * Omit to leave live-reload dormant.
36
+ */
37
+ changes?: ChangeNotifier;
38
+ logger: CommonLogger;
39
+ /** The gateway the test console relays requests to. */
40
+ deploymentUrl: string;
41
+ /**
42
+ * The project's location on the machine running the editor. Used only to build
43
+ * external-editor deep links; all file access goes through `fs`.
44
+ */
45
+ sourceDirectory: string;
46
+ /** Overrides for the account/environment names the portal displays. */
47
+ labels?: LocalEditorLabels;
48
+ }
49
+ export interface EditorApp {
50
+ /**
51
+ * The Hono app. Hand its `fetch` to whatever is listening — `@hono/node-server`
52
+ * on Node, the Worker's default export on Cloudflare.
53
+ */
54
+ app: Hono;
55
+ /** Releases SSE clients and stops listening for changes. */
56
+ dispose: () => Promise<void>;
57
+ }
58
+ /**
59
+ * Builds the editor's HTTP app from injected platform capabilities.
60
+ *
61
+ * This is the whole server minus the platform: no listener, no filesystem, no
62
+ * file watcher, no logger implementation. `LocalEditorServer` wraps it for Node
63
+ * and the preview Worker wraps it for Cloudflare, so both run these exact
64
+ * routes rather than approximations of each other.
65
+ */
66
+ export declare function createEditorApp(options: EditorAppOptions): EditorApp;
67
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,EAAwB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAG3E,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,cAAc,EACf,MAAM,YAAY,CAAC;AAGpB;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAE5C,6EAA6E;IAC7E,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,EAAE,EAAE,cAAc,CAAC;IAEnB,uDAAuD;IACvD,WAAW,EAAE,qBAAqB,CAAC;IAEnC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IAEzB,MAAM,EAAE,YAAY,CAAC;IAErB,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IAExB,uEAAuE;IACvE,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,GAAG,EAAE,IAAI,CAAC;IAEV,4DAA4D;IAC5D,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAoIpE"}
package/dist/app.js ADDED
@@ -0,0 +1,116 @@
1
+ import { ApiError, HttpStatusCode } from "@zuplo/errors";
2
+ import { Hono } from "hono";
3
+ import { bodyLimit } from "hono/body-limit";
4
+ import { cors } from "hono/cors";
5
+ import { getLocalEditorConfig } from "./config.js";
6
+ import { editorConfig } from "./constants.js";
7
+ import { createErrorHandler, createNotFoundHandler } from "./error-handler.js";
8
+ import { createApiRoutes } from "./routes/api.js";
9
+ const staticFileExtensions = [".css", ".js", ".json", ".svg", ".png", ".ico"];
10
+ /**
11
+ * Builds the editor's HTTP app from injected platform capabilities.
12
+ *
13
+ * This is the whole server minus the platform: no listener, no filesystem, no
14
+ * file watcher, no logger implementation. `LocalEditorServer` wraps it for Node
15
+ * and the preview Worker wraps it for Cloudflare, so both run these exact
16
+ * routes rather than approximations of each other.
17
+ */
18
+ export function createEditorApp(options) {
19
+ const { fs, preferences, assets, changes, logger, deploymentUrl, sourceDirectory, labels, } = options;
20
+ const app = new Hono();
21
+ const sseClients = new Set();
22
+ app.onError(createErrorHandler(logger));
23
+ app.notFound(createNotFoundHandler());
24
+ // Middleware only wraps routes registered after it, so this has to come
25
+ // before the routes below.
26
+ app.use(bodyLimit({
27
+ maxSize: editorConfig.server.bodyLimit,
28
+ onError: () => {
29
+ throw new ApiError({
30
+ type: "https://zup.fail/http-status/413",
31
+ title: "Payload Too Large",
32
+ status: HttpStatusCode.PAYLOAD_TOO_LARGE,
33
+ detail: `Request body exceeds the maximum size of ${editorConfig.server.bodyLimit} bytes`,
34
+ });
35
+ },
36
+ }));
37
+ app.use(cors({
38
+ origin: (origin) => origin ?? "*",
39
+ allowMethods: [
40
+ "GET",
41
+ "POST",
42
+ "PATCH",
43
+ "PUT",
44
+ "DELETE",
45
+ "OPTIONS",
46
+ "HEAD",
47
+ ],
48
+ allowHeaders: ["*"],
49
+ exposeHeaders: ["*"],
50
+ }));
51
+ app.route("/api", createApiRoutes({
52
+ fs,
53
+ editorPreferences: preferences,
54
+ sourceDirectory,
55
+ logger,
56
+ sseClients,
57
+ deploymentUrl,
58
+ }));
59
+ if (assets) {
60
+ app.on(["GET", "HEAD"], "*", async (c) => {
61
+ const isDocumentRequest = c.req.header("accept")?.includes("text/html") &&
62
+ !staticFileExtensions.some((ext) => c.req.path.endsWith(ext));
63
+ if (!isDocumentRequest) {
64
+ return ((await assets.serve(c)) ??
65
+ // The asset source hands misses back rather than 404ing itself, so
66
+ // they get the same problem+json every other miss does.
67
+ c.notFound());
68
+ }
69
+ // Every navigation gets index.html — the SPA owns routing.
70
+ const template = await assets.readDocument();
71
+ if (!template) {
72
+ // No client build yet, which is normal in development: Vite is in front
73
+ // and this handler is never reached there.
74
+ return c.notFound();
75
+ }
76
+ const localEditorConfig = await getLocalEditorConfig({
77
+ deploymentUrl,
78
+ fs,
79
+ logger,
80
+ labels,
81
+ });
82
+ // Inject localEditorConfig into the HTML
83
+ const html = template.replace(`"{{localEditorConfig}}"`, // Note the quotes are correct
84
+ // A replacer function, so `$&` and friends in the JSON aren't treated
85
+ // as substitution patterns.
86
+ () => JSON.stringify(localEditorConfig));
87
+ return c.html(html);
88
+ });
89
+ }
90
+ const stopWatching = changes?.onChange((filePath) => {
91
+ // Broadcast to all SSE clients
92
+ const data = JSON.stringify({ type: "change", filePath });
93
+ for (const client of sseClients) {
94
+ client.send("change", data).catch((err) => {
95
+ logger.warn({ err }, "Error sending SSE change event");
96
+ });
97
+ }
98
+ logger.info(`File changed: ${filePath}`);
99
+ });
100
+ const dispose = async () => {
101
+ stopWatching?.();
102
+ for (const client of sseClients) {
103
+ try {
104
+ if (!client.closed) {
105
+ await client.close();
106
+ }
107
+ }
108
+ catch (err) {
109
+ logger.warn({ err }, "Error closing SSE client");
110
+ }
111
+ }
112
+ sseClients.clear();
113
+ };
114
+ return { app, dispose };
115
+ }
116
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
Binary file
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { CommonLogger } from "./interfaces.js";
2
2
  import { type ApeContext } from "./interfaces.portal.js";
3
+ import type { FileSystemPort } from "./ports.js";
3
4
  /**
4
5
  * Local-specific configuration that is provided to the React app
5
6
  *
@@ -10,9 +11,22 @@ export interface LocalEditorConfig {
10
11
  defaultPath: string;
11
12
  apeContext: ApeContext;
12
13
  }
13
- export declare function getLocalEditorConfig({ deploymentUrl, sourceDirectory, logger }: {
14
+ /**
15
+ * Human-readable names shown in the portal's account and environment chrome.
16
+ *
17
+ * The preview Worker overrides these so a hosted preview identifies itself as
18
+ * one — with its branch and commit — instead of claiming to be somebody's local
19
+ * machine. That reuses UI the portal already renders, so nothing preview-only
20
+ * has to exist in the frontend.
21
+ */
22
+ export interface LocalEditorLabels {
23
+ account?: string;
24
+ environment?: string;
25
+ }
26
+ export declare function getLocalEditorConfig({ deploymentUrl, fs, logger, labels }: {
14
27
  deploymentUrl: string;
15
- sourceDirectory: string;
28
+ fs: FileSystemPort;
16
29
  logger: CommonLogger;
30
+ labels?: LocalEditorLabels;
17
31
  }): Promise<LocalEditorConfig>;
18
32
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,wBAAwB,CAAC;AAEhC;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAIhC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;CACxB;AAaD,wBAAsB,oBAAoB,CAAC,EACzC,aAAa,EACb,eAAe,EACf,MAAM,EACP,EAAE;IACD,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;CACtB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA6G7B"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAEL,KAAK,UAAU,EAIhB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAIhC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;CACxB;AAaD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wBAAsB,oBAAoB,CAAC,EACzC,aAAa,EACb,EAAE,EACF,MAAM,EACN,MAAM,EACP,EAAE;IACD,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,cAAc,CAAC;IACnB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4G7B"}
package/dist/config.js CHANGED
@@ -1,4 +1,3 @@
1
- import { readFile } from "node:fs/promises";
2
1
  import { EnvironmentType, } from "./interfaces.portal.js";
3
2
  function npmNameToUrlSafe(name) {
4
3
  // Scoped packages: @scope/pkg -> scope-pkg
@@ -8,10 +7,11 @@ function npmNameToUrlSafe(name) {
8
7
  // Other names: just URI encode to be safe
9
8
  return encodeURIComponent(name.toLowerCase());
10
9
  }
11
- export async function getLocalEditorConfig({ deploymentUrl, sourceDirectory, logger, }) {
10
+ export async function getLocalEditorConfig({ deploymentUrl, fs, logger, labels, }) {
12
11
  let pkgJson;
13
12
  try {
14
- pkgJson = (await readFile(`${sourceDirectory}/package.json`, "utf-8").then(JSON.parse));
13
+ const { content } = await fs.readFileWithStat("package.json");
14
+ pkgJson = JSON.parse(content);
15
15
  }
16
16
  catch (err) {
17
17
  logger.warn({ err }, "Failed to load package.json");
@@ -20,7 +20,7 @@ export async function getLocalEditorConfig({ deploymentUrl, sourceDirectory, log
20
20
  const account = {
21
21
  id: "dev",
22
22
  name: "dev",
23
- label: "Local Development",
23
+ label: labels?.account ?? "Local Development",
24
24
  createdOn: new Date().toISOString(),
25
25
  isSharingEnabled: false,
26
26
  isMfaEnforced: false,
@@ -70,7 +70,7 @@ export async function getLocalEditorConfig({ deploymentUrl, sourceDirectory, log
70
70
  const environmentName = `local-${projectName}`;
71
71
  const environment = {
72
72
  name: environmentName,
73
- label: "local",
73
+ label: labels?.environment ?? "local",
74
74
  urlParameter: environmentName,
75
75
  createdOn: Date.now(),
76
76
  accountName: account.name,
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The sample Zuplo project the editor preview deployment starts from, as
3
+ * project-relative path → file content.
4
+ */
5
+ export declare const previewProject: Record<string, string>;
6
+ //# sourceMappingURL=preview-project.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-project.d.ts","sourceRoot":"","sources":["../../src/fixtures/preview-project.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuBjD,CAAC"}
@@ -0,0 +1,21 @@
1
+ // Generated by scripts/build-fixture.ts — do not edit.
2
+ // Regenerate with `pnpm --filter @zuplo/editor build:fixture` after changing
3
+ // anything under fixtures/preview-project.
4
+ /**
5
+ * The sample Zuplo project the editor preview deployment starts from, as
6
+ * project-relative path → file content.
7
+ */
8
+ export const previewProject = {
9
+ "config/policies.json": '{\n "policies": [\n {\n "name": "api-key-auth",\n "policyType": "api-key-inbound",\n "handler": {\n "export": "ApiKeyInboundPolicy",\n "module": "$import(@zuplo/runtime)",\n "options": {\n "allowUnauthenticatedRequests": false\n }\n }\n },\n {\n "name": "rate-limit-products",\n "policyType": "rate-limit-inbound",\n "handler": {\n "export": "RateLimitInboundPolicy",\n "module": "$import(@zuplo/runtime)",\n "options": {\n "rateLimitBy": "ip",\n "requestsAllowed": 100,\n "timeWindowMinutes": 1\n }\n }\n },\n {\n "name": "validate-product",\n "policyType": "request-validation-inbound",\n "handler": {\n "export": "RequestValidationInboundPolicy",\n "module": "$import(@zuplo/runtime)",\n "options": {\n "validateBody": true,\n "includeRequestInLogs": true\n }\n }\n }\n ]\n}\n',
10
+ "config/routes.oas.json": '{\n "openapi": "3.1.0",\n "info": {\n "title": "Preview Gateway",\n "version": "1.0.0",\n "description": "A sample API used by the editor preview deployment."\n },\n "paths": {\n "/products": {\n "get": {\n "summary": "List products",\n "operationId": "list-products",\n "x-zuplo-route": {\n "corsPolicy": "anything-goes",\n "handler": {\n "export": "urlForwardHandler",\n "module": "$import(@zuplo/runtime)",\n "options": {\n "baseUrl": "https://echo.zuplo.io/products"\n }\n },\n "policies": {\n "inbound": ["rate-limit-products", "api-key-auth"]\n }\n },\n "responses": {\n "200": {\n "description": "A page of products",\n "content": {\n "application/json": {\n "schema": { "$ref": "../schemas/product-list.json" }\n }\n }\n }\n }\n },\n "post": {\n "summary": "Create a product",\n "operationId": "create-product",\n "x-zuplo-route": {\n "corsPolicy": "anything-goes",\n "handler": {\n "export": "default",\n "module": "$import(./modules/create-product)"\n },\n "policies": {\n "inbound": ["validate-product", "api-key-auth"]\n }\n },\n "requestBody": {\n "required": true,\n "content": {\n "application/json": {\n "schema": { "$ref": "../schemas/product.json" }\n }\n }\n },\n "responses": {\n "201": { "description": "The created product" }\n }\n }\n },\n "/products/:productId": {\n "get": {\n "summary": "Get a product",\n "operationId": "get-product",\n "x-zuplo-route": {\n "corsPolicy": "anything-goes",\n "handler": {\n "export": "getProduct",\n "module": "$import(./modules/get-product)"\n },\n "policies": {\n "inbound": ["api-key-auth"]\n }\n },\n "responses": {\n "200": { "description": "The product" },\n "404": { "description": "No such product" }\n }\n }\n },\n "/health": {\n "get": {\n "summary": "Health check",\n "operationId": "health",\n "x-zuplo-route": {\n "corsPolicy": "anything-goes",\n "handler": {\n "export": "health",\n "module": "$import(./modules/health)"\n }\n },\n "responses": {\n "200": { "description": "The gateway is up" }\n }\n }\n }\n }\n}\n',
11
+ "docs/introduction.md": '---\ntitle: Introduction\n---\n\n# Preview Gateway\n\nThis is a sample Zuplo project. It exists so the editor\'s per-PR preview\ndeployment has a realistic project to render — routes with policies attached,\nJSON schemas, handler modules, tests, and these docs.\n\n## Authentication\n\nEvery route except `/health` requires an API key:\n\n```bash\ncurl https://example.com/products \\\n -H "Authorization: Bearer $API_KEY"\n```\n\n## Rate limits\n\n`GET /products` is limited to 100 requests per minute per IP.\n',
12
+ "modules/create-product.ts": 'import { ZuploContext, ZuploRequest } from "@zuplo/runtime";\n\ninterface NewProduct {\n name: string;\n price: number;\n}\n\nexport default async function (request: ZuploRequest, context: ZuploContext) {\n const body = (await request.json()) as NewProduct;\n\n context.log.info(`Creating product ${body.name}`);\n\n return new Response(\n JSON.stringify({ id: crypto.randomUUID(), ...body }),\n {\n status: 201,\n headers: { "content-type": "application/json" },\n },\n );\n}\n',
13
+ "modules/get-product.ts": 'import { ZuploContext, ZuploRequest } from "@zuplo/runtime";\n\nconst products: Record<string, { id: string; name: string; price: number }> = {\n "1": { id: "1", name: "Espresso Machine", price: 449 },\n "2": { id: "2", name: "Burr Grinder", price: 179 },\n};\n\nexport async function getProduct(request: ZuploRequest, context: ZuploContext) {\n const { productId } = request.params;\n const product = products[productId];\n\n if (!product) {\n context.log.warn(`No product with id ${productId}`);\n return new Response("Not found", { status: 404 });\n }\n\n return product;\n}\n',
14
+ "modules/health.ts": 'import { ZuploContext, ZuploRequest } from "@zuplo/runtime";\n\nexport function health(request: ZuploRequest, context: ZuploContext) {\n return { status: "ok" };\n}\n',
15
+ "package.json": '{\n "name": "preview-gateway",\n "version": "1.0.0",\n "private": true,\n "description": "Sample Zuplo project served by the editor preview deployment"\n}\n',
16
+ "schemas/product-list.json": '{\n "$schema": "http://json-schema.org/draft-07/schema#",\n "title": "ProductList",\n "type": "object",\n "required": ["data"],\n "properties": {\n "data": {\n "type": "array",\n "items": { "$ref": "./product.json" }\n },\n "nextCursor": {\n "type": ["string", "null"]\n }\n }\n}\n',
17
+ "schemas/product.json": '{\n "$schema": "http://json-schema.org/draft-07/schema#",\n "title": "Product",\n "type": "object",\n "required": ["name", "price"],\n "additionalProperties": false,\n "properties": {\n "id": {\n "type": "string",\n "description": "Server-assigned identifier."\n },\n "name": {\n "type": "string",\n "minLength": 1,\n "maxLength": 200\n },\n "price": {\n "type": "number",\n "minimum": 0\n }\n }\n}\n',
18
+ "tests/products.test.ts": 'import { describe, expect, it } from "@zuplo/test";\n\ndescribe("products", () => {\n it("lists products", async () => {\n const response = await fetch(`${process.env.ZUPLO_URL}/products`);\n expect(response.status).toBe(200);\n });\n\n it("rejects a product with no name", async () => {\n const response = await fetch(`${process.env.ZUPLO_URL}/products`, {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: JSON.stringify({ price: 10 }),\n });\n expect(response.status).toBe(400);\n });\n});\n',
19
+ "zuplo.jsonc": '{\n // Sample project for the editor preview deployment. `version` pins the\n // runtime the gateway would build against.\n "version": 1,\n "runtime": {\n "compatibilityDate": "2026-05-19"\n }\n}\n',
20
+ };
21
+ //# sourceMappingURL=preview-project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview-project.js","sourceRoot":"","sources":["../../src/fixtures/preview-project.ts"],"names":[],"mappings":"AAAA,yDAAuD;AACvD,6EAA6E;AAC7E,2CAA2C;AAE3C;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,sBAAsB,EACpB,u+BAAu+B;IACz+B,wBAAwB,EACtB,gsFAAgsF;IAClsF,sBAAsB,EACpB,qgBAAmgB;IACrgB,2BAA2B,EACzB,yfAAyf;IAC3f,wBAAwB,EACtB,klBAAklB;IACplB,mBAAmB,EACjB,yKAAyK;IAC3K,cAAc,EACZ,kKAAkK;IACpK,2BAA2B,EACzB,4TAA4T;IAC9T,sBAAsB,EACpB,8cAA8c;IAChd,wBAAwB,EACtB,6iBAA6iB;IAC/iB,aAAa,EACX,+MAA+M;CAClN,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,5 @@
1
+ export { type AssetSource, createEditorApp, type EditorApp, type EditorAppOptions, } from "./app.js";
2
+ export { getLocalEditorConfig, type LocalEditorConfig, type LocalEditorLabels, } from "./config.js";
3
+ export { type ChangeNotifier, type DirEntry, type EditorPreferencesPort, type FileSystemPort, type FileWithStat, PathNotFoundError, type PathStat, type PathType, UnsafePathError, } from "./ports.js";
1
4
  export { type LocalEditorOptions, LocalEditorServer } from "./server.js";
2
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,iBAAiB,EACjB,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
+ export { createEditorApp, } from "./app.js";
2
+ export { getLocalEditorConfig, } from "./config.js";
3
+ export { PathNotFoundError, UnsafePathError, } from "./ports.js";
1
4
  export { LocalEditorServer } from "./server.js";
2
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,eAAe,GAGhB,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,oBAAoB,GAGrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAML,iBAAiB,EAGjB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAA2B,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * The platform capabilities the editor's routes need, stated in the routes' own
3
+ * terms rather than as a subset of `node:fs`.
4
+ *
5
+ * Everything here is injected, so the same Hono app runs against a developer's
6
+ * real project directory (the Zuplo CLI) and against an in-memory project (the
7
+ * per-PR preview Worker). Keeping the seam this narrow is what stops the two
8
+ * from drifting: a route can't reach for `node:fs` without adding a method
9
+ * here, and the preview build fails when something does.
10
+ */
11
+ /**
12
+ * A path that escaped the project root, e.g. `?path=../../../etc/passwd`.
13
+ *
14
+ * Rejecting traversal is the port's job rather than each route's, because a
15
+ * route that forgets is a directory-traversal hole and there is no other
16
+ * chokepoint every path passes through.
17
+ */
18
+ export declare class UnsafePathError extends Error {
19
+ readonly path: string;
20
+ constructor(path: string);
21
+ }
22
+ /**
23
+ * A path that doesn't exist.
24
+ *
25
+ * Routes branch on this instead of on `ENOENT`, so a miss reports identically
26
+ * however the filesystem is implemented — the case an errno-shaped port gets
27
+ * subtly wrong.
28
+ */
29
+ export declare class PathNotFoundError extends Error {
30
+ readonly path: string;
31
+ constructor(path: string);
32
+ }
33
+ export type PathType = "file" | "directory";
34
+ export interface DirEntry {
35
+ /** The entry's own name, not a path. */
36
+ name: string;
37
+ type: PathType;
38
+ }
39
+ export interface PathStat {
40
+ type: PathType;
41
+ mtime: Date;
42
+ }
43
+ export interface FileWithStat {
44
+ content: string;
45
+ /** The mtime of the content returned alongside it — see `readFileWithStat`. */
46
+ mtime: Date;
47
+ }
48
+ /**
49
+ * A project directory the editor can read and write.
50
+ *
51
+ * Every path is relative to the project root and slash-separated
52
+ * (`config/routes.oas.json`), with or without a leading slash. Implementations
53
+ * reject paths that climb out of the root with `UnsafePathError`.
54
+ */
55
+ export interface FileSystemPort {
56
+ /**
57
+ * Reads a file and the mtime describing *that* content, together.
58
+ *
59
+ * The pair has to be consistent: the portal saves the returned mtime as
60
+ * `Last-Modified` and sends it back as `If-Unmodified-Since` to detect
61
+ * clobbering, so a stat taken separately from the read can describe a
62
+ * different version of the file and produce phantom conflicts — or miss real
63
+ * ones. How that's achieved is the adapter's problem (Node holds a single
64
+ * file handle open across both).
65
+ *
66
+ * @throws PathNotFoundError when the file doesn't exist.
67
+ */
68
+ readFileWithStat(path: string): Promise<FileWithStat>;
69
+ /**
70
+ * Lists a directory's immediate children. Order is not significant.
71
+ *
72
+ * @throws PathNotFoundError when the directory doesn't exist.
73
+ */
74
+ listDir(path: string): Promise<DirEntry[]>;
75
+ /** Returns `undefined` when nothing exists at `path`. */
76
+ statPath(path: string): Promise<PathStat | undefined>;
77
+ /**
78
+ * Writes a file, creating any missing parent directories, and returns the
79
+ * stat it now has — the caller hands that mtime straight back to the client.
80
+ */
81
+ writeFile(path: string, content: string | Uint8Array): Promise<PathStat>;
82
+ /** @throws PathNotFoundError when `from` doesn't exist. */
83
+ rename(from: string, to: string): Promise<void>;
84
+ /** @throws PathNotFoundError when `path` doesn't exist. */
85
+ remove(path: string): Promise<void>;
86
+ /** Creates a directory and any missing parents. Succeeds if it exists. */
87
+ mkdirp(path: string): Promise<void>;
88
+ }
89
+ /**
90
+ * Which external editor the user picked for "open in editor".
91
+ *
92
+ * Separate from `FileSystemPort` because this lives outside the project — an
93
+ * XDG config file under the user's home directory on Node, and nowhere at all
94
+ * in a Worker, which has no home directory to write to.
95
+ */
96
+ export interface EditorPreferencesPort {
97
+ /** The saved editor id, or `null` if the user hasn't chosen one. */
98
+ get(): Promise<string | null>;
99
+ set(editor: string): Promise<void>;
100
+ }
101
+ /**
102
+ * Tells the editor a file changed underneath it, so the SPA can reload it.
103
+ *
104
+ * On Node this is chokidar watching the project directory. In the preview it's
105
+ * the in-memory filesystem reporting its own writes, which is what keeps the
106
+ * SPA's live-reload path exercised there rather than dormant.
107
+ */
108
+ export interface ChangeNotifier {
109
+ /**
110
+ * Registers `listener`, called with a project-relative path on each change.
111
+ * Returns a function that unsubscribes.
112
+ */
113
+ onChange(listener: (path: string) => void): () => void;
114
+ }
115
+ /**
116
+ * Splits a project-relative path into its segments, rejecting anything that
117
+ * climbs out of the project or isn't relative to begin with.
118
+ *
119
+ * Both adapters route every path through this, so traversal is refused once, at
120
+ * the boundary, instead of relying on each route to remember.
121
+ */
122
+ export declare function toSafeSegments(path: string): string[];
123
+ //# sourceMappingURL=ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAIvB;CACF;AAED;;;;;;GAMG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAIvB;CACF;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAC;AAE5C,MAAM,WAAW,QAAQ;IACvB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,IAAI,CAAC;CACb;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAA+E;IAC/E,KAAK,EAAE,IAAI,CAAC;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAEtD;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IAE3C,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IAEtD;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEzE,2DAA2D;IAC3D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD,2DAA2D;IAC3D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC,0EAA0E;IAC1E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,oEAAoE;IACpE,GAAG,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9B,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAwBrD"}
package/dist/ports.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The platform capabilities the editor's routes need, stated in the routes' own
3
+ * terms rather than as a subset of `node:fs`.
4
+ *
5
+ * Everything here is injected, so the same Hono app runs against a developer's
6
+ * real project directory (the Zuplo CLI) and against an in-memory project (the
7
+ * per-PR preview Worker). Keeping the seam this narrow is what stops the two
8
+ * from drifting: a route can't reach for `node:fs` without adding a method
9
+ * here, and the preview build fails when something does.
10
+ */
11
+ /**
12
+ * A path that escaped the project root, e.g. `?path=../../../etc/passwd`.
13
+ *
14
+ * Rejecting traversal is the port's job rather than each route's, because a
15
+ * route that forgets is a directory-traversal hole and there is no other
16
+ * chokepoint every path passes through.
17
+ */
18
+ export class UnsafePathError extends Error {
19
+ path;
20
+ constructor(path) {
21
+ super(`Path escapes the project directory: ${path}`);
22
+ this.name = "UnsafePathError";
23
+ this.path = path;
24
+ }
25
+ }
26
+ /**
27
+ * A path that doesn't exist.
28
+ *
29
+ * Routes branch on this instead of on `ENOENT`, so a miss reports identically
30
+ * however the filesystem is implemented — the case an errno-shaped port gets
31
+ * subtly wrong.
32
+ */
33
+ export class PathNotFoundError extends Error {
34
+ path;
35
+ constructor(path) {
36
+ super(`Path not found: ${path}`);
37
+ this.name = "PathNotFoundError";
38
+ this.path = path;
39
+ }
40
+ }
41
+ /**
42
+ * Splits a project-relative path into its segments, rejecting anything that
43
+ * climbs out of the project or isn't relative to begin with.
44
+ *
45
+ * Both adapters route every path through this, so traversal is refused once, at
46
+ * the boundary, instead of relying on each route to remember.
47
+ */
48
+ export function toSafeSegments(path) {
49
+ // A Windows drive letter or a UNC path is absolute but survives POSIX
50
+ // normalization, so it has to be caught by shape rather than by segment.
51
+ if (/^[a-zA-Z]:/.test(path) || path.startsWith("\\\\")) {
52
+ throw new UnsafePathError(path);
53
+ }
54
+ const segments = [];
55
+ // Backslashes are separators on Windows; treating them as ordinary characters
56
+ // would let `..\..` through as a single innocuous-looking segment.
57
+ for (const segment of path.split(/[/\\]+/)) {
58
+ if (segment === "" || segment === ".") {
59
+ continue;
60
+ }
61
+ if (segment === "..") {
62
+ // Refuse rather than resolve. `config/../modules` is legitimate but
63
+ // nothing the portal sends, and allowing it means the guard depends on
64
+ // segment order — every prefix would have to stay inside the root too.
65
+ throw new UnsafePathError(path);
66
+ }
67
+ segments.push(segment);
68
+ }
69
+ return segments;
70
+ }
71
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE;QACxB,KAAK,CAAC,uCAAuC,IAAI,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAAA,CAClB;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACjC,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE;QACxB,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAAA,CAClB;CACF;AAiGD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY,EAAY;IACrD,sEAAsE;IACtE,yEAAyE;IACzE,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,8EAA8E;IAC9E,mEAAmE;IACnE,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3C,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;YACtC,SAAS;QACX,CAAC;QACD,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,oEAAoE;YACpE,uEAAuE;YACvE,yEAAuE;YACvE,MAAM,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;QAClC,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,QAAQ,CAAC;AAAA,CACjB"}
@@ -1,7 +1,12 @@
1
1
  import { Hono } from "hono";
2
2
  import type { CommonLogger, SseClient } from "../interfaces.js";
3
+ import type { EditorPreferencesPort, FileSystemPort } from "../ports.js";
3
4
  interface ApiRoutesOptions {
5
+ /** Only used to build external-editor deep links — file access goes via `fs`. */
4
6
  sourceDirectory: string;
7
+ fs: FileSystemPort;
8
+ /** Distinct from the portal's own user preferences stubbed further down. */
9
+ editorPreferences: EditorPreferencesPort;
5
10
  logger: CommonLogger;
6
11
  sseClients: Set<SseClient>;
7
12
  deploymentUrl: string;
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/routes/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAQhE,UAAU,gBAAgB;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;CACvB;AAMD,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAkI/D"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/routes/api.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQzE,UAAU,gBAAgB;IACxB,mFAAiF;IACjF,eAAe,EAAE,MAAM,CAAC;IACxB,EAAE,EAAE,cAAc,CAAC;IACnB,4EAA4E;IAC5E,iBAAiB,EAAE,qBAAqB,CAAC;IACzC,MAAM,EAAE,YAAY,CAAC;IACrB,UAAU,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;CACvB;AAMD,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA+I/D"}
@@ -12,12 +12,15 @@ const PreferencesBody = Type.Object({
12
12
  data: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
13
13
  });
14
14
  export function createApiRoutes(options) {
15
- const { sourceDirectory, logger, sseClients, deploymentUrl } = options;
15
+ const { sourceDirectory, fs, editorPreferences, logger, sseClients, deploymentUrl, } = options;
16
16
  const app = new Hono();
17
17
  // Filesystem routes
18
- app.route("/v1/deployments/:deploymentName/fs", createFsRoutes({ sourceDirectory, logger }));
18
+ app.route("/v1/deployments/:deploymentName/fs", createFsRoutes({ fs, logger }));
19
19
  // Editor routes (for opening files in external editor)
20
- app.route("/v1/editor", createEditorRoutes({ sourceDirectory }));
20
+ app.route("/v1/editor", createEditorRoutes({
21
+ sourceDirectory,
22
+ preferences: editorPreferences,
23
+ }));
21
24
  // Test routes (for proxying test requests to local Zuplo gateway). Mounted
22
25
  // twice because Fastify answered on both spellings and Hono doesn't: the
23
26
  // portal posts to /v1/test, but the real test-proxy accepts either.
@@ -70,11 +73,11 @@ export function createApiRoutes(options) {
70
73
  // state from GET /users/me/preferences (and writes via PUT). Local mode has
71
74
  // no real user store — keep an in-memory copy for the process lifetime so
72
75
  // the editor can load and the expand/collapse toggle still works in-session.
73
- let preferences = {};
74
- app.get("/v1/users/me/preferences", (c) => c.json({ data: preferences }));
76
+ let userPreferences = {};
77
+ app.get("/v1/users/me/preferences", (c) => c.json({ data: userPreferences }));
75
78
  app.put("/v1/users/me/preferences", validate("json", PreferencesBody), (c) => {
76
- preferences = c.req.valid("json").data ?? {};
77
- return c.json({ data: preferences });
79
+ userPreferences = c.req.valid("json").data ?? {};
80
+ return c.json({ data: userPreferences });
78
81
  });
79
82
  // Get deployment info (includes URL for local gateway)
80
83
  app.get("/v1/deployments/:deploymentName", (c) => c.json({