@deskwork/studio 0.9.5

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 (93) hide show
  1. package/dist/build-client-assets.d.ts +51 -0
  2. package/dist/build-client-assets.d.ts.map +1 -0
  3. package/dist/build-client-assets.js +341 -0
  4. package/dist/build-client-assets.js.map +1 -0
  5. package/dist/components/scrapbook-item.d.ts +108 -0
  6. package/dist/components/scrapbook-item.d.ts.map +1 -0
  7. package/dist/components/scrapbook-item.js +205 -0
  8. package/dist/components/scrapbook-item.js.map +1 -0
  9. package/dist/lib/editorial-skills-catalogue.d.ts +33 -0
  10. package/dist/lib/editorial-skills-catalogue.d.ts.map +1 -0
  11. package/dist/lib/editorial-skills-catalogue.js +211 -0
  12. package/dist/lib/editorial-skills-catalogue.js.map +1 -0
  13. package/dist/lib/override-render.d.ts +41 -0
  14. package/dist/lib/override-render.d.ts.map +1 -0
  15. package/dist/lib/override-render.js +80 -0
  16. package/dist/lib/override-render.js.map +1 -0
  17. package/dist/listen.d.ts +78 -0
  18. package/dist/listen.d.ts.map +1 -0
  19. package/dist/listen.js +155 -0
  20. package/dist/listen.js.map +1 -0
  21. package/dist/pages/chrome.d.ts +26 -0
  22. package/dist/pages/chrome.d.ts.map +1 -0
  23. package/dist/pages/chrome.js +50 -0
  24. package/dist/pages/chrome.js.map +1 -0
  25. package/dist/pages/content-detail.d.ts +14 -0
  26. package/dist/pages/content-detail.d.ts.map +1 -0
  27. package/dist/pages/content-detail.js +279 -0
  28. package/dist/pages/content-detail.js.map +1 -0
  29. package/dist/pages/content.d.ts +39 -0
  30. package/dist/pages/content.d.ts.map +1 -0
  31. package/dist/pages/content.js +414 -0
  32. package/dist/pages/content.js.map +1 -0
  33. package/dist/pages/dashboard.d.ts +32 -0
  34. package/dist/pages/dashboard.d.ts.map +1 -0
  35. package/dist/pages/dashboard.js +803 -0
  36. package/dist/pages/dashboard.js.map +1 -0
  37. package/dist/pages/help.d.ts +24 -0
  38. package/dist/pages/help.d.ts.map +1 -0
  39. package/dist/pages/help.js +433 -0
  40. package/dist/pages/help.js.map +1 -0
  41. package/dist/pages/html.d.ts +35 -0
  42. package/dist/pages/html.d.ts.map +1 -0
  43. package/dist/pages/html.js +73 -0
  44. package/dist/pages/html.js.map +1 -0
  45. package/dist/pages/index.d.ts +21 -0
  46. package/dist/pages/index.d.ts.map +1 -0
  47. package/dist/pages/index.js +174 -0
  48. package/dist/pages/index.js.map +1 -0
  49. package/dist/pages/layout.d.ts +33 -0
  50. package/dist/pages/layout.d.ts.map +1 -0
  51. package/dist/pages/layout.js +50 -0
  52. package/dist/pages/layout.js.map +1 -0
  53. package/dist/pages/review-scrapbook-drawer.d.ts +20 -0
  54. package/dist/pages/review-scrapbook-drawer.d.ts.map +1 -0
  55. package/dist/pages/review-scrapbook-drawer.js +98 -0
  56. package/dist/pages/review-scrapbook-drawer.js.map +1 -0
  57. package/dist/pages/review.d.ts +68 -0
  58. package/dist/pages/review.d.ts.map +1 -0
  59. package/dist/pages/review.js +434 -0
  60. package/dist/pages/review.js.map +1 -0
  61. package/dist/pages/scrapbook.d.ts +21 -0
  62. package/dist/pages/scrapbook.d.ts.map +1 -0
  63. package/dist/pages/scrapbook.js +250 -0
  64. package/dist/pages/scrapbook.js.map +1 -0
  65. package/dist/pages/shortform.d.ts +17 -0
  66. package/dist/pages/shortform.d.ts.map +1 -0
  67. package/dist/pages/shortform.js +142 -0
  68. package/dist/pages/shortform.js.map +1 -0
  69. package/dist/request-context.d.ts +52 -0
  70. package/dist/request-context.d.ts.map +1 -0
  71. package/dist/request-context.js +84 -0
  72. package/dist/request-context.js.map +1 -0
  73. package/dist/routes/api.d.ts +36 -0
  74. package/dist/routes/api.d.ts.map +1 -0
  75. package/dist/routes/api.js +175 -0
  76. package/dist/routes/api.js.map +1 -0
  77. package/dist/routes/scrapbook-file.d.ts +19 -0
  78. package/dist/routes/scrapbook-file.d.ts.map +1 -0
  79. package/dist/routes/scrapbook-file.js +77 -0
  80. package/dist/routes/scrapbook-file.js.map +1 -0
  81. package/dist/routes/scrapbook-mutations.d.ts +33 -0
  82. package/dist/routes/scrapbook-mutations.d.ts.map +1 -0
  83. package/dist/routes/scrapbook-mutations.js +310 -0
  84. package/dist/routes/scrapbook-mutations.js.map +1 -0
  85. package/dist/server.d.ts +52 -0
  86. package/dist/server.d.ts.map +1 -0
  87. package/dist/server.js +581 -0
  88. package/dist/server.js.map +1 -0
  89. package/dist/tailscale.d.ts +63 -0
  90. package/dist/tailscale.d.ts.map +1 -0
  91. package/dist/tailscale.js +118 -0
  92. package/dist/tailscale.js.map +1 -0
  93. package/package.json +60 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * API route handlers for the studio. Each route is a thin wrapper over a
3
+ * `lib/review/handlers.ts` function — it parses the HTTP request, calls
4
+ * the handler with `(projectRoot, config, body)`, and serializes the
5
+ * `{status, body}` result as JSON.
6
+ *
7
+ * Mirrors the audiocontrol Astro routes byte-for-byte so the existing
8
+ * client code (editorial-review-client.ts, editorial-studio-client.ts)
9
+ * keeps working without changes.
10
+ */
11
+ import { Hono } from 'hono';
12
+ import { handleAnnotate, handleListAnnotations, handleDecision, handleGetWorkflow, handleCreateVersion, handleStartLongform, } from '@deskwork/core/review/handlers';
13
+ import { handleStartShortform } from '@deskwork/core/review/start-handlers';
14
+ import { renderMarkdownToHtml } from '@deskwork/core/review/render';
15
+ /**
16
+ * Narrow a `HandlerResult.body` (typed as `unknown`) to extract the
17
+ * workflow id. Returns null when the shape doesn't match — phase 21c's
18
+ * start-shortform route uses this to decide whether to augment the
19
+ * response with a reviewUrl. Avoids `as`-casts on the body.
20
+ */
21
+ function extractWorkflowId(body) {
22
+ if (typeof body !== 'object' || body === null)
23
+ return null;
24
+ const workflow = Reflect.get(body, 'workflow');
25
+ if (typeof workflow !== 'object' || workflow === null)
26
+ return null;
27
+ const id = Reflect.get(workflow, 'id');
28
+ return typeof id === 'string' && id.length > 0 ? id : null;
29
+ }
30
+ /**
31
+ * Build a fresh response object that preserves every field from the
32
+ * original handler body and adds `reviewUrl`. Iterates own keys via
33
+ * `Object.entries` so the result is a plain `Record<string, unknown>`
34
+ * rather than carrying the input's unknown-typed shape.
35
+ */
36
+ function withReviewUrl(body, workflowId) {
37
+ const out = {};
38
+ if (typeof body === 'object' && body !== null) {
39
+ for (const [k, v] of Object.entries(body)) {
40
+ out[k] = v;
41
+ }
42
+ }
43
+ out.reviewUrl = `/dev/editorial-review/${workflowId}`;
44
+ return out;
45
+ }
46
+ export function createApiRouter(ctx) {
47
+ const app = new Hono();
48
+ // POST /api/dev/editorial-review/annotate
49
+ app.post('/annotate', async (c) => {
50
+ let body;
51
+ try {
52
+ body = await c.req.json();
53
+ }
54
+ catch {
55
+ return c.json({ error: 'invalid JSON body' }, 400);
56
+ }
57
+ const r = handleAnnotate(ctx.projectRoot, ctx.config, body);
58
+ return c.json(r.body, r.status);
59
+ });
60
+ // GET /api/dev/editorial-review/annotations?workflowId=...&version=...
61
+ app.get('/annotations', (c) => {
62
+ const r = handleListAnnotations(ctx.projectRoot, ctx.config, {
63
+ workflowId: c.req.query('workflowId') ?? null,
64
+ version: c.req.query('version') ?? null,
65
+ });
66
+ return c.json(r.body, r.status);
67
+ });
68
+ // POST /api/dev/editorial-review/decision
69
+ app.post('/decision', async (c) => {
70
+ let body;
71
+ try {
72
+ body = await c.req.json();
73
+ }
74
+ catch {
75
+ return c.json({ error: 'invalid JSON body' }, 400);
76
+ }
77
+ const r = handleDecision(ctx.projectRoot, ctx.config, body);
78
+ return c.json(r.body, r.status);
79
+ });
80
+ // GET /api/dev/editorial-review/workflow?id=... or ?site=...&slug=...&...
81
+ app.get('/workflow', (c) => {
82
+ const r = handleGetWorkflow(ctx.projectRoot, ctx.config, {
83
+ id: c.req.query('id') ?? null,
84
+ site: c.req.query('site') ?? null,
85
+ slug: c.req.query('slug') ?? null,
86
+ contentKind: c.req.query('contentKind') ?? null,
87
+ platform: c.req.query('platform') ?? null,
88
+ channel: c.req.query('channel') ?? null,
89
+ });
90
+ return c.json(r.body, r.status);
91
+ });
92
+ // POST /api/dev/editorial-review/version
93
+ app.post('/version', async (c) => {
94
+ let body;
95
+ try {
96
+ body = await c.req.json();
97
+ }
98
+ catch {
99
+ return c.json({ error: 'invalid JSON body' }, 400);
100
+ }
101
+ const r = handleCreateVersion(ctx.projectRoot, ctx.config, body);
102
+ return c.json(r.body, r.status);
103
+ });
104
+ // POST /api/dev/editorial-review/start-longform
105
+ app.post('/start-longform', async (c) => {
106
+ let body;
107
+ try {
108
+ body = await c.req.json();
109
+ }
110
+ catch {
111
+ return c.json({ error: 'invalid JSON body' }, 400);
112
+ }
113
+ const r = handleStartLongform(ctx.projectRoot, ctx.config, body);
114
+ return c.json(r.body, r.status);
115
+ });
116
+ // POST /api/dev/editorial-review/start-shortform
117
+ //
118
+ // Phase 21c: Mirrors start-longform but for shortform workflows.
119
+ // Augments the handler's success body with a `reviewUrl` so the
120
+ // dashboard's matrix-cell start button can fetch + redirect in one
121
+ // round-trip. The handler scaffolds the disk file (frontmatter +
122
+ // initial body) when missing and is idempotent on
123
+ // (entryId|site+slug, contentKind, platform, channel).
124
+ app.post('/start-shortform', async (c) => {
125
+ let body;
126
+ try {
127
+ body = await c.req.json();
128
+ }
129
+ catch {
130
+ return c.json({ error: 'invalid JSON body' }, 400);
131
+ }
132
+ const r = handleStartShortform(ctx.projectRoot, ctx.config, body);
133
+ const workflowId = extractWorkflowId(r.body);
134
+ if (r.status === 200 && workflowId !== null) {
135
+ const augmented = withReviewUrl(r.body, workflowId);
136
+ return c.json(augmented, 200);
137
+ }
138
+ return c.json(r.body, r.status);
139
+ });
140
+ // POST /api/dev/editorial-review/render
141
+ //
142
+ // Replaces the upstream client-side dynamic import of
143
+ // `scripts/lib/editorial-review/render.js`. The preview pane in
144
+ // `editorial-review-client.ts` POSTs the markdown source here and
145
+ // gets HTML back. Centralising the render keeps the unified pipeline
146
+ // (remark-parse + remark-strip-first-h1 + remark-image-figure +
147
+ // remark-rehype + rehype-stringify) on the server, where it already
148
+ // runs for the initial review-page render — one source of truth.
149
+ app.post('/render', async (c) => {
150
+ let body;
151
+ try {
152
+ body = await c.req.json();
153
+ }
154
+ catch {
155
+ return c.json({ error: 'invalid JSON body' }, 400);
156
+ }
157
+ if (!body || typeof body !== 'object') {
158
+ return c.json({ error: 'expected JSON object body' }, 400);
159
+ }
160
+ const markdown = body.markdown;
161
+ if (typeof markdown !== 'string') {
162
+ return c.json({ error: 'markdown (string) is required' }, 400);
163
+ }
164
+ try {
165
+ const html = await renderMarkdownToHtml(markdown);
166
+ return c.json({ html });
167
+ }
168
+ catch (err) {
169
+ const reason = err instanceof Error ? err.message : String(err);
170
+ return c.json({ error: `render failed: ${reason}` }, 500);
171
+ }
172
+ });
173
+ return app;
174
+ }
175
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/routes/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAIpE;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,IAAa;IACtC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC/C,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACvC,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,IAAa,EAAE,UAAkB;IACtD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAC9C,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACb,CAAC;IACH,CAAC;IACD,GAAG,CAAC,SAAS,GAAG,yBAAyB,UAAU,EAAE,CAAC;IACtD,OAAO,GAAG,CAAC;AACb,CAAC;AAwBD,MAAM,UAAU,eAAe,CAAC,GAAkB;IAChD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,0CAA0C;IAC1C,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAChC,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5D,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,uEAAuE;IACvE,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE;QAC5B,MAAM,CAAC,GAAG,qBAAqB,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE;YAC3D,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,IAAI;YAC7C,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI;SACxC,CAAC,CAAC;QACH,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,0CAA0C;IAC1C,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAChC,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,CAAC,GAAG,cAAc,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5D,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,0EAA0E;IAC1E,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE;QACzB,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE;YACvD,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI;YAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI;YACjC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,IAAI;YACjC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,IAAI;YAC/C,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI;YACzC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,IAAI;SACxC,CAAC,CAAC;QACH,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,yCAAyC;IACzC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC/B,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,gDAAgD;IAChD,GAAG,CAAC,IAAI,CAAC,iBAAiB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACtC,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,CAAC,GAAG,mBAAmB,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,iDAAiD;IACjD,EAAE;IACF,iEAAiE;IACjE,gEAAgE;IAChE,mEAAmE;IACnE,iEAAiE;IACjE,kDAAkD;IAClD,uDAAuD;IACvD,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACvC,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,MAAM,CAAC,GAAG,oBAAoB,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAClE,MAAM,UAAU,GAAG,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YAC5C,MAAM,SAAS,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YACpD,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAChC,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAe,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,wCAAwC;IACxC,EAAE;IACF,sDAAsD;IACtD,gEAAgE;IAChE,kEAAkE;IAClE,qEAAqE;IACrE,gEAAgE;IAChE,oEAAoE;IACpE,iEAAiE;IACjE,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC9B,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC;QACD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YACtC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,EAAE,GAAG,CAAC,CAAC;QAC7D,CAAC;QACD,MAAM,QAAQ,GAAI,IAA+B,CAAC,QAAQ,CAAC;QAC3D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+BAA+B,EAAE,EAAE,GAAG,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,oBAAoB,CAAC,QAAQ,CAAC,CAAC;YAClD,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kBAAkB,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Read-only binary endpoint for scrapbook files.
3
+ *
4
+ * `GET /api/dev/scrapbook-file?site=<slug>&path=<scrapbook path>&name=<filename>[&secret=1]`
5
+ *
6
+ * Returns the raw bytes of a single scrapbook file with a sensible
7
+ * Content-Type header. Read-only — no write/rename/delete here. The
8
+ * shared scrapbook-item renderer uses this for image thumbnails,
9
+ * PDF iframes, and download links on the review-drawer + content-view
10
+ * surfaces.
11
+ *
12
+ * Validation runs through `@deskwork/core/scrapbook`'s own
13
+ * `assertSlug` / `assertFilename` (via `readScrapbookFile`), so path
14
+ * traversal attempts are caught at the core boundary, not here.
15
+ */
16
+ import type { Context } from 'hono';
17
+ import type { StudioContext } from './api.ts';
18
+ export declare function serveScrapbookFile(c: Context, ctx: StudioContext): Promise<Response>;
19
+ //# sourceMappingURL=scrapbook-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-file.d.ts","sourceRoot":"","sources":["../../src/routes/scrapbook-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAwB9C,wBAAsB,kBAAkB,CACtC,CAAC,EAAE,OAAO,EACV,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,QAAQ,CAAC,CA2CnB"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Read-only binary endpoint for scrapbook files.
3
+ *
4
+ * `GET /api/dev/scrapbook-file?site=<slug>&path=<scrapbook path>&name=<filename>[&secret=1]`
5
+ *
6
+ * Returns the raw bytes of a single scrapbook file with a sensible
7
+ * Content-Type header. Read-only — no write/rename/delete here. The
8
+ * shared scrapbook-item renderer uses this for image thumbnails,
9
+ * PDF iframes, and download links on the review-drawer + content-view
10
+ * surfaces.
11
+ *
12
+ * Validation runs through `@deskwork/core/scrapbook`'s own
13
+ * `assertSlug` / `assertFilename` (via `readScrapbookFile`), so path
14
+ * traversal attempts are caught at the core boundary, not here.
15
+ */
16
+ import { extname } from 'node:path';
17
+ import { readScrapbookFile } from '@deskwork/core/scrapbook';
18
+ const MIME_TYPES = {
19
+ '.png': 'image/png',
20
+ '.jpg': 'image/jpeg',
21
+ '.jpeg': 'image/jpeg',
22
+ '.gif': 'image/gif',
23
+ '.webp': 'image/webp',
24
+ '.svg': 'image/svg+xml',
25
+ '.pdf': 'application/pdf',
26
+ '.json': 'application/json; charset=utf-8',
27
+ '.jsonl': 'application/jsonl; charset=utf-8',
28
+ '.txt': 'text/plain; charset=utf-8',
29
+ '.log': 'text/plain; charset=utf-8',
30
+ '.md': 'text/markdown; charset=utf-8',
31
+ '.markdown': 'text/markdown; charset=utf-8',
32
+ '.mdx': 'text/markdown; charset=utf-8',
33
+ };
34
+ function contentTypeFor(filename) {
35
+ const ext = extname(filename).toLowerCase();
36
+ return MIME_TYPES[ext] ?? 'application/octet-stream';
37
+ }
38
+ export async function serveScrapbookFile(c, ctx) {
39
+ const site = c.req.query('site');
40
+ const path = c.req.query('path');
41
+ const name = c.req.query('name');
42
+ const secret = c.req.query('secret') === '1';
43
+ if (!site || !path || !name) {
44
+ return c.json({ error: 'site, path, and name query params are required' }, 400);
45
+ }
46
+ if (!(site in ctx.config.sites)) {
47
+ return c.json({ error: `unknown site: ${site}` }, 404);
48
+ }
49
+ let result;
50
+ try {
51
+ result = readScrapbookFile(ctx.projectRoot, ctx.config, site, path, name, {
52
+ secret,
53
+ });
54
+ }
55
+ catch (err) {
56
+ const reason = err instanceof Error ? err.message : String(err);
57
+ // The core helper throws on invalid slug / invalid filename / file
58
+ // not found / path traversal. Treat all of them as 404 from the
59
+ // operator's perspective — the read-only endpoint shouldn't
60
+ // distinguish "doesn't exist" from "off-limits".
61
+ return c.json({ error: reason }, 404);
62
+ }
63
+ // Hono's c.body() expects a BodyInit. Node Buffers are typed as
64
+ // `Uint8Array<ArrayBufferLike>` but Hono's overload demands
65
+ // `Uint8Array<ArrayBuffer>` — copy into a fresh buffer to satisfy
66
+ // the type without bypassing typing. Scrapbook files are small
67
+ // (the operator put them there by hand) so the copy cost is fine.
68
+ const src = result.content;
69
+ const copy = new Uint8Array(src.byteLength);
70
+ copy.set(src);
71
+ return c.body(copy, 200, {
72
+ 'Content-Type': contentTypeFor(result.name),
73
+ 'Content-Length': String(result.size),
74
+ 'Cache-Control': 'private, max-age=10',
75
+ });
76
+ }
77
+ //# sourceMappingURL=scrapbook-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-file.js","sourceRoot":"","sources":["../../src/routes/scrapbook-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Mutation endpoints for the standalone scrapbook viewer
3
+ * (`/dev/scrapbook/<site>/<path>`). Five POST endpoints — save, rename,
4
+ * delete, create, upload — that the client at
5
+ * `plugins/deskwork-studio/public/src/scrapbook-client.ts` calls when
6
+ * the operator edits a slip, renames it, deletes it, drafts a new note,
7
+ * or drops a file onto the page.
8
+ *
9
+ * Phase 13 ported the client chrome but never landed these routes;
10
+ * Phase 16d retrofitted only the read path. v0.4.1 closes the gap
11
+ * (issue #21).
12
+ *
13
+ * Design notes:
14
+ * - Path resolution + traversal protection runs through
15
+ * `@deskwork/core/scrapbook` helpers (`scrapbookFilePath` etc.).
16
+ * Those throw on `..` sequences, absolute paths, and any filename
17
+ * that escapes the scrapbook dir; we surface those as 400.
18
+ * - The client speaks `{ site, slug, filename, body }` for save/create,
19
+ * `{ site, slug, oldName, newName }` for rename, `{ site, slug,
20
+ * filename }` for delete, multipart `{ site, slug, file }` for
21
+ * upload. Endpoints accept those exact field names.
22
+ * - We never log file contents on save / upload (privacy). Logging
23
+ * filename + slug is fine but kept silent here — the studio's
24
+ * console is the operator's, not a server log.
25
+ * - Errors from the core helpers carry text like `"file not found:
26
+ * <name>"`, `"file already exists: <name>"`, or `"resolved path
27
+ * escapes scrapbook dir"`. We map those onto the right HTTP code
28
+ * so the client UI can flash a useful message.
29
+ */
30
+ import { Hono } from 'hono';
31
+ import type { StudioContext } from './api.ts';
32
+ export declare function createScrapbookMutationsRouter(ctx: StudioContext): Hono;
33
+ //# sourceMappingURL=scrapbook-mutations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-mutations.d.ts","sourceRoot":"","sources":["../../src/routes/scrapbook-mutations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAc5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AA0F9C,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAqSvE"}
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Mutation endpoints for the standalone scrapbook viewer
3
+ * (`/dev/scrapbook/<site>/<path>`). Five POST endpoints — save, rename,
4
+ * delete, create, upload — that the client at
5
+ * `plugins/deskwork-studio/public/src/scrapbook-client.ts` calls when
6
+ * the operator edits a slip, renames it, deletes it, drafts a new note,
7
+ * or drops a file onto the page.
8
+ *
9
+ * Phase 13 ported the client chrome but never landed these routes;
10
+ * Phase 16d retrofitted only the read path. v0.4.1 closes the gap
11
+ * (issue #21).
12
+ *
13
+ * Design notes:
14
+ * - Path resolution + traversal protection runs through
15
+ * `@deskwork/core/scrapbook` helpers (`scrapbookFilePath` etc.).
16
+ * Those throw on `..` sequences, absolute paths, and any filename
17
+ * that escapes the scrapbook dir; we surface those as 400.
18
+ * - The client speaks `{ site, slug, filename, body }` for save/create,
19
+ * `{ site, slug, oldName, newName }` for rename, `{ site, slug,
20
+ * filename }` for delete, multipart `{ site, slug, file }` for
21
+ * upload. Endpoints accept those exact field names.
22
+ * - We never log file contents on save / upload (privacy). Logging
23
+ * filename + slug is fine but kept silent here — the studio's
24
+ * console is the operator's, not a server log.
25
+ * - Errors from the core helpers carry text like `"file not found:
26
+ * <name>"`, `"file already exists: <name>"`, or `"resolved path
27
+ * escapes scrapbook dir"`. We map those onto the right HTTP code
28
+ * so the client UI can flash a useful message.
29
+ */
30
+ import { Hono } from 'hono';
31
+ import { classify, createScrapbookMarkdown, deleteScrapbookFile, renameScrapbookFile, saveScrapbookFile, writeScrapbookUpload, } from '@deskwork/core/scrapbook';
32
+ import { existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
33
+ import { dirname } from 'node:path';
34
+ import { scrapbookFilePath } from '@deskwork/core/scrapbook';
35
+ /**
36
+ * Validate the common `{ site, slug, secret? }` envelope. Returns a
37
+ * typed object or a 400/404 Response that the caller propagates
38
+ * directly. The site existence check is here so every mutation 404s
39
+ * on unknown sites the same way the read endpoint does.
40
+ */
41
+ function checkEnvelope(ctx, body) {
42
+ const site = body.site;
43
+ const slug = body.slug;
44
+ if (typeof site !== 'string' || site.length === 0) {
45
+ return { error: 'site is required', status: 400 };
46
+ }
47
+ if (typeof slug !== 'string' || slug.length === 0) {
48
+ return { error: 'slug is required', status: 400 };
49
+ }
50
+ if (!(site in ctx.config.sites)) {
51
+ return { error: `unknown site: ${site}`, status: 404 };
52
+ }
53
+ const secretRaw = body.secret;
54
+ if (secretRaw !== undefined && typeof secretRaw !== 'boolean') {
55
+ return { error: 'secret must be a boolean when provided', status: 400 };
56
+ }
57
+ return { site, slug, secret: secretRaw === true };
58
+ }
59
+ /**
60
+ * Map a core-helper error message onto the right HTTP status. The core
61
+ * helpers throw plain `Error` with descriptive text; we keep the text
62
+ * in the response body so the client can surface it.
63
+ */
64
+ function statusForError(message) {
65
+ if (/already exists/i.test(message))
66
+ return 409;
67
+ if (/not found/i.test(message))
68
+ return 404;
69
+ // Slug, filename, traversal, and missing-arg errors are all 400.
70
+ return 400;
71
+ }
72
+ function isJsonObject(v) {
73
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
74
+ }
75
+ async function readJson(c) {
76
+ let raw;
77
+ try {
78
+ raw = await c.req.json();
79
+ }
80
+ catch {
81
+ return { ok: false, status: 400, error: 'invalid JSON body' };
82
+ }
83
+ if (!isJsonObject(raw)) {
84
+ return { ok: false, status: 400, error: 'body must be a JSON object' };
85
+ }
86
+ return { ok: true, value: raw };
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Route module
90
+ // ---------------------------------------------------------------------------
91
+ export function createScrapbookMutationsRouter(ctx) {
92
+ const app = new Hono();
93
+ // POST /api/dev/scrapbook/save
94
+ // Body: { site, slug, filename, body }
95
+ // Behavior: write `body` to <contentDir>/<slug>/scrapbook/<filename>.
96
+ // Creates the file if missing (delegates to create when absent so the
97
+ // operator's first save lands rather than 404'ing).
98
+ app.post('/save', async (c) => {
99
+ const parsed = await readJson(c);
100
+ if (!parsed.ok)
101
+ return c.json({ error: parsed.error }, parsed.status);
102
+ const env = checkEnvelope(ctx, parsed.value);
103
+ if ('error' in env)
104
+ return c.json({ error: env.error }, env.status);
105
+ const filename = parsed.value.filename;
106
+ const bodyText = parsed.value.body;
107
+ if (typeof filename !== 'string' || filename.length === 0) {
108
+ return c.json({ error: 'filename is required' }, 400);
109
+ }
110
+ if (typeof bodyText !== 'string') {
111
+ return c.json({ error: 'body must be a string' }, 400);
112
+ }
113
+ let item;
114
+ try {
115
+ // Resolve to test for existence. If absent, create-then-return.
116
+ const abs = scrapbookFilePath(ctx.projectRoot, ctx.config, env.site, env.slug, filename, { secret: env.secret });
117
+ if (!existsSync(abs)) {
118
+ // Only `.md` files can be created by the create helper, but
119
+ // save's contract is "write whatever was sent". For non-md
120
+ // files that don't yet exist, fall through to upload semantics.
121
+ if (filename.endsWith('.md')) {
122
+ item = createScrapbookMarkdown(ctx.projectRoot, ctx.config, env.site, env.slug, filename, bodyText, { secret: env.secret });
123
+ }
124
+ else {
125
+ item = writeScrapbookUpload(ctx.projectRoot, ctx.config, env.site, env.slug, filename, Buffer.from(bodyText, 'utf-8'), { secret: env.secret });
126
+ }
127
+ }
128
+ else {
129
+ item = saveScrapbookFile(ctx.projectRoot, ctx.config, env.site, env.slug, filename, bodyText, { secret: env.secret });
130
+ }
131
+ }
132
+ catch (err) {
133
+ const reason = err instanceof Error ? err.message : String(err);
134
+ return c.json({ error: reason }, statusForError(reason));
135
+ }
136
+ return c.json({ item }, 200);
137
+ });
138
+ // POST /api/dev/scrapbook/rename
139
+ // Body: { site, slug, oldName, newName, secret?, toSecret? }
140
+ //
141
+ // Two modes:
142
+ // - In-place rename: secret = source location (defaults to false);
143
+ // toSecret omitted (or equal to secret). The file is renamed
144
+ // inside the same section.
145
+ // - Cross-section move (#28): secret = source, toSecret = target
146
+ // section. When secret !== toSecret, the file is moved between
147
+ // `scrapbook/` and `scrapbook/secret/`. The studio's UI exposes
148
+ // this as "Mark secret" / "Mark public".
149
+ //
150
+ // 409 if the target newName already exists (in the destination
151
+ // section). 404 if oldName is missing in the source section.
152
+ app.post('/rename', async (c) => {
153
+ const parsed = await readJson(c);
154
+ if (!parsed.ok)
155
+ return c.json({ error: parsed.error }, parsed.status);
156
+ const env = checkEnvelope(ctx, parsed.value);
157
+ if ('error' in env)
158
+ return c.json({ error: env.error }, env.status);
159
+ const oldName = parsed.value.oldName;
160
+ const newName = parsed.value.newName;
161
+ if (typeof oldName !== 'string' || oldName.length === 0) {
162
+ return c.json({ error: 'oldName is required' }, 400);
163
+ }
164
+ if (typeof newName !== 'string' || newName.length === 0) {
165
+ return c.json({ error: 'newName is required' }, 400);
166
+ }
167
+ const toSecretRaw = parsed.value.toSecret;
168
+ if (toSecretRaw !== undefined && typeof toSecretRaw !== 'boolean') {
169
+ return c.json({ error: 'toSecret must be a boolean when provided' }, 400);
170
+ }
171
+ const toSecret = toSecretRaw === undefined ? env.secret : toSecretRaw;
172
+ let item;
173
+ try {
174
+ if (toSecret === env.secret) {
175
+ item = renameScrapbookFile(ctx.projectRoot, ctx.config, env.site, env.slug, oldName, newName, { secret: env.secret });
176
+ }
177
+ else {
178
+ // Cross-section move. Use the path-resolver to compute the
179
+ // source and destination absolute paths under the right
180
+ // sub-roots (and let the resolver enforce traversal guards).
181
+ // Then physically rename across the two paths.
182
+ const srcAbs = scrapbookFilePath(ctx.projectRoot, ctx.config, env.site, env.slug, oldName, { secret: env.secret });
183
+ const dstAbs = scrapbookFilePath(ctx.projectRoot, ctx.config, env.site, env.slug, newName, { secret: toSecret });
184
+ if (!existsSync(srcAbs)) {
185
+ return c.json({ error: `file not found: "${oldName}"` }, 404);
186
+ }
187
+ if (existsSync(dstAbs)) {
188
+ return c.json({ error: `target name already exists: "${newName}"` }, 409);
189
+ }
190
+ // Ensure the destination directory exists (creates `secret/`
191
+ // when promoting public → secret for the first time).
192
+ mkdirSync(dirname(dstAbs), { recursive: true });
193
+ renameSync(srcAbs, dstAbs);
194
+ const st = statSync(dstAbs);
195
+ // Build the response item the same shape the helper uses.
196
+ item = {
197
+ name: newName,
198
+ kind: classify(newName),
199
+ size: st.size,
200
+ mtime: st.mtime.toISOString(),
201
+ };
202
+ }
203
+ }
204
+ catch (err) {
205
+ const reason = err instanceof Error ? err.message : String(err);
206
+ return c.json({ error: reason }, statusForError(reason));
207
+ }
208
+ return c.json({ item }, 200);
209
+ });
210
+ // POST /api/dev/scrapbook/delete
211
+ // Body: { site, slug, filename }
212
+ // Behavior: unlink the file. 404 if missing.
213
+ app.post('/delete', async (c) => {
214
+ const parsed = await readJson(c);
215
+ if (!parsed.ok)
216
+ return c.json({ error: parsed.error }, parsed.status);
217
+ const env = checkEnvelope(ctx, parsed.value);
218
+ if ('error' in env)
219
+ return c.json({ error: env.error }, env.status);
220
+ const filename = parsed.value.filename;
221
+ if (typeof filename !== 'string' || filename.length === 0) {
222
+ return c.json({ error: 'filename is required' }, 400);
223
+ }
224
+ try {
225
+ deleteScrapbookFile(ctx.projectRoot, ctx.config, env.site, env.slug, filename, { secret: env.secret });
226
+ }
227
+ catch (err) {
228
+ const reason = err instanceof Error ? err.message : String(err);
229
+ return c.json({ error: reason }, statusForError(reason));
230
+ }
231
+ return c.json({ ok: true }, 200);
232
+ });
233
+ // POST /api/dev/scrapbook/create
234
+ // Body: { site, slug, filename, body? }
235
+ // Behavior: create a new markdown file. 409 if it already exists.
236
+ app.post('/create', async (c) => {
237
+ const parsed = await readJson(c);
238
+ if (!parsed.ok)
239
+ return c.json({ error: parsed.error }, parsed.status);
240
+ const env = checkEnvelope(ctx, parsed.value);
241
+ if ('error' in env)
242
+ return c.json({ error: env.error }, env.status);
243
+ const filename = parsed.value.filename;
244
+ const bodyText = parsed.value.body ?? '';
245
+ if (typeof filename !== 'string' || filename.length === 0) {
246
+ return c.json({ error: 'filename is required' }, 400);
247
+ }
248
+ if (typeof bodyText !== 'string') {
249
+ return c.json({ error: 'body must be a string' }, 400);
250
+ }
251
+ let item;
252
+ try {
253
+ item = createScrapbookMarkdown(ctx.projectRoot, ctx.config, env.site, env.slug, filename, bodyText, { secret: env.secret });
254
+ }
255
+ catch (err) {
256
+ const reason = err instanceof Error ? err.message : String(err);
257
+ return c.json({ error: reason }, statusForError(reason));
258
+ }
259
+ return c.json({ item }, 200);
260
+ });
261
+ // POST /api/dev/scrapbook/upload
262
+ // Multipart body: { site, slug, file, secret? }
263
+ // Behavior: save the uploaded file (binary-safe) at
264
+ // <contentDir>/<slug>/scrapbook/<file.name>, or under
265
+ // `scrapbook/secret/` when the `secret` form field is the literal
266
+ // string "true". 409 if it already exists — operator must rename
267
+ // or delete first.
268
+ app.post('/upload', async (c) => {
269
+ let form;
270
+ try {
271
+ form = await c.req.formData();
272
+ }
273
+ catch {
274
+ return c.json({ error: 'invalid multipart body' }, 400);
275
+ }
276
+ const site = form.get('site');
277
+ const slug = form.get('slug');
278
+ const file = form.get('file');
279
+ const secretField = form.get('secret');
280
+ const secret = typeof secretField === 'string' && secretField === 'true';
281
+ if (typeof site !== 'string' || site.length === 0) {
282
+ return c.json({ error: 'site is required' }, 400);
283
+ }
284
+ if (typeof slug !== 'string' || slug.length === 0) {
285
+ return c.json({ error: 'slug is required' }, 400);
286
+ }
287
+ if (!(site in ctx.config.sites)) {
288
+ return c.json({ error: `unknown site: ${site}` }, 404);
289
+ }
290
+ if (!(file instanceof File)) {
291
+ return c.json({ error: 'file is required (multipart)' }, 400);
292
+ }
293
+ const filename = file.name;
294
+ if (typeof filename !== 'string' || filename.length === 0) {
295
+ return c.json({ error: 'uploaded file has no name' }, 400);
296
+ }
297
+ let item;
298
+ try {
299
+ const buf = Buffer.from(await file.arrayBuffer());
300
+ item = writeScrapbookUpload(ctx.projectRoot, ctx.config, site, slug, filename, buf, { secret });
301
+ }
302
+ catch (err) {
303
+ const reason = err instanceof Error ? err.message : String(err);
304
+ return c.json({ error: reason }, statusForError(reason));
305
+ }
306
+ return c.json({ item }, 200);
307
+ });
308
+ return app;
309
+ }
310
+ //# sourceMappingURL=scrapbook-mutations.js.map