@deskwork/studio 0.14.1 → 0.16.0

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 (86) hide show
  1. package/dist/components/scrapbook-item.d.ts +20 -0
  2. package/dist/components/scrapbook-item.d.ts.map +1 -1
  3. package/dist/components/scrapbook-item.js +21 -2
  4. package/dist/components/scrapbook-item.js.map +1 -1
  5. package/dist/pages/dashboard/affordances.d.ts.map +1 -1
  6. package/dist/pages/dashboard/affordances.js +13 -1
  7. package/dist/pages/dashboard/affordances.js.map +1 -1
  8. package/dist/pages/dashboard/affordances.ts +13 -1
  9. package/dist/pages/dashboard/press-queue.d.ts +16 -0
  10. package/dist/pages/dashboard/press-queue.d.ts.map +1 -0
  11. package/dist/pages/dashboard/press-queue.js +91 -0
  12. package/dist/pages/dashboard/press-queue.js.map +1 -0
  13. package/dist/pages/dashboard/press-queue.ts +112 -0
  14. package/dist/pages/dashboard.d.ts.map +1 -1
  15. package/dist/pages/dashboard.js +2 -0
  16. package/dist/pages/dashboard.js.map +1 -1
  17. package/dist/pages/dashboard.ts +2 -0
  18. package/dist/pages/entry-review/decision-strip.d.ts.map +1 -1
  19. package/dist/pages/entry-review/decision-strip.js +10 -2
  20. package/dist/pages/entry-review/decision-strip.js.map +1 -1
  21. package/dist/pages/entry-review/index.d.ts.map +1 -1
  22. package/dist/pages/entry-review/index.js +7 -4
  23. package/dist/pages/entry-review/index.js.map +1 -1
  24. package/dist/pages/help.js +11 -11
  25. package/dist/pages/help.js.map +1 -1
  26. package/dist/pages/help.ts +11 -11
  27. package/dist/pages/review-scrapbook-drawer.d.ts.map +1 -1
  28. package/dist/pages/review-scrapbook-drawer.js +10 -1
  29. package/dist/pages/review-scrapbook-drawer.js.map +1 -1
  30. package/dist/pages/review-scrapbook-drawer.ts +11 -1
  31. package/dist/pages/scrapbook/dispatch.d.ts +50 -0
  32. package/dist/pages/scrapbook/dispatch.d.ts.map +1 -0
  33. package/dist/pages/scrapbook/dispatch.js +104 -0
  34. package/dist/pages/scrapbook/dispatch.js.map +1 -0
  35. package/dist/pages/scrapbook/image-readers.d.ts +24 -0
  36. package/dist/pages/scrapbook/image-readers.d.ts.map +1 -0
  37. package/dist/pages/scrapbook/image-readers.js +135 -0
  38. package/dist/pages/scrapbook/image-readers.js.map +1 -0
  39. package/dist/pages/scrapbook/index.d.ts +21 -0
  40. package/dist/pages/scrapbook/index.d.ts.map +1 -0
  41. package/dist/pages/scrapbook/index.js +96 -0
  42. package/dist/pages/scrapbook/index.js.map +1 -0
  43. package/dist/pages/scrapbook/render.d.ts +68 -0
  44. package/dist/pages/scrapbook/render.d.ts.map +1 -0
  45. package/dist/pages/scrapbook/render.js +315 -0
  46. package/dist/pages/scrapbook/render.js.map +1 -0
  47. package/dist/pages/scrapbook/text-helpers.d.ts +40 -0
  48. package/dist/pages/scrapbook/text-helpers.d.ts.map +1 -0
  49. package/dist/pages/scrapbook/text-helpers.js +92 -0
  50. package/dist/pages/scrapbook/text-helpers.js.map +1 -0
  51. package/dist/pages/scrapbook/types.d.ts +26 -0
  52. package/dist/pages/scrapbook/types.d.ts.map +1 -0
  53. package/dist/pages/scrapbook/types.js +12 -0
  54. package/dist/pages/scrapbook/types.js.map +1 -0
  55. package/dist/pages/scrapbook.d.ts +8 -8
  56. package/dist/pages/scrapbook.d.ts.map +1 -1
  57. package/dist/pages/scrapbook.js +8 -600
  58. package/dist/pages/scrapbook.js.map +1 -1
  59. package/dist/pages/scrapbook.ts +11 -660
  60. package/dist/routes/api.d.ts.map +1 -1
  61. package/dist/routes/api.js +102 -140
  62. package/dist/routes/api.js.map +1 -1
  63. package/dist/routes/entry-annotation-body.d.ts +29 -0
  64. package/dist/routes/entry-annotation-body.d.ts.map +1 -1
  65. package/dist/routes/entry-annotation-body.js +80 -0
  66. package/dist/routes/entry-annotation-body.js.map +1 -1
  67. package/dist/routes/scrapbook-file.d.ts +17 -9
  68. package/dist/routes/scrapbook-file.d.ts.map +1 -1
  69. package/dist/routes/scrapbook-file.js +48 -15
  70. package/dist/routes/scrapbook-file.js.map +1 -1
  71. package/dist/routes/scrapbook-mutation-dispatch.d.ts +29 -0
  72. package/dist/routes/scrapbook-mutation-dispatch.d.ts.map +1 -0
  73. package/dist/routes/scrapbook-mutation-dispatch.js +63 -0
  74. package/dist/routes/scrapbook-mutation-dispatch.js.map +1 -0
  75. package/dist/routes/scrapbook-mutation-envelope.d.ts +93 -0
  76. package/dist/routes/scrapbook-mutation-envelope.d.ts.map +1 -0
  77. package/dist/routes/scrapbook-mutation-envelope.js +147 -0
  78. package/dist/routes/scrapbook-mutation-envelope.js.map +1 -0
  79. package/dist/routes/scrapbook-mutations.d.ts +25 -7
  80. package/dist/routes/scrapbook-mutations.d.ts.map +1 -1
  81. package/dist/routes/scrapbook-mutations.js +67 -92
  82. package/dist/routes/scrapbook-mutations.js.map +1 -1
  83. package/dist/server.d.ts.map +1 -1
  84. package/dist/server.js +16 -2
  85. package/dist/server.js.map +1 -1
  86. package/package.json +2 -2
@@ -1,20 +1,29 @@
1
1
  /**
2
2
  * Read-only binary endpoint for scrapbook files.
3
3
  *
4
- * `GET /api/dev/scrapbook-file?site=<slug>&path=<scrapbook path>&name=<filename>[&secret=1]`
4
+ * Two addressing modes:
5
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.
6
+ * `GET /api/dev/scrapbook-file?site=<slug>&path=<scrapbook path>&name=<filename>[&secret=1]`
7
+ * slug-shape addressing. `path` is the directory under `contentDir`
8
+ * whose `scrapbook/` subdir holds the file. Validated through
9
+ * `assertSlug` (kebab-case-only); rejects paths with dots / uppercase
10
+ * / non-slug characters.
11
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.
12
+ * `GET /api/dev/scrapbook-file?site=<slug>&entryId=<uuid>&name=<filename>[&secret=1]`
13
+ * entry-id addressing. The route reads the entry's sidecar to find
14
+ * its on-disk artifactPath, derives the scrapbook dir from the
15
+ * artifact's parent directory, and serves the file. No slug-shape
16
+ * validation — works for projects whose feature-doc layout doesn't
17
+ * match the kebab-case slug template (e.g. `docs/<version>/<status>/<feature>/`).
18
+ *
19
+ * Both modes return the raw bytes of a single scrapbook file with a
20
+ * sensible Content-Type header. Filename + path-traversal guards apply
21
+ * in both modes (via `assertFilename` + the containment check in
22
+ * `scrapbookFilePathAtDir`).
15
23
  */
16
24
  import { extname } from 'node:path';
17
- import { readScrapbookFile } from '@deskwork/core/scrapbook';
25
+ import { readScrapbookFile, readScrapbookFileForEntry, } from '@deskwork/core/scrapbook';
26
+ import { readSidecar } from '@deskwork/core/sidecar';
18
27
  const MIME_TYPES = {
19
28
  '.png': 'image/png',
20
29
  '.jpg': 'image/jpeg',
@@ -38,19 +47,43 @@ function contentTypeFor(filename) {
38
47
  export async function serveScrapbookFile(c, ctx) {
39
48
  const site = c.req.query('site');
40
49
  const path = c.req.query('path');
50
+ const entryId = c.req.query('entryId');
41
51
  const name = c.req.query('name');
42
52
  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);
53
+ if (!site || !name) {
54
+ return c.json({ error: 'site and name query params are required' }, 400);
55
+ }
56
+ if (!path && !entryId) {
57
+ return c.json({ error: 'either path or entryId query param is required' }, 400);
45
58
  }
46
59
  if (!(site in ctx.config.sites)) {
47
60
  return c.json({ error: `unknown site: ${site}` }, 404);
48
61
  }
62
+ // Reject malformed entryId before it reaches the filesystem. `readSidecar`
63
+ // composes the path as `<projectRoot>/.deskwork/entries/<entryId>.json` —
64
+ // `node:path`'s join collapses `..` segments, so an unvalidated entryId
65
+ // can probe arbitrary on-disk locations even though no data is leaked
66
+ // (ENOENT becomes 404). Match the UUID schema enforced on entry creation.
67
+ if (entryId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entryId)) {
68
+ return c.json({ error: 'invalid entryId' }, 400);
69
+ }
49
70
  let result;
50
71
  try {
51
- result = readScrapbookFile(ctx.projectRoot, ctx.config, site, path, name, {
52
- secret,
53
- });
72
+ if (entryId) {
73
+ // Entry-id mode: resolve the scrapbook dir via the entry's sidecar.
74
+ // Bypasses slug-shape validation so projects with non-kebab-case
75
+ // content layouts (dots, uppercase, etc.) can still serve assets.
76
+ const entry = await readSidecar(ctx.projectRoot, entryId);
77
+ result = readScrapbookFileForEntry(ctx.projectRoot, ctx.config, site, { id: entry.uuid, slug: entry.slug }, name, { secret });
78
+ }
79
+ else {
80
+ // Slug-shape mode: backwards-compatible with existing scrapbook-viewer
81
+ // callers. `path!` is non-null here because the early-return covered
82
+ // the both-missing case.
83
+ result = readScrapbookFile(ctx.projectRoot, ctx.config, site, path, name, {
84
+ secret,
85
+ });
86
+ }
54
87
  }
55
88
  catch (err) {
56
89
  const reason = err instanceof Error ? err.message : String(err);
@@ -1 +1 @@
1
- {"version":3,"file":"scrapbook-file.js","sourceRoot":"","sources":["../../src/routes/scrapbook-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scrapbook-file.js","sourceRoot":"","sources":["../../src/routes/scrapbook-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Per-mode dispatch helpers for the studio scrapbook mutation routes.
3
+ *
4
+ * Post-#192: both modes (`entry` and `slug`) route through the entry-
5
+ * aware `*AtDir` family after `resolveScrapbookDir` produces an absolute
6
+ * scrapbook directory. The slug-mode resolution still goes through
7
+ * `scrapbookDirForEntry` — passing `{ slug }` without an id triggers
8
+ * the internal slug-template fallback inside the resolver — but the
9
+ * public CRUD surface is unified on the `*AtDir` helpers. The route
10
+ * handler stays thin: read JSON, parse envelope, validate route-
11
+ * specific args, dispatch.
12
+ *
13
+ * Extracted from `scrapbook-mutations.ts` (#191) to keep the route file
14
+ * under the project's 300–500 line cap.
15
+ */
16
+ import { type ScrapbookItem } from '@deskwork/core/scrapbook';
17
+ import type { StudioContext } from './api.ts';
18
+ import { type ParsedEnvelope } from './scrapbook-mutation-envelope.ts';
19
+ export declare function saveDispatch(ctx: StudioContext, env: ParsedEnvelope, filename: string, bodyText: string): Promise<ScrapbookItem>;
20
+ export declare function renameInPlace(ctx: StudioContext, env: ParsedEnvelope, oldName: string, newName: string): Promise<ScrapbookItem>;
21
+ export interface CrossSectionPaths {
22
+ srcAbs: string;
23
+ dstAbs: string;
24
+ }
25
+ export declare function resolveCrossSectionPaths(ctx: StudioContext, env: ParsedEnvelope, oldName: string, newName: string, toSecret: boolean): Promise<CrossSectionPaths>;
26
+ export declare function deleteDispatch(ctx: StudioContext, env: ParsedEnvelope, filename: string): Promise<void>;
27
+ export declare function createDispatch(ctx: StudioContext, env: ParsedEnvelope, filename: string, bodyText: string): Promise<ScrapbookItem>;
28
+ export declare function uploadDispatch(ctx: StudioContext, env: ParsedEnvelope, filename: string, buf: Buffer): Promise<ScrapbookItem>;
29
+ //# sourceMappingURL=scrapbook-mutation-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-mutation-dispatch.d.ts","sourceRoot":"","sources":["../../src/routes/scrapbook-mutation-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,kCAAkC,CAAC;AAE1C,wBAAsB,YAAY,CAChC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAmBxB;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAKxB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAM5B;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,aAAa,CAAC,CAKxB;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,aAAa,CAAC,CAKxB"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Per-mode dispatch helpers for the studio scrapbook mutation routes.
3
+ *
4
+ * Post-#192: both modes (`entry` and `slug`) route through the entry-
5
+ * aware `*AtDir` family after `resolveScrapbookDir` produces an absolute
6
+ * scrapbook directory. The slug-mode resolution still goes through
7
+ * `scrapbookDirForEntry` — passing `{ slug }` without an id triggers
8
+ * the internal slug-template fallback inside the resolver — but the
9
+ * public CRUD surface is unified on the `*AtDir` helpers. The route
10
+ * handler stays thin: read JSON, parse envelope, validate route-
11
+ * specific args, dispatch.
12
+ *
13
+ * Extracted from `scrapbook-mutations.ts` (#191) to keep the route file
14
+ * under the project's 300–500 line cap.
15
+ */
16
+ import { createScrapbookMarkdownAtDir, deleteScrapbookFileAtDir, renameScrapbookFileAtDir, saveScrapbookFileAtDir, scrapbookFilePathAtDir, writeScrapbookUploadAtDir, } from '@deskwork/core/scrapbook';
17
+ import { existsSync } from 'node:fs';
18
+ import { resolveScrapbookDir, } from "./scrapbook-mutation-envelope.js";
19
+ export async function saveDispatch(ctx, env, filename, bodyText) {
20
+ const dir = await resolveScrapbookDir(ctx, env);
21
+ const abs = scrapbookFilePathAtDir(dir, filename, { secret: env.secret });
22
+ if (!existsSync(abs)) {
23
+ if (filename.endsWith('.md')) {
24
+ return createScrapbookMarkdownAtDir(dir, filename, bodyText, {
25
+ secret: env.secret,
26
+ });
27
+ }
28
+ return writeScrapbookUploadAtDir(dir, filename, Buffer.from(bodyText, 'utf-8'), { secret: env.secret });
29
+ }
30
+ return saveScrapbookFileAtDir(dir, filename, bodyText, {
31
+ secret: env.secret,
32
+ });
33
+ }
34
+ export async function renameInPlace(ctx, env, oldName, newName) {
35
+ const dir = await resolveScrapbookDir(ctx, env);
36
+ return renameScrapbookFileAtDir(dir, oldName, newName, {
37
+ secret: env.secret,
38
+ });
39
+ }
40
+ export async function resolveCrossSectionPaths(ctx, env, oldName, newName, toSecret) {
41
+ const dir = await resolveScrapbookDir(ctx, env);
42
+ return {
43
+ srcAbs: scrapbookFilePathAtDir(dir, oldName, { secret: env.secret }),
44
+ dstAbs: scrapbookFilePathAtDir(dir, newName, { secret: toSecret }),
45
+ };
46
+ }
47
+ export async function deleteDispatch(ctx, env, filename) {
48
+ const dir = await resolveScrapbookDir(ctx, env);
49
+ deleteScrapbookFileAtDir(dir, filename, { secret: env.secret });
50
+ }
51
+ export async function createDispatch(ctx, env, filename, bodyText) {
52
+ const dir = await resolveScrapbookDir(ctx, env);
53
+ return createScrapbookMarkdownAtDir(dir, filename, bodyText, {
54
+ secret: env.secret,
55
+ });
56
+ }
57
+ export async function uploadDispatch(ctx, env, filename, buf) {
58
+ const dir = await resolveScrapbookDir(ctx, env);
59
+ return writeScrapbookUploadAtDir(dir, filename, buf, {
60
+ secret: env.secret,
61
+ });
62
+ }
63
+ //# sourceMappingURL=scrapbook-mutation-dispatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-mutation-dispatch.js","sourceRoot":"","sources":["../../src/routes/scrapbook-mutation-dispatch.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Mutation-envelope parsing for the studio scrapbook routes.
3
+ *
4
+ * Lives next to `scrapbook-mutations.ts`; extracted (#191) to keep the
5
+ * route file under the project's 300–500 line cap. The single
6
+ * responsibility is:
7
+ *
8
+ * 1. Validate the request envelope (`site`, `entryId | slug`, `secret?`).
9
+ * 2. Validate `entryId` against the UUID regex BEFORE filesystem access
10
+ * (the scrapbook-file route's same pattern, landed in v0.15.0
11
+ * commit `14ffbe7`).
12
+ * 3. Resolve the absolute scrapbook directory for whichever addressing
13
+ * mode the request used. Both modes route through
14
+ * `scrapbookDirForEntry`: entry-id mode reads the sidecar to get
15
+ * the bound `{ uuid, slug }`; slug mode synthesizes `{ slug }`
16
+ * (no id) and lets the entry-aware resolver fall back to its
17
+ * internal slug-template path. Post-#192 this is a single code
18
+ * path with no public slug-template entry point.
19
+ *
20
+ * The envelope is a discriminated union (`mode: 'entry' | 'slug'`); call
21
+ * sites narrow without `as` casts.
22
+ */
23
+ import type { StudioContext } from './api.ts';
24
+ /**
25
+ * Mirrors the UUID regex enforced on entry creation and used by the
26
+ * scrapbook-file route. Rejects malformed `entryId` before it reaches
27
+ * the filesystem — `readSidecar` composes its path as
28
+ * `<projectRoot>/.deskwork/entries/<entryId>.json`, and `node:path`'s
29
+ * join collapses `..` segments, so an unvalidated `entryId` could probe
30
+ * arbitrary on-disk locations.
31
+ */
32
+ export declare const UUID_RE: RegExp;
33
+ /**
34
+ * Successfully-parsed mutation envelope.
35
+ *
36
+ * Exactly one of `mode === 'entry'` or `mode === 'slug'` is set per
37
+ * request — the discriminant tells the dispatch which addressing mode to
38
+ * use without `as` casts at the call site.
39
+ */
40
+ export type ParsedEnvelope = {
41
+ mode: 'entry';
42
+ site: string;
43
+ entryId: string;
44
+ secret: boolean;
45
+ } | {
46
+ mode: 'slug';
47
+ site: string;
48
+ slug: string;
49
+ secret: boolean;
50
+ };
51
+ export interface EnvelopeError {
52
+ error: string;
53
+ status: 400 | 404;
54
+ }
55
+ export declare function isEnvelopeError(v: ParsedEnvelope | EnvelopeError): v is EnvelopeError;
56
+ /**
57
+ * Validate the common `{ site, entryId? | slug?, secret? }` envelope for
58
+ * JSON-body routes. Returns a typed object or an `EnvelopeError` that
59
+ * the caller propagates directly. The site existence check is here so
60
+ * every mutation 404s on unknown sites the same way the read endpoint
61
+ * does.
62
+ *
63
+ * `entryId` is preferred when both are present. UUID validation runs
64
+ * BEFORE filesystem access for the same reason as the scrapbook-file
65
+ * route — see the UUID_RE comment above.
66
+ */
67
+ export declare function checkJsonEnvelope(ctx: StudioContext, body: Record<string, unknown>): ParsedEnvelope | EnvelopeError;
68
+ /**
69
+ * Same shape as `checkJsonEnvelope` but reads from a `FormData` instance.
70
+ * Used by the upload route, which speaks multipart for binary file
71
+ * payloads. The semantics match — `entryId` preferred, `slug` is the
72
+ * deprecation-window fallback, UUID validation up-front.
73
+ *
74
+ * Note: the multipart `secret` field arrives as the literal string
75
+ * `"true"` (form fields are always strings); we normalize it here so
76
+ * the rest of the pipeline doesn't have to special-case form-vs-json.
77
+ */
78
+ export declare function checkFormEnvelope(ctx: StudioContext, form: FormData): ParsedEnvelope | EnvelopeError;
79
+ /**
80
+ * Resolve the absolute scrapbook directory for a parsed envelope. Both
81
+ * modes go through `scrapbookDirForEntry`:
82
+ * - entry-id mode reads the sidecar and passes `{ id, slug }` so the
83
+ * resolver hits the index lookup (refactor-proof binding).
84
+ * - slug mode passes `{ slug }` only; the resolver still goes through
85
+ * `findEntryFile`, which falls back to its private slug-template
86
+ * path when no id binding exists. Post-#192 this is the only code
87
+ * path; the public `scrapbookDir(slug)` was retired.
88
+ *
89
+ * Throws on lookup / resolution failure; the caller maps the error
90
+ * message onto the right HTTP status via `statusForError`.
91
+ */
92
+ export declare function resolveScrapbookDir(ctx: StudioContext, env: ParsedEnvelope): Promise<string>;
93
+ //# sourceMappingURL=scrapbook-mutation-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-mutation-envelope.d.ts","sourceRoot":"","sources":["../../src/routes/scrapbook-mutation-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAM9C;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,QAC+C,CAAC;AAMpE;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;CACjB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEN,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;CACnB;AAED,wBAAgB,eAAe,CAC7B,CAAC,EAAE,cAAc,GAAG,aAAa,GAChC,CAAC,IAAI,aAAa,CAEpB;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,cAAc,GAAG,aAAa,CAmChC;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,QAAQ,GACb,cAAc,GAAG,aAAa,CAwBhC;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Mutation-envelope parsing for the studio scrapbook routes.
3
+ *
4
+ * Lives next to `scrapbook-mutations.ts`; extracted (#191) to keep the
5
+ * route file under the project's 300–500 line cap. The single
6
+ * responsibility is:
7
+ *
8
+ * 1. Validate the request envelope (`site`, `entryId | slug`, `secret?`).
9
+ * 2. Validate `entryId` against the UUID regex BEFORE filesystem access
10
+ * (the scrapbook-file route's same pattern, landed in v0.15.0
11
+ * commit `14ffbe7`).
12
+ * 3. Resolve the absolute scrapbook directory for whichever addressing
13
+ * mode the request used. Both modes route through
14
+ * `scrapbookDirForEntry`: entry-id mode reads the sidecar to get
15
+ * the bound `{ uuid, slug }`; slug mode synthesizes `{ slug }`
16
+ * (no id) and lets the entry-aware resolver fall back to its
17
+ * internal slug-template path. Post-#192 this is a single code
18
+ * path with no public slug-template entry point.
19
+ *
20
+ * The envelope is a discriminated union (`mode: 'entry' | 'slug'`); call
21
+ * sites narrow without `as` casts.
22
+ */
23
+ import { scrapbookDirForEntry } from '@deskwork/core/scrapbook';
24
+ import { readSidecar } from '@deskwork/core/sidecar';
25
+ // ---------------------------------------------------------------------------
26
+ // UUID validation
27
+ // ---------------------------------------------------------------------------
28
+ /**
29
+ * Mirrors the UUID regex enforced on entry creation and used by the
30
+ * scrapbook-file route. Rejects malformed `entryId` before it reaches
31
+ * the filesystem — `readSidecar` composes its path as
32
+ * `<projectRoot>/.deskwork/entries/<entryId>.json`, and `node:path`'s
33
+ * join collapses `..` segments, so an unvalidated `entryId` could probe
34
+ * arbitrary on-disk locations.
35
+ */
36
+ export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
37
+ export function isEnvelopeError(v) {
38
+ return 'error' in v;
39
+ }
40
+ // ---------------------------------------------------------------------------
41
+ // JSON envelope check
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * Validate the common `{ site, entryId? | slug?, secret? }` envelope for
45
+ * JSON-body routes. Returns a typed object or an `EnvelopeError` that
46
+ * the caller propagates directly. The site existence check is here so
47
+ * every mutation 404s on unknown sites the same way the read endpoint
48
+ * does.
49
+ *
50
+ * `entryId` is preferred when both are present. UUID validation runs
51
+ * BEFORE filesystem access for the same reason as the scrapbook-file
52
+ * route — see the UUID_RE comment above.
53
+ */
54
+ export function checkJsonEnvelope(ctx, body) {
55
+ const site = body.site;
56
+ if (typeof site !== 'string' || site.length === 0) {
57
+ return { error: 'site is required', status: 400 };
58
+ }
59
+ if (!(site in ctx.config.sites)) {
60
+ return { error: `unknown site: ${site}`, status: 404 };
61
+ }
62
+ const secretRaw = body.secret;
63
+ if (secretRaw !== undefined && typeof secretRaw !== 'boolean') {
64
+ return { error: 'secret must be a boolean when provided', status: 400 };
65
+ }
66
+ const secret = secretRaw === true;
67
+ const entryId = body.entryId;
68
+ if (entryId !== undefined) {
69
+ if (typeof entryId !== 'string' || entryId.length === 0) {
70
+ return {
71
+ error: 'entryId must be a non-empty string when provided',
72
+ status: 400,
73
+ };
74
+ }
75
+ if (!UUID_RE.test(entryId)) {
76
+ return { error: 'invalid entryId', status: 400 };
77
+ }
78
+ return { mode: 'entry', site, entryId, secret };
79
+ }
80
+ // Back-compat slug-template fallback. To be removed in #192 once all
81
+ // callers send entryId.
82
+ const slug = body.slug;
83
+ if (typeof slug !== 'string' || slug.length === 0) {
84
+ return { error: 'entryId or slug is required', status: 400 };
85
+ }
86
+ return { mode: 'slug', site, slug, secret };
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Multipart envelope check
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Same shape as `checkJsonEnvelope` but reads from a `FormData` instance.
93
+ * Used by the upload route, which speaks multipart for binary file
94
+ * payloads. The semantics match — `entryId` preferred, `slug` is the
95
+ * deprecation-window fallback, UUID validation up-front.
96
+ *
97
+ * Note: the multipart `secret` field arrives as the literal string
98
+ * `"true"` (form fields are always strings); we normalize it here so
99
+ * the rest of the pipeline doesn't have to special-case form-vs-json.
100
+ */
101
+ export function checkFormEnvelope(ctx, form) {
102
+ const site = form.get('site');
103
+ if (typeof site !== 'string' || site.length === 0) {
104
+ return { error: 'site is required', status: 400 };
105
+ }
106
+ if (!(site in ctx.config.sites)) {
107
+ return { error: `unknown site: ${site}`, status: 404 };
108
+ }
109
+ const secretField = form.get('secret');
110
+ const secret = typeof secretField === 'string' && secretField === 'true';
111
+ const entryIdField = form.get('entryId');
112
+ if (typeof entryIdField === 'string' && entryIdField.length > 0) {
113
+ if (!UUID_RE.test(entryIdField)) {
114
+ return { error: 'invalid entryId', status: 400 };
115
+ }
116
+ return { mode: 'entry', site, entryId: entryIdField, secret };
117
+ }
118
+ const slug = form.get('slug');
119
+ if (typeof slug !== 'string' || slug.length === 0) {
120
+ return { error: 'entryId or slug is required', status: 400 };
121
+ }
122
+ return { mode: 'slug', site, slug, secret };
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Directory resolution
126
+ // ---------------------------------------------------------------------------
127
+ /**
128
+ * Resolve the absolute scrapbook directory for a parsed envelope. Both
129
+ * modes go through `scrapbookDirForEntry`:
130
+ * - entry-id mode reads the sidecar and passes `{ id, slug }` so the
131
+ * resolver hits the index lookup (refactor-proof binding).
132
+ * - slug mode passes `{ slug }` only; the resolver still goes through
133
+ * `findEntryFile`, which falls back to its private slug-template
134
+ * path when no id binding exists. Post-#192 this is the only code
135
+ * path; the public `scrapbookDir(slug)` was retired.
136
+ *
137
+ * Throws on lookup / resolution failure; the caller maps the error
138
+ * message onto the right HTTP status via `statusForError`.
139
+ */
140
+ export async function resolveScrapbookDir(ctx, env) {
141
+ if (env.mode === 'entry') {
142
+ const entry = await readSidecar(ctx.projectRoot, env.entryId);
143
+ return scrapbookDirForEntry(ctx.projectRoot, ctx.config, env.site, { id: entry.uuid, slug: entry.slug });
144
+ }
145
+ return scrapbookDirForEntry(ctx.projectRoot, ctx.config, env.site, { slug: env.slug });
146
+ }
147
+ //# sourceMappingURL=scrapbook-mutation-envelope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapbook-mutation-envelope.js","sourceRoot":"","sources":["../../src/routes/scrapbook-mutation-envelope.ts"],"names":[],"mappings":"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"}
@@ -10,15 +10,33 @@
10
10
  * Phase 16d retrofitted only the read path. v0.4.1 closes the gap
11
11
  * (issue #21).
12
12
  *
13
+ * Two addressing modes (#191, unified post-#192):
14
+ * - **Entry-id mode** (preferred): `{ site, entryId, ... }` — looks up
15
+ * the entry's sidecar, derives the scrapbook dir from the artifact's
16
+ * parent directory via `scrapbookDirForEntry`, mutates there. Same
17
+ * code path the read endpoints use; refactor-proof for projects whose
18
+ * feature-doc layout doesn't match the kebab-case slug template.
19
+ * - **Slug mode** (back-compat fallback): `{ site, slug, ... }` —
20
+ * accepted for back-compat with older clients. Post-#192, slug mode
21
+ * also routes through `scrapbookDirForEntry`: passing `{ slug }`
22
+ * without an id triggers the resolver's internal slug-template
23
+ * fallback, so the public mutation surface is unified on the
24
+ * entry-aware code path. The legacy `scrapbookDir(slug)` and the
25
+ * slug-template CRUD primitives are no longer exported from
26
+ * `@deskwork/core/scrapbook`.
27
+ *
28
+ * Companion modules (#191 split, to keep this file under the project's
29
+ * 300–500 line cap):
30
+ * - `scrapbook-mutation-envelope.ts` — JSON / form envelope parsing,
31
+ * UUID validation, dir resolution.
32
+ * - `scrapbook-mutation-dispatch.ts` — per-mode dispatch helpers
33
+ * (entry-aware vs. slug-template), one per route verb.
34
+ *
13
35
  * Design notes:
14
36
  * - 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.
37
+ * `@deskwork/core/scrapbook` helpers. Those throw on `..` sequences,
38
+ * absolute paths, and any filename that escapes the scrapbook dir;
39
+ * we surface those as 400.
22
40
  * - We never log file contents on save / upload (privacy). Logging
23
41
  * filename + slug is fine but kept silent here — the studio's
24
42
  * console is the operator's, not a server log.
@@ -1 +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"}
1
+ {"version":3,"file":"scrapbook-mutations.d.ts","sourceRoot":"","sources":["../../src/routes/scrapbook-mutations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAgE9C,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAyMvE"}