@imgly/codesign-mcp 0.1.1 → 0.1.2

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 (115) hide show
  1. package/dist/cli/stdio.d.ts.map +1 -1
  2. package/dist/cli/stdio.js +54 -21
  3. package/dist/cli/stdio.js.map +1 -1
  4. package/dist/engine/cesdk-adapter.d.ts +7 -1
  5. package/dist/engine/cesdk-adapter.d.ts.map +1 -1
  6. package/dist/engine/cesdk-adapter.js +7 -1
  7. package/dist/engine/cesdk-adapter.js.map +1 -1
  8. package/dist/http/server.d.ts.map +1 -1
  9. package/dist/http/server.js +0 -1
  10. package/dist/http/server.js.map +1 -1
  11. package/dist/server.d.ts +0 -2
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js.map +1 -1
  14. package/dist/test/test/cli/redirect-console.test.d.ts +2 -0
  15. package/dist/test/test/cli/redirect-console.test.d.ts.map +1 -0
  16. package/dist/test/test/cli/redirect-console.test.js +47 -0
  17. package/dist/test/test/cli/redirect-console.test.js.map +1 -0
  18. package/dist/test/test/cli/resolve-license.test.d.ts +2 -0
  19. package/dist/test/test/cli/resolve-license.test.d.ts.map +1 -0
  20. package/dist/test/test/cli/resolve-license.test.js +20 -0
  21. package/dist/test/test/cli/resolve-license.test.js.map +1 -0
  22. package/dist/test/test/cli/stdio-license.test.d.ts +2 -0
  23. package/dist/test/test/cli/stdio-license.test.d.ts.map +1 -0
  24. package/dist/test/test/cli/stdio-license.test.js +19 -0
  25. package/dist/test/test/cli/stdio-license.test.js.map +1 -0
  26. package/dist/test/test/config.test.d.ts +2 -0
  27. package/dist/test/test/config.test.d.ts.map +1 -0
  28. package/dist/test/test/config.test.js +91 -0
  29. package/dist/test/test/config.test.js.map +1 -0
  30. package/dist/test/test/engine/asset-sources.test.d.ts +2 -0
  31. package/dist/test/test/engine/asset-sources.test.d.ts.map +1 -0
  32. package/dist/test/test/engine/asset-sources.test.js +100 -0
  33. package/dist/test/test/engine/asset-sources.test.js.map +1 -0
  34. package/dist/test/test/engine/mutex.test.d.ts +2 -0
  35. package/dist/test/test/engine/mutex.test.d.ts.map +1 -0
  36. package/dist/test/test/engine/mutex.test.js +63 -0
  37. package/dist/test/test/engine/mutex.test.js.map +1 -0
  38. package/dist/test/test/engine/session.test.js +15 -0
  39. package/dist/test/test/engine/session.test.js.map +1 -1
  40. package/dist/test/test/engine/unsplash-source.test.d.ts +2 -0
  41. package/dist/test/test/engine/unsplash-source.test.d.ts.map +1 -0
  42. package/dist/test/test/engine/unsplash-source.test.js +129 -0
  43. package/dist/test/test/engine/unsplash-source.test.js.map +1 -0
  44. package/dist/test/test/handbook-contract.test.js +8 -4
  45. package/dist/test/test/handbook-contract.test.js.map +1 -1
  46. package/dist/test/test/http/server.test.js +129 -0
  47. package/dist/test/test/http/server.test.js.map +1 -1
  48. package/dist/test/test/http/session-id-header.test.d.ts +2 -0
  49. package/dist/test/test/http/session-id-header.test.d.ts.map +1 -0
  50. package/dist/test/test/http/session-id-header.test.js +76 -0
  51. package/dist/test/test/http/session-id-header.test.js.map +1 -0
  52. package/dist/test/test/http/session-manager.test.js +14 -0
  53. package/dist/test/test/http/session-manager.test.js.map +1 -1
  54. package/dist/test/test/http/workspace-auth.test.d.ts +2 -0
  55. package/dist/test/test/http/workspace-auth.test.d.ts.map +1 -0
  56. package/dist/test/test/http/workspace-auth.test.js +41 -0
  57. package/dist/test/test/http/workspace-auth.test.js.map +1 -0
  58. package/dist/test/test/metrics/mount.test.d.ts +2 -0
  59. package/dist/test/test/metrics/mount.test.d.ts.map +1 -0
  60. package/dist/test/test/metrics/mount.test.js +59 -0
  61. package/dist/test/test/metrics/mount.test.js.map +1 -0
  62. package/dist/test/test/metrics/recorder.test.d.ts +2 -0
  63. package/dist/test/test/metrics/recorder.test.d.ts.map +1 -0
  64. package/dist/test/test/metrics/recorder.test.js +118 -0
  65. package/dist/test/test/metrics/recorder.test.js.map +1 -0
  66. package/dist/test/test/revisions/store.test.js +81 -11
  67. package/dist/test/test/revisions/store.test.js.map +1 -1
  68. package/dist/test/test/server.metrics.test.d.ts +2 -0
  69. package/dist/test/test/server.metrics.test.d.ts.map +1 -0
  70. package/dist/test/test/server.metrics.test.js +87 -0
  71. package/dist/test/test/server.metrics.test.js.map +1 -0
  72. package/dist/test/test/server.test.js +33 -4
  73. package/dist/test/test/server.test.js.map +1 -1
  74. package/dist/test/test/tools/asset-search.test.d.ts +2 -0
  75. package/dist/test/test/tools/asset-search.test.d.ts.map +1 -0
  76. package/dist/test/test/tools/asset-search.test.js +121 -0
  77. package/dist/test/test/tools/asset-search.test.js.map +1 -0
  78. package/dist/test/test/tools/edit.test.js +71 -9
  79. package/dist/test/test/tools/edit.test.js.map +1 -1
  80. package/dist/test/test/tools/export.test.js +60 -30
  81. package/dist/test/test/tools/export.test.js.map +1 -1
  82. package/dist/test/test/tools/index.test.d.ts +2 -0
  83. package/dist/test/test/tools/index.test.d.ts.map +1 -0
  84. package/dist/test/test/tools/index.test.js +55 -0
  85. package/dist/test/test/tools/index.test.js.map +1 -0
  86. package/dist/test/test/tools/metrics.test.d.ts +2 -0
  87. package/dist/test/test/tools/metrics.test.d.ts.map +1 -0
  88. package/dist/test/test/tools/metrics.test.js +29 -0
  89. package/dist/test/test/tools/metrics.test.js.map +1 -0
  90. package/dist/test/test/tools/preview.test.js +6 -4
  91. package/dist/test/test/tools/preview.test.js.map +1 -1
  92. package/dist/test/test/tools/rename.test.js +1 -2
  93. package/dist/test/test/tools/rename.test.js.map +1 -1
  94. package/dist/test/test/tools/save.test.d.ts +2 -0
  95. package/dist/test/test/tools/save.test.d.ts.map +1 -0
  96. package/dist/test/test/tools/save.test.js +137 -0
  97. package/dist/test/test/tools/save.test.js.map +1 -0
  98. package/dist/test/test/viewer/router.test.js +56 -0
  99. package/dist/test/test/viewer/router.test.js.map +1 -1
  100. package/dist/test/test/viewer/session-mount.test.d.ts +2 -0
  101. package/dist/test/test/viewer/session-mount.test.d.ts.map +1 -0
  102. package/dist/test/test/viewer/session-mount.test.js +52 -0
  103. package/dist/test/test/viewer/session-mount.test.js.map +1 -0
  104. package/dist/test/test/viewer/url-builder.test.js +76 -9
  105. package/dist/test/test/viewer/url-builder.test.js.map +1 -1
  106. package/dist/test/test/workspace/s3.test.d.ts +2 -0
  107. package/dist/test/test/workspace/s3.test.d.ts.map +1 -0
  108. package/dist/test/test/workspace/s3.test.js +153 -0
  109. package/dist/test/test/workspace/s3.test.js.map +1 -0
  110. package/dist/test/test/workspace/scoped.test.d.ts +2 -0
  111. package/dist/test/test/workspace/scoped.test.d.ts.map +1 -0
  112. package/dist/test/test/workspace/scoped.test.js +59 -0
  113. package/dist/test/test/workspace/scoped.test.js.map +1 -0
  114. package/dist/test/tsconfig.test.tsbuildinfo +1 -1
  115. package/package.json +1 -1
@@ -13,6 +13,18 @@ async function makeApp() {
13
13
  app.use(createViewerRouter({ store, broadcaster, fs }));
14
14
  return { app, fs, store, broadcaster };
15
15
  }
16
+ // The HTTP transport mounts the router under "/viewer"; the stdio listener
17
+ // mounts it at root. The SPA must address its assets/API relative to the live
18
+ // mount path, injected at serve time. This mounts under a prefix to lock that
19
+ // in.
20
+ async function makeMountedApp(mount) {
21
+ const fs = new InMemoryWorkspaceFS();
22
+ const store = new RevisionStore(fs);
23
+ const broadcaster = new SseBroadcaster();
24
+ const app = express();
25
+ app.use(mount, createViewerRouter({ store, broadcaster, fs }));
26
+ return { app, fs, store, broadcaster };
27
+ }
16
28
  describe('viewer router', () => {
17
29
  it('GET / serves the studio HTML shell', async () => {
18
30
  const { app } = await makeApp();
@@ -73,6 +85,14 @@ describe('viewer router', () => {
73
85
  title: 'T'
74
86
  });
75
87
  });
88
+ it('GET /snapshot/<rev> reflects a renamed title (live, not creation-time)', async () => {
89
+ const { app, store } = await makeApp();
90
+ const r = await store.write({ scene: 'S', parent: null, title: 'Before' });
91
+ await store.renameRoot(r.id, 'After');
92
+ const res = await request(app).get(`/snapshot/${r.id}`);
93
+ expect(res.status).toBe(200);
94
+ expect(res.body.title).toBe('After');
95
+ });
76
96
  it('GET /snapshot/<rev>/thumbnail returns the per-revision webp if present', async () => {
77
97
  const { app, store, fs } = await makeApp();
78
98
  const r = await store.write({ scene: 'S', parent: null, title: 'T' });
@@ -151,6 +171,42 @@ describe('viewer router', () => {
151
171
  expect(res.status).toBe(200);
152
172
  expect(res.body.resourceBase).toBe('/r/');
153
173
  });
174
+ it('serves a root-relative SPA base when mounted at root', async () => {
175
+ const { app, store } = await makeApp();
176
+ const r = await store.write({ scene: 'S', parent: null, title: 'T' });
177
+ const res = await request(app).get(`/d/${r.id}`);
178
+ expect(res.text).toContain("window.__VIEWER_BASE__ = '';");
179
+ expect(res.text).toContain('src="/static/viewer.js"');
180
+ });
181
+ it('injects the mount prefix into the SPA base when mounted at /viewer', async () => {
182
+ const { app, store } = await makeMountedApp('/viewer');
183
+ const r = await store.write({ scene: 'S', parent: null, title: 'T' });
184
+ const res = await request(app).get(`/viewer/d/${r.id}`);
185
+ expect(res.status).toBe(200);
186
+ expect(res.text).toContain("window.__VIEWER_BASE__ = '/viewer';");
187
+ expect(res.text).toContain('src="/viewer/static/viewer.js"');
188
+ // No leftover unprefixed asset path that would 404 under the mount.
189
+ expect(res.text).not.toContain('src="/static/viewer.js"');
190
+ });
191
+ it('studio shell carries the mount prefix too when mounted at /viewer', async () => {
192
+ const { app } = await makeMountedApp('/viewer');
193
+ const res = await request(app).get('/viewer/');
194
+ expect(res.status).toBe(200);
195
+ expect(res.text).toContain("window.__VIEWER_BASE__ = '/viewer';");
196
+ expect(res.text).toContain('src="/viewer/static/studio.js"');
197
+ });
198
+ it('resourceBase is mount-relative when mounted at /viewer', async () => {
199
+ const { app } = await makeMountedApp('/viewer');
200
+ const res = await request(app).get('/viewer/api/config');
201
+ expect(res.status).toBe(200);
202
+ expect(res.body.resourceBase).toBe('/viewer/r/');
203
+ });
204
+ it('serves the SPA JS under the mount prefix', async () => {
205
+ const { app } = await makeMountedApp('/viewer');
206
+ const res = await request(app).get('/viewer/static/viewer.js');
207
+ expect(res.status).toBe(200);
208
+ expect(res.headers['content-type']).toMatch(/javascript/);
209
+ });
154
210
  it('GET /r/<asset> is reachable through the viewer router', async () => {
155
211
  const { app, fs } = await makeApp();
156
212
  await fs.writeFile('assets/abc.png', new Uint8Array([1, 2, 3]));
@@ -1 +1 @@
1
- {"version":3,"file":"router.test.js","sourceRoot":"","sources":["../../../../test/viewer/router.test.ts"],"names":[],"mappings":"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","sourcesContent":["import express from 'express';\nimport request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { SseBroadcaster } from '../../src/viewer/broadcaster.js';\nimport { createViewerRouter } from '../../src/viewer/router.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nasync function makeApp() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const broadcaster = new SseBroadcaster();\n const app = express();\n app.use(createViewerRouter({ store, broadcaster, fs }));\n return { app, fs, store, broadcaster };\n}\n\ndescribe('viewer router', () => {\n it('GET / serves the studio HTML shell', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('IMG.LY CODESIGN MCP Workspace');\n });\n\n it('GET /d/<root> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /d/<root>/<rev> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /s/<rev> is removed — returns 404', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/s/${r.id}`);\n expect(res.status).toBe(404);\n });\n\n it('GET /static/viewer.js serves the SPA JS', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/viewer.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('CreativeEditorSDK');\n expect(res.text).toContain('setURIResolver');\n });\n\n it('GET /static/revision-panel.js serves the panel module', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/revision-panel.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('createRevisionPanel');\n });\n\n it('GET /snapshot/<rev> returns scene + meta + rootRevision', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({\n scene: 'SCENE_BYTES',\n parent: null,\n title: 'T'\n });\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n scene: 'SCENE_BYTES',\n rootRevision: r.id,\n title: 'T'\n });\n });\n\n it('GET /snapshot/<rev>/thumbnail returns the per-revision webp if present', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const shard = r.id.slice(0, 2);\n const rest = r.id.slice(2);\n const bytes = new Uint8Array([1, 2, 3, 4]);\n await fs.writeFile(`exports/${shard}/${rest}/preview.webp`, bytes);\n const res = await request(app).get(`/snapshot/${r.id}/thumbnail`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toBe('image/webp');\n });\n\n it('GET /api/designs returns the root list', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get('/api/designs');\n expect(res.status).toBe(200);\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ rootRevision: r.id, title: 'T1' })\n ])\n );\n });\n\n it(\"GET /api/design/<root> returns that design's record\", async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get(`/api/design/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n rootRevision: r.id,\n title: 'T1',\n latestLeaf: r.id\n });\n });\n\n it('GET /api/design/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it(\"GET /api/design/<root>/revisions returns the design's revision tree\", async () => {\n const { app, store } = await makeApp();\n const r1 = await store.write({ scene: 'a', parent: null, title: 'T' });\n const r2 = await store.write({ scene: 'b', parent: r1.id, note: 'second' });\n const fork = await store.write({\n scene: 'b2',\n parent: r1.id,\n note: 'fork'\n });\n const res = await request(app).get(`/api/design/${r1.id}/revisions`);\n expect(res.status).toBe(200);\n expect(res.body.map((n: { id: string }) => n.id).sort()).toEqual(\n [r1.id, r2.id, fork.id].sort()\n );\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ id: r2.id, parent: r1.id, note: 'second' })\n ])\n );\n });\n\n it('GET /api/design/<unknown>/revisions returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead/revisions');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it('a /api/design/<root>/revisions error is sanitised to a generic 500', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/api/design/${r.id}/revisions`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n });\n\n it('GET /snapshot/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/snapshot/deadbeefdead');\n expect(res.status).toBe(404);\n });\n\n it('GET /api/config includes the /r/ resourceBase', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/config');\n expect(res.status).toBe(200);\n expect(res.body.resourceBase).toBe('/r/');\n });\n\n it('GET /r/<asset> is reachable through the viewer router', async () => {\n const { app, fs } = await makeApp();\n await fs.writeFile('assets/abc.png', new Uint8Array([1, 2, 3]));\n const res = await request(app).get('/r/assets/abc.png');\n expect(res.status).toBe(200);\n });\n\n it('a route error is sanitised to a generic 500 (no leak)', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n // Corrupt the revision meta so store.read throws a non-ENOENT parse error.\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n expect(res.text).not.toContain('not json');\n });\n});\n"]}
1
+ {"version":3,"file":"router.test.js","sourceRoot":"","sources":["../../../../test/viewer/router.test.ts"],"names":[],"mappings":"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","sourcesContent":["import express from 'express';\nimport request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { SseBroadcaster } from '../../src/viewer/broadcaster.js';\nimport { createViewerRouter } from '../../src/viewer/router.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\n\nasync function makeApp() {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const broadcaster = new SseBroadcaster();\n const app = express();\n app.use(createViewerRouter({ store, broadcaster, fs }));\n return { app, fs, store, broadcaster };\n}\n\n// The HTTP transport mounts the router under \"/viewer\"; the stdio listener\n// mounts it at root. The SPA must address its assets/API relative to the live\n// mount path, injected at serve time. This mounts under a prefix to lock that\n// in.\nasync function makeMountedApp(mount: string) {\n const fs = new InMemoryWorkspaceFS();\n const store = new RevisionStore(fs);\n const broadcaster = new SseBroadcaster();\n const app = express();\n app.use(mount, createViewerRouter({ store, broadcaster, fs }));\n return { app, fs, store, broadcaster };\n}\n\ndescribe('viewer router', () => {\n it('GET / serves the studio HTML shell', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('IMG.LY CODESIGN MCP Workspace');\n });\n\n it('GET /d/<root> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /d/<root>/<rev> serves the viewer HTML shell', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('codesign viewer');\n });\n\n it('GET /s/<rev> is removed — returns 404', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/s/${r.id}`);\n expect(res.status).toBe(404);\n });\n\n it('GET /static/viewer.js serves the SPA JS', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/viewer.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('CreativeEditorSDK');\n expect(res.text).toContain('setURIResolver');\n });\n\n it('GET /static/revision-panel.js serves the panel module', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/static/revision-panel.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n expect(res.text).toContain('createRevisionPanel');\n });\n\n it('GET /snapshot/<rev> returns scene + meta + rootRevision', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({\n scene: 'SCENE_BYTES',\n parent: null,\n title: 'T'\n });\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n scene: 'SCENE_BYTES',\n rootRevision: r.id,\n title: 'T'\n });\n });\n\n it('GET /snapshot/<rev> reflects a renamed title (live, not creation-time)', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'Before' });\n await store.renameRoot(r.id, 'After');\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body.title).toBe('After');\n });\n\n it('GET /snapshot/<rev>/thumbnail returns the per-revision webp if present', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const shard = r.id.slice(0, 2);\n const rest = r.id.slice(2);\n const bytes = new Uint8Array([1, 2, 3, 4]);\n await fs.writeFile(`exports/${shard}/${rest}/preview.webp`, bytes);\n const res = await request(app).get(`/snapshot/${r.id}/thumbnail`);\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toBe('image/webp');\n });\n\n it('GET /api/designs returns the root list', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get('/api/designs');\n expect(res.status).toBe(200);\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ rootRevision: r.id, title: 'T1' })\n ])\n );\n });\n\n it(\"GET /api/design/<root> returns that design's record\", async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T1' });\n const res = await request(app).get(`/api/design/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.body).toMatchObject({\n rootRevision: r.id,\n title: 'T1',\n latestLeaf: r.id\n });\n });\n\n it('GET /api/design/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it(\"GET /api/design/<root>/revisions returns the design's revision tree\", async () => {\n const { app, store } = await makeApp();\n const r1 = await store.write({ scene: 'a', parent: null, title: 'T' });\n const r2 = await store.write({ scene: 'b', parent: r1.id, note: 'second' });\n const fork = await store.write({\n scene: 'b2',\n parent: r1.id,\n note: 'fork'\n });\n const res = await request(app).get(`/api/design/${r1.id}/revisions`);\n expect(res.status).toBe(200);\n expect(res.body.map((n: { id: string }) => n.id).sort()).toEqual(\n [r1.id, r2.id, fork.id].sort()\n );\n expect(res.body).toEqual(\n expect.arrayContaining([\n expect.objectContaining({ id: r2.id, parent: r1.id, note: 'second' })\n ])\n );\n });\n\n it('GET /api/design/<unknown>/revisions returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/design/deadbeefdead/revisions');\n expect(res.status).toBe(404);\n expect(res.body).toEqual({ error: 'unknown design' });\n });\n\n it('a /api/design/<root>/revisions error is sanitised to a generic 500', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/api/design/${r.id}/revisions`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n });\n\n it('GET /snapshot/<unknown> returns 404', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/snapshot/deadbeefdead');\n expect(res.status).toBe(404);\n });\n\n it('GET /api/config includes the /r/ resourceBase', async () => {\n const { app } = await makeApp();\n const res = await request(app).get('/api/config');\n expect(res.status).toBe(200);\n expect(res.body.resourceBase).toBe('/r/');\n });\n\n it('serves a root-relative SPA base when mounted at root', async () => {\n const { app, store } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/d/${r.id}`);\n expect(res.text).toContain(\"window.__VIEWER_BASE__ = '';\");\n expect(res.text).toContain('src=\"/static/viewer.js\"');\n });\n\n it('injects the mount prefix into the SPA base when mounted at /viewer', async () => {\n const { app, store } = await makeMountedApp('/viewer');\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n const res = await request(app).get(`/viewer/d/${r.id}`);\n expect(res.status).toBe(200);\n expect(res.text).toContain(\"window.__VIEWER_BASE__ = '/viewer';\");\n expect(res.text).toContain('src=\"/viewer/static/viewer.js\"');\n // No leftover unprefixed asset path that would 404 under the mount.\n expect(res.text).not.toContain('src=\"/static/viewer.js\"');\n });\n\n it('studio shell carries the mount prefix too when mounted at /viewer', async () => {\n const { app } = await makeMountedApp('/viewer');\n const res = await request(app).get('/viewer/');\n expect(res.status).toBe(200);\n expect(res.text).toContain(\"window.__VIEWER_BASE__ = '/viewer';\");\n expect(res.text).toContain('src=\"/viewer/static/studio.js\"');\n });\n\n it('resourceBase is mount-relative when mounted at /viewer', async () => {\n const { app } = await makeMountedApp('/viewer');\n const res = await request(app).get('/viewer/api/config');\n expect(res.status).toBe(200);\n expect(res.body.resourceBase).toBe('/viewer/r/');\n });\n\n it('serves the SPA JS under the mount prefix', async () => {\n const { app } = await makeMountedApp('/viewer');\n const res = await request(app).get('/viewer/static/viewer.js');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/javascript/);\n });\n\n it('GET /r/<asset> is reachable through the viewer router', async () => {\n const { app, fs } = await makeApp();\n await fs.writeFile('assets/abc.png', new Uint8Array([1, 2, 3]));\n const res = await request(app).get('/r/assets/abc.png');\n expect(res.status).toBe(200);\n });\n\n it('a route error is sanitised to a generic 500 (no leak)', async () => {\n const { app, store, fs } = await makeApp();\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n // Corrupt the revision meta so store.read throws a non-ENOENT parse error.\n await fs.writeFile(\n `objects/${r.id.slice(0, 2)}/${r.id.slice(2)}.json`,\n 'not json'\n );\n const res = await request(app).get(`/snapshot/${r.id}`);\n expect(res.status).toBe(500);\n expect(res.text).toBe('internal error');\n expect(res.text).not.toContain('not json');\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-mount.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-mount.test.d.ts","sourceRoot":"","sources":["../../../../test/viewer/session-mount.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,52 @@
1
+ import express from 'express';
2
+ import request from 'supertest';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { RevisionStore } from '../../src/revisions/store.js';
5
+ import { SseBroadcaster } from '../../src/viewer/broadcaster.js';
6
+ import { mountSessionViewer } from '../../src/viewer/session-mount.js';
7
+ import { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';
8
+ import { ScopedWorkspaceFS } from '../../src/workspace/scoped.js';
9
+ function makeApp() {
10
+ const rawFs = new InMemoryWorkspaceFS();
11
+ const app = express();
12
+ mountSessionViewer(app, {
13
+ rawFs,
14
+ broadcaster: new SseBroadcaster(),
15
+ cesdkLicense: 'lic-123'
16
+ });
17
+ return { app, rawFs };
18
+ }
19
+ describe('session viewer mount (/v/:sid)', () => {
20
+ it('serves the studio shell with the per-session base injected', async () => {
21
+ const { app } = makeApp();
22
+ const res = await request(app).get('/v/sid-1/');
23
+ expect(res.status).toBe(200);
24
+ expect(res.headers['content-type']).toMatch(/html/);
25
+ expect(res.text).toContain('/v/sid-1');
26
+ });
27
+ it('scopes data to the sid: a design in s/<sid> is visible only there', async () => {
28
+ const { app, rawFs } = makeApp();
29
+ const store = new RevisionStore(new ScopedWorkspaceFS(rawFs, 's/sid-1'));
30
+ const r = await store.write({ scene: 'S', parent: null, title: 'T' });
31
+ const mine = await request(app).get('/v/sid-1/api/designs');
32
+ expect(mine.status).toBe(200);
33
+ expect(mine.body.map((d) => d.rootRevision)).toContain(r.id);
34
+ const theirs = await request(app).get('/v/sid-2/api/designs');
35
+ expect(theirs.status).toBe(200);
36
+ expect(theirs.body).toEqual([]);
37
+ });
38
+ it('serves the license via /api/config', async () => {
39
+ const { app } = makeApp();
40
+ const res = await request(app).get('/v/sid-1/api/config');
41
+ expect(res.status).toBe(200);
42
+ expect(res.body.license).toBe('lic-123');
43
+ expect(res.body.resourceBase).toBe('/v/sid-1/r/');
44
+ });
45
+ it('rejects a traversal sid with 404', async () => {
46
+ const { app } = makeApp();
47
+ // %2E%2E decodes to ".." — sanitizeWorkspaceId must reject it.
48
+ const res = await request(app).get('/v/%2E%2E/api/designs');
49
+ expect(res.status).toBe(404);
50
+ });
51
+ });
52
+ //# sourceMappingURL=session-mount.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-mount.test.js","sourceRoot":"","sources":["../../../../test/viewer/session-mount.test.ts"],"names":[],"mappings":"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","sourcesContent":["import express from 'express';\nimport request from 'supertest';\nimport { describe, expect, it } from 'vitest';\nimport { RevisionStore } from '../../src/revisions/store.js';\nimport { SseBroadcaster } from '../../src/viewer/broadcaster.js';\nimport { mountSessionViewer } from '../../src/viewer/session-mount.js';\nimport { InMemoryWorkspaceFS } from '../../src/workspace/in-memory.js';\nimport { ScopedWorkspaceFS } from '../../src/workspace/scoped.js';\n\nfunction makeApp() {\n const rawFs = new InMemoryWorkspaceFS();\n const app = express();\n mountSessionViewer(app, {\n rawFs,\n broadcaster: new SseBroadcaster(),\n cesdkLicense: 'lic-123'\n });\n return { app, rawFs };\n}\n\ndescribe('session viewer mount (/v/:sid)', () => {\n it('serves the studio shell with the per-session base injected', async () => {\n const { app } = makeApp();\n const res = await request(app).get('/v/sid-1/');\n expect(res.status).toBe(200);\n expect(res.headers['content-type']).toMatch(/html/);\n expect(res.text).toContain('/v/sid-1');\n });\n\n it('scopes data to the sid: a design in s/<sid> is visible only there', async () => {\n const { app, rawFs } = makeApp();\n const store = new RevisionStore(new ScopedWorkspaceFS(rawFs, 's/sid-1'));\n const r = await store.write({ scene: 'S', parent: null, title: 'T' });\n\n const mine = await request(app).get('/v/sid-1/api/designs');\n expect(mine.status).toBe(200);\n expect(\n mine.body.map((d: { rootRevision: string }) => d.rootRevision)\n ).toContain(r.id);\n\n const theirs = await request(app).get('/v/sid-2/api/designs');\n expect(theirs.status).toBe(200);\n expect(theirs.body).toEqual([]);\n });\n\n it('serves the license via /api/config', async () => {\n const { app } = makeApp();\n const res = await request(app).get('/v/sid-1/api/config');\n expect(res.status).toBe(200);\n expect(res.body.license).toBe('lic-123');\n expect(res.body.resourceBase).toBe('/v/sid-1/r/');\n });\n\n it('rejects a traversal sid with 404', async () => {\n const { app } = makeApp();\n // %2E%2E decodes to \"..\" — sanitizeWorkspaceId must reject it.\n const res = await request(app).get('/v/%2E%2E/api/designs');\n expect(res.status).toBe(404);\n });\n});\n"]}
@@ -1,5 +1,7 @@
1
+ import { S3Client } from '@aws-sdk/client-s3';
1
2
  import { describe, expect, it } from 'vitest';
2
- import { HostedStubViewerUrlBuilder, LocalViewerUrlBuilder } from '../../src/viewer/url-builder.js';
3
+ import { LocalViewerUrlBuilder, viewerBaseFor } from '../../src/viewer/url-builder.js';
4
+ import { PresignedR2UrlBuilder } from '../../src/viewer/presigned-r2-url-builder.js';
3
5
  describe('LocalViewerUrlBuilder', () => {
4
6
  it('builds /d/<root> design URLs against the listener base', () => {
5
7
  const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });
@@ -17,15 +19,80 @@ describe('LocalViewerUrlBuilder', () => {
17
19
  const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });
18
20
  expect(b.forResource('assets/9f2a1c.png')).toBe('http://127.0.0.1:8745/r/assets/9f2a1c.png');
19
21
  });
22
+ it('forDownload resolves to the same URL as forResource', async () => {
23
+ const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });
24
+ const relpath = 'exports/ab/cd1234/image.png';
25
+ await expect(b.forDownload(relpath)).resolves.toBe(b.forResource(relpath));
26
+ });
27
+ });
28
+ describe('viewerBaseFor', () => {
29
+ it('session mode + public base → /v/<sid> on the public origin', () => {
30
+ expect(viewerBaseFor({
31
+ publicBaseUrl: 'https://codesign.example.com',
32
+ port: 8080,
33
+ mode: 'session',
34
+ sessionId: 'abc-123'
35
+ })).toBe('https://codesign.example.com/v/abc-123');
36
+ });
37
+ it('session mode without public base → loopback /v/<sid>', () => {
38
+ expect(viewerBaseFor({
39
+ publicBaseUrl: undefined,
40
+ port: 8745,
41
+ mode: 'session',
42
+ sessionId: 'abc-123'
43
+ })).toBe('http://127.0.0.1:8745/v/abc-123');
44
+ });
45
+ it('single mode → /viewer (the fixed-workspace mount)', () => {
46
+ expect(viewerBaseFor({
47
+ publicBaseUrl: undefined,
48
+ port: 8745,
49
+ mode: 'single',
50
+ sessionId: 'abc-123'
51
+ })).toBe('http://127.0.0.1:8745/viewer');
52
+ });
20
53
  });
21
- describe('HostedStubViewerUrlBuilder', () => {
22
- it('forDesign throws Phase 2 not implemented', () => {
23
- const b = new HostedStubViewerUrlBuilder();
24
- expect(() => b.forDesign('abc123def456')).toThrow('Phase 2');
25
- });
26
- it('forPinnedRevision throws — Phase 2 not implemented', () => {
27
- const b = new HostedStubViewerUrlBuilder();
28
- expect(() => b.forPinnedRevision('abc123def456', '0011deadbeef')).toThrow('Phase 2');
54
+ describe('PresignedR2UrlBuilder', () => {
55
+ // getSignedUrl from @aws-sdk/s3-request-presigner signs locally no network
56
+ // call is made. We use dummy credentials and a fake R2 endpoint.
57
+ const client = new S3Client({
58
+ region: 'auto',
59
+ endpoint: 'https://accountid.r2.cloudflarestorage.com',
60
+ forcePathStyle: true,
61
+ credentials: { accessKeyId: 'test', secretAccessKey: 'test' }
62
+ });
63
+ const workspaceId = 'ws-test-001';
64
+ const builder = new PresignedR2UrlBuilder({
65
+ base: 'http://127.0.0.1:8745',
66
+ client,
67
+ bucket: 'codesign-bucket',
68
+ workspaceId,
69
+ expiresIn: 300
70
+ });
71
+ it('forDownload returns a presigned URL containing the workspace-scoped R2 key', async () => {
72
+ const relpath = 'exports/ab/cd1234ef/scene.scene';
73
+ const url = await builder.forDownload(relpath);
74
+ // The full key must appear as one ordered, contiguous path segment —
75
+ // workspaces/<workspaceId>/<relpath> — not just the fragments in any order.
76
+ // R2 keys aren't percent-encoded for these chars, so assert the exact path.
77
+ expect(url).toContain(`workspaces/${workspaceId}/${relpath}`);
78
+ });
79
+ it('forDownload URL contains X-Amz-Signature (proves it is presigned)', async () => {
80
+ const url = await builder.forDownload('exports/ab/cd/image.png');
81
+ expect(url).toContain('X-Amz-Signature');
82
+ });
83
+ it('inherits sync forResource from LocalViewerUrlBuilder unchanged', () => {
84
+ expect(builder.forResource('assets/9f2a1c.png')).toBe('http://127.0.0.1:8745/r/assets/9f2a1c.png');
85
+ });
86
+ it('forDownload with default expiresIn (3600) also signs', async () => {
87
+ const b = new PresignedR2UrlBuilder({
88
+ base: 'http://127.0.0.1:8745',
89
+ client,
90
+ bucket: 'codesign-bucket',
91
+ workspaceId: 'ws-default'
92
+ });
93
+ const url = await b.forDownload('exports/11/22/design.scene');
94
+ expect(url).toContain('X-Amz-Signature');
95
+ expect(url).toContain('ws-default');
29
96
  });
30
97
  });
31
98
  //# sourceMappingURL=url-builder.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"url-builder.test.js","sourceRoot":"","sources":["../../../../test/viewer/url-builder.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AAEzC,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,CAAC,GAAG,IAAI,qBAAqB,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CACtC,sCAAsC,CACvC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,GAAG,IAAI,qBAAqB,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,IAAI,CAC9D,mDAAmD,CACpD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,IAAI,qBAAqB,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,CAAC,GAAG,IAAI,qBAAqB,CAAC,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAC7C,2CAA2C,CAC5C,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,GAAG,IAAI,0BAA0B,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,GAAG,IAAI,0BAA0B,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,iBAAiB,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,OAAO,CACvE,SAAS,CACV,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from 'vitest';\nimport {\n HostedStubViewerUrlBuilder,\n LocalViewerUrlBuilder\n} from '../../src/viewer/url-builder.js';\n\ndescribe('LocalViewerUrlBuilder', () => {\n it('builds /d/<root> design URLs against the listener base', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forDesign('abc123def456')).toBe(\n 'http://127.0.0.1:8745/d/abc123def456'\n );\n });\n\n it('builds /d/<root>/<rev> pinned-revision URLs', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forPinnedRevision('abc123def456', '0011deadbeef')).toBe(\n 'http://127.0.0.1:8745/d/abc123def456/0011deadbeef'\n );\n });\n\n it('builds the studio landing URL', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forStudio()).toBe('http://127.0.0.1:8745/');\n });\n\n it('builds /r/<relpath> resource URLs against the listener base', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forResource('assets/9f2a1c.png')).toBe(\n 'http://127.0.0.1:8745/r/assets/9f2a1c.png'\n );\n });\n});\n\ndescribe('HostedStubViewerUrlBuilder', () => {\n it('forDesign throws Phase 2 not implemented', () => {\n const b = new HostedStubViewerUrlBuilder();\n expect(() => b.forDesign('abc123def456')).toThrow('Phase 2');\n });\n\n it('forPinnedRevision throws Phase 2 not implemented', () => {\n const b = new HostedStubViewerUrlBuilder();\n expect(() => b.forPinnedRevision('abc123def456', '0011deadbeef')).toThrow(\n 'Phase 2'\n );\n });\n});\n"]}
1
+ {"version":3,"file":"url-builder.test.js","sourceRoot":"","sources":["../../../../test/viewer/url-builder.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { S3Client } from '@aws-sdk/client-s3';\nimport { describe, expect, it } from 'vitest';\nimport {\n LocalViewerUrlBuilder,\n viewerBaseFor\n} from '../../src/viewer/url-builder.js';\nimport { PresignedR2UrlBuilder } from '../../src/viewer/presigned-r2-url-builder.js';\n\ndescribe('LocalViewerUrlBuilder', () => {\n it('builds /d/<root> design URLs against the listener base', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forDesign('abc123def456')).toBe(\n 'http://127.0.0.1:8745/d/abc123def456'\n );\n });\n\n it('builds /d/<root>/<rev> pinned-revision URLs', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forPinnedRevision('abc123def456', '0011deadbeef')).toBe(\n 'http://127.0.0.1:8745/d/abc123def456/0011deadbeef'\n );\n });\n\n it('builds the studio landing URL', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forStudio()).toBe('http://127.0.0.1:8745/');\n });\n\n it('builds /r/<relpath> resource URLs against the listener base', () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n expect(b.forResource('assets/9f2a1c.png')).toBe(\n 'http://127.0.0.1:8745/r/assets/9f2a1c.png'\n );\n });\n\n it('forDownload resolves to the same URL as forResource', async () => {\n const b = new LocalViewerUrlBuilder({ base: 'http://127.0.0.1:8745' });\n const relpath = 'exports/ab/cd1234/image.png';\n await expect(b.forDownload(relpath)).resolves.toBe(b.forResource(relpath));\n });\n});\n\ndescribe('viewerBaseFor', () => {\n it('session mode + public base /v/<sid> on the public origin', () => {\n expect(\n viewerBaseFor({\n publicBaseUrl: 'https://codesign.example.com',\n port: 8080,\n mode: 'session',\n sessionId: 'abc-123'\n })\n ).toBe('https://codesign.example.com/v/abc-123');\n });\n\n it('session mode without public base → loopback /v/<sid>', () => {\n expect(\n viewerBaseFor({\n publicBaseUrl: undefined,\n port: 8745,\n mode: 'session',\n sessionId: 'abc-123'\n })\n ).toBe('http://127.0.0.1:8745/v/abc-123');\n });\n\n it('single mode → /viewer (the fixed-workspace mount)', () => {\n expect(\n viewerBaseFor({\n publicBaseUrl: undefined,\n port: 8745,\n mode: 'single',\n sessionId: 'abc-123'\n })\n ).toBe('http://127.0.0.1:8745/viewer');\n });\n});\n\ndescribe('PresignedR2UrlBuilder', () => {\n // getSignedUrl from @aws-sdk/s3-request-presigner signs locally — no network\n // call is made. We use dummy credentials and a fake R2 endpoint.\n const client = new S3Client({\n region: 'auto',\n endpoint: 'https://accountid.r2.cloudflarestorage.com',\n forcePathStyle: true,\n credentials: { accessKeyId: 'test', secretAccessKey: 'test' }\n });\n const workspaceId = 'ws-test-001';\n const builder = new PresignedR2UrlBuilder({\n base: 'http://127.0.0.1:8745',\n client,\n bucket: 'codesign-bucket',\n workspaceId,\n expiresIn: 300\n });\n\n it('forDownload returns a presigned URL containing the workspace-scoped R2 key', async () => {\n const relpath = 'exports/ab/cd1234ef/scene.scene';\n const url = await builder.forDownload(relpath);\n // The full key must appear as one ordered, contiguous path segment —\n // workspaces/<workspaceId>/<relpath> — not just the fragments in any order.\n // R2 keys aren't percent-encoded for these chars, so assert the exact path.\n expect(url).toContain(`workspaces/${workspaceId}/${relpath}`);\n });\n\n it('forDownload URL contains X-Amz-Signature (proves it is presigned)', async () => {\n const url = await builder.forDownload('exports/ab/cd/image.png');\n expect(url).toContain('X-Amz-Signature');\n });\n\n it('inherits sync forResource from LocalViewerUrlBuilder unchanged', () => {\n expect(builder.forResource('assets/9f2a1c.png')).toBe(\n 'http://127.0.0.1:8745/r/assets/9f2a1c.png'\n );\n });\n\n it('forDownload with default expiresIn (3600) also signs', async () => {\n const b = new PresignedR2UrlBuilder({\n base: 'http://127.0.0.1:8745',\n client,\n bucket: 'codesign-bucket',\n workspaceId: 'ws-default'\n });\n const url = await b.forDownload('exports/11/22/design.scene');\n expect(url).toContain('X-Amz-Signature');\n expect(url).toContain('ws-default');\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=s3.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.test.d.ts","sourceRoot":"","sources":["../../../../test/workspace/s3.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from '@aws-sdk/client-s3';
3
+ import { S3WorkspaceFS } from '../../src/workspace/s3.js';
4
+ /** Minimal in-memory fake of the S3 `send`-based client the adapter uses. */
5
+ function makeFakeS3(seed = {}) {
6
+ const store = new Map();
7
+ for (const [k, v] of Object.entries(seed))
8
+ store.set(k, new TextEncoder().encode(v));
9
+ const notFound = () => {
10
+ const err = new Error('NoSuchKey');
11
+ err.name = 'NoSuchKey';
12
+ err.$metadata = { httpStatusCode: 404 };
13
+ throw err;
14
+ };
15
+ const client = {
16
+ store,
17
+ send: vi.fn(async (cmd) => {
18
+ if (cmd instanceof PutObjectCommand) {
19
+ const { Key, Body } = cmd.input;
20
+ const bytes = typeof Body === 'string'
21
+ ? new TextEncoder().encode(Body)
22
+ : Body;
23
+ store.set(Key, bytes);
24
+ return {};
25
+ }
26
+ if (cmd instanceof GetObjectCommand) {
27
+ const bytes = store.get(cmd.input.Key);
28
+ if (!bytes)
29
+ notFound();
30
+ return {
31
+ Body: {
32
+ transformToByteArray: async () => bytes
33
+ }
34
+ };
35
+ }
36
+ if (cmd instanceof HeadObjectCommand) {
37
+ const bytes = store.get(cmd.input.Key);
38
+ if (!bytes)
39
+ notFound();
40
+ return { ContentLength: bytes.byteLength, LastModified: new Date(0) };
41
+ }
42
+ if (cmd instanceof ListObjectsV2Command) {
43
+ const prefix = cmd.input.Prefix ?? '';
44
+ const delimiter = cmd.input.Delimiter;
45
+ const keys = [...store.keys()].filter((k) => k.startsWith(prefix));
46
+ const contents = [];
47
+ const commonPrefixes = new Set();
48
+ for (const k of keys) {
49
+ const rest = k.slice(prefix.length);
50
+ if (delimiter && rest.includes(delimiter)) {
51
+ commonPrefixes.add(prefix + rest.slice(0, rest.indexOf(delimiter) + 1));
52
+ }
53
+ else {
54
+ contents.push({ Key: k });
55
+ }
56
+ }
57
+ return {
58
+ Contents: contents,
59
+ CommonPrefixes: [...commonPrefixes].map((Prefix) => ({ Prefix })),
60
+ IsTruncated: false
61
+ };
62
+ }
63
+ if (cmd instanceof DeleteObjectCommand) {
64
+ store.delete(cmd.input.Key);
65
+ return {};
66
+ }
67
+ throw new Error(`fake S3: unhandled command ${cmd.constructor.name}`);
68
+ })
69
+ };
70
+ return client;
71
+ }
72
+ function makeFs(seed) {
73
+ const client = makeFakeS3(seed);
74
+ const fs = new S3WorkspaceFS({ client: client, bucket: 'b' });
75
+ return { fs, client };
76
+ }
77
+ describe('S3WorkspaceFS', () => {
78
+ it('writeFile then readFile round-trips utf8', async () => {
79
+ const { fs } = makeFs();
80
+ await fs.writeFile('objects/ab/cd.scene', 'hello');
81
+ expect(await fs.readFile('objects/ab/cd.scene', 'utf8')).toBe('hello');
82
+ });
83
+ it('readFile throws ENOENT for a missing key', async () => {
84
+ const { fs } = makeFs();
85
+ await expect(fs.readFile('missing', 'utf8')).rejects.toMatchObject({
86
+ code: 'ENOENT'
87
+ });
88
+ });
89
+ it('exists is true for a written key and false otherwise', async () => {
90
+ const { fs } = makeFs({ 'a/b': 'x' });
91
+ expect(await fs.exists('a/b')).toBe(true);
92
+ expect(await fs.exists('a/z')).toBe(false);
93
+ });
94
+ it('exists is true for a prefix that has children (a "directory")', async () => {
95
+ const { fs } = makeFs({ 'journal/root1/0001-leaf': '' });
96
+ expect(await fs.exists('journal')).toBe(true);
97
+ });
98
+ it('readdir returns immediate child names (dirs + files)', async () => {
99
+ const { fs } = makeFs({
100
+ 'journal/root1/0001-a': '',
101
+ 'journal/root2/0002-b': ''
102
+ });
103
+ expect((await fs.readdir('journal')).sort()).toEqual(['root1', 'root2']);
104
+ expect(await fs.readdir('journal/root1')).toEqual(['0001-a']);
105
+ });
106
+ it('rename copies bytes to the new key and removes the old', async () => {
107
+ const { fs } = makeFs({ 'a/old': 'payload' });
108
+ await fs.rename('a/old', 'a/new');
109
+ expect(await fs.exists('a/old')).toBe(false);
110
+ expect(await fs.readFile('a/new', 'utf8')).toBe('payload');
111
+ });
112
+ it('stat reports a file for an object and a directory for a prefix', async () => {
113
+ const { fs } = makeFs({ 'd/child': 'x' });
114
+ const fileStat = await fs.stat('d/child');
115
+ expect(fileStat.isFile()).toBe(true);
116
+ expect(fileStat.isDirectory()).toBe(false);
117
+ const dirStat = await fs.stat('d');
118
+ expect(dirStat.isDirectory()).toBe(true);
119
+ expect(dirStat.isFile()).toBe(false);
120
+ });
121
+ it('rm recursive deletes the prefix subtree AND the key itself', async () => {
122
+ const { fs } = makeFs({
123
+ p: 'self',
124
+ 'p/a': '1',
125
+ 'p/b/c': '2'
126
+ });
127
+ await fs.rm('p', { recursive: true });
128
+ expect(await fs.exists('p')).toBe(false);
129
+ expect(await fs.exists('p/a')).toBe(false);
130
+ expect(await fs.exists('p/b/c')).toBe(false);
131
+ });
132
+ it('retries a read-after-write 404 then succeeds (bounded)', async () => {
133
+ const { fs, client } = makeFs();
134
+ let gets = 0;
135
+ const realSend = client.send;
136
+ client.send = vi.fn(async (cmd) => {
137
+ if (cmd instanceof GetObjectCommand) {
138
+ gets += 1;
139
+ if (gets === 1) {
140
+ const err = new Error('NoSuchKey');
141
+ err.name = 'NoSuchKey';
142
+ err.$metadata = { httpStatusCode: 404 };
143
+ throw err;
144
+ }
145
+ }
146
+ return realSend(cmd);
147
+ });
148
+ client.store.set('k', new TextEncoder().encode('v'));
149
+ expect(await fs.readFile('k', 'utf8')).toBe('v');
150
+ expect(gets).toBe(2);
151
+ });
152
+ });
153
+ //# sourceMappingURL=s3.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3.test.js","sourceRoot":"","sources":["../../../../test/workspace/s3.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it, vi } from 'vitest';\nimport {\n DeleteObjectCommand,\n GetObjectCommand,\n HeadObjectCommand,\n ListObjectsV2Command,\n PutObjectCommand\n} from '@aws-sdk/client-s3';\nimport { S3WorkspaceFS } from '../../src/workspace/s3.js';\n\n/** Minimal in-memory fake of the S3 `send`-based client the adapter uses. */\nfunction makeFakeS3(seed: Record<string, string> = {}) {\n const store = new Map<string, Uint8Array>();\n for (const [k, v] of Object.entries(seed))\n store.set(k, new TextEncoder().encode(v));\n\n const notFound = (): never => {\n const err = new Error('NoSuchKey') as Error & {\n name: string;\n $metadata: { httpStatusCode: number };\n };\n err.name = 'NoSuchKey';\n err.$metadata = { httpStatusCode: 404 };\n throw err;\n };\n\n const client = {\n store,\n send: vi.fn(async (cmd: unknown) => {\n if (cmd instanceof PutObjectCommand) {\n const { Key, Body } = cmd.input;\n const bytes =\n typeof Body === 'string'\n ? new TextEncoder().encode(Body)\n : (Body as Uint8Array);\n store.set(Key as string, bytes);\n return {};\n }\n if (cmd instanceof GetObjectCommand) {\n const bytes = store.get(cmd.input.Key as string);\n if (!bytes) notFound();\n return {\n Body: {\n transformToByteArray: async () => bytes\n }\n };\n }\n if (cmd instanceof HeadObjectCommand) {\n const bytes = store.get(cmd.input.Key as string);\n if (!bytes) notFound();\n return { ContentLength: bytes!.byteLength, LastModified: new Date(0) };\n }\n if (cmd instanceof ListObjectsV2Command) {\n const prefix = (cmd.input.Prefix as string) ?? '';\n const delimiter = cmd.input.Delimiter as string | undefined;\n const keys = [...store.keys()].filter((k) => k.startsWith(prefix));\n const contents: Array<{ Key: string }> = [];\n const commonPrefixes = new Set<string>();\n for (const k of keys) {\n const rest = k.slice(prefix.length);\n if (delimiter && rest.includes(delimiter)) {\n commonPrefixes.add(\n prefix + rest.slice(0, rest.indexOf(delimiter) + 1)\n );\n } else {\n contents.push({ Key: k });\n }\n }\n return {\n Contents: contents,\n CommonPrefixes: [...commonPrefixes].map((Prefix) => ({ Prefix })),\n IsTruncated: false\n };\n }\n if (cmd instanceof DeleteObjectCommand) {\n store.delete(cmd.input.Key as string);\n return {};\n }\n throw new Error(\n `fake S3: unhandled command ${(cmd as object).constructor.name}`\n );\n })\n };\n return client;\n}\n\nfunction makeFs(seed?: Record<string, string>) {\n const client = makeFakeS3(seed);\n const fs = new S3WorkspaceFS({ client: client as never, bucket: 'b' });\n return { fs, client };\n}\n\ndescribe('S3WorkspaceFS', () => {\n it('writeFile then readFile round-trips utf8', async () => {\n const { fs } = makeFs();\n await fs.writeFile('objects/ab/cd.scene', 'hello');\n expect(await fs.readFile('objects/ab/cd.scene', 'utf8')).toBe('hello');\n });\n\n it('readFile throws ENOENT for a missing key', async () => {\n const { fs } = makeFs();\n await expect(fs.readFile('missing', 'utf8')).rejects.toMatchObject({\n code: 'ENOENT'\n });\n });\n\n it('exists is true for a written key and false otherwise', async () => {\n const { fs } = makeFs({ 'a/b': 'x' });\n expect(await fs.exists('a/b')).toBe(true);\n expect(await fs.exists('a/z')).toBe(false);\n });\n\n it('exists is true for a prefix that has children (a \"directory\")', async () => {\n const { fs } = makeFs({ 'journal/root1/0001-leaf': '' });\n expect(await fs.exists('journal')).toBe(true);\n });\n\n it('readdir returns immediate child names (dirs + files)', async () => {\n const { fs } = makeFs({\n 'journal/root1/0001-a': '',\n 'journal/root2/0002-b': ''\n });\n expect((await fs.readdir('journal')).sort()).toEqual(['root1', 'root2']);\n expect(await fs.readdir('journal/root1')).toEqual(['0001-a']);\n });\n\n it('rename copies bytes to the new key and removes the old', async () => {\n const { fs } = makeFs({ 'a/old': 'payload' });\n await fs.rename('a/old', 'a/new');\n expect(await fs.exists('a/old')).toBe(false);\n expect(await fs.readFile('a/new', 'utf8')).toBe('payload');\n });\n\n it('stat reports a file for an object and a directory for a prefix', async () => {\n const { fs } = makeFs({ 'd/child': 'x' });\n const fileStat = await fs.stat('d/child');\n expect(fileStat.isFile()).toBe(true);\n expect(fileStat.isDirectory()).toBe(false);\n const dirStat = await fs.stat('d');\n expect(dirStat.isDirectory()).toBe(true);\n expect(dirStat.isFile()).toBe(false);\n });\n\n it('rm recursive deletes the prefix subtree AND the key itself', async () => {\n const { fs } = makeFs({\n p: 'self',\n 'p/a': '1',\n 'p/b/c': '2'\n });\n await fs.rm('p', { recursive: true });\n expect(await fs.exists('p')).toBe(false);\n expect(await fs.exists('p/a')).toBe(false);\n expect(await fs.exists('p/b/c')).toBe(false);\n });\n\n it('retries a read-after-write 404 then succeeds (bounded)', async () => {\n const { fs, client } = makeFs();\n let gets = 0;\n const realSend = client.send;\n client.send = vi.fn(async (cmd: unknown) => {\n if (cmd instanceof GetObjectCommand) {\n gets += 1;\n if (gets === 1) {\n const err = new Error('NoSuchKey') as Error & {\n name: string;\n $metadata: { httpStatusCode: number };\n };\n err.name = 'NoSuchKey';\n err.$metadata = { httpStatusCode: 404 };\n throw err;\n }\n }\n return realSend(cmd);\n });\n client.store.set('k', new TextEncoder().encode('v'));\n expect(await fs.readFile('k', 'utf8')).toBe('v');\n expect(gets).toBe(2);\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scoped.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scoped.test.d.ts","sourceRoot":"","sources":["../../../../test/workspace/scoped.test.ts"],"names":[],"mappings":""}