@plumix/plugin-media 0.1.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/admin/FocalPointField.d.ts +22 -0
  4. package/dist/admin/FocalPointField.d.ts.map +1 -0
  5. package/dist/admin/FocalPointField.js +71 -0
  6. package/dist/admin/FocalPointField.js.map +1 -0
  7. package/dist/admin/MediaLibrary.d.ts +43 -0
  8. package/dist/admin/MediaLibrary.d.ts.map +1 -0
  9. package/dist/admin/MediaLibrary.js +698 -0
  10. package/dist/admin/MediaLibrary.js.map +1 -0
  11. package/dist/admin/MediaListPickerField.d.ts +14 -0
  12. package/dist/admin/MediaListPickerField.d.ts.map +1 -0
  13. package/dist/admin/MediaListPickerField.js +161 -0
  14. package/dist/admin/MediaListPickerField.js.map +1 -0
  15. package/dist/admin/MediaPickerField.d.ts +25 -0
  16. package/dist/admin/MediaPickerField.d.ts.map +1 -0
  17. package/dist/admin/MediaPickerField.js +82 -0
  18. package/dist/admin/MediaPickerField.js.map +1 -0
  19. package/dist/admin/MediaUrlField.d.ts +14 -0
  20. package/dist/admin/MediaUrlField.d.ts.map +1 -0
  21. package/dist/admin/MediaUrlField.js +29 -0
  22. package/dist/admin/MediaUrlField.js.map +1 -0
  23. package/dist/admin/index.d.ts +11 -0
  24. package/dist/admin/index.d.ts.map +1 -0
  25. package/dist/admin/index.js +49 -0
  26. package/dist/admin/index.js.map +1 -0
  27. package/dist/admin/messages.d.ts +24 -0
  28. package/dist/admin/messages.d.ts.map +1 -0
  29. package/dist/admin/messages.js +28 -0
  30. package/dist/admin/messages.js.map +1 -0
  31. package/dist/blocks/file/index.d.ts +2 -0
  32. package/dist/blocks/file/index.d.ts.map +1 -0
  33. package/dist/blocks/file/index.js +109 -0
  34. package/dist/blocks/file/index.js.map +1 -0
  35. package/dist/blocks/file/normalize.d.ts +10 -0
  36. package/dist/blocks/file/normalize.d.ts.map +1 -0
  37. package/dist/blocks/file/normalize.js +45 -0
  38. package/dist/blocks/file/normalize.js.map +1 -0
  39. package/dist/blocks/image/index.d.ts +2 -0
  40. package/dist/blocks/image/index.d.ts.map +1 -0
  41. package/dist/blocks/image/index.js +141 -0
  42. package/dist/blocks/image/index.js.map +1 -0
  43. package/dist/blocks/image/normalize.d.ts +7 -0
  44. package/dist/blocks/image/normalize.d.ts.map +1 -0
  45. package/dist/blocks/image/normalize.js +20 -0
  46. package/dist/blocks/image/normalize.js.map +1 -0
  47. package/dist/blocks.d.ts +2 -0
  48. package/dist/blocks.d.ts.map +1 -0
  49. package/dist/blocks.js +6 -0
  50. package/dist/blocks.js.map +1 -0
  51. package/dist/builder.d.ts +80 -0
  52. package/dist/builder.d.ts.map +1 -0
  53. package/dist/builder.js +61 -0
  54. package/dist/builder.js.map +1 -0
  55. package/dist/fields.d.ts +3 -0
  56. package/dist/fields.d.ts.map +1 -0
  57. package/dist/fields.js +7 -0
  58. package/dist/fields.js.map +1 -0
  59. package/dist/image-dimensions.d.ts +7 -0
  60. package/dist/image-dimensions.d.ts.map +1 -0
  61. package/dist/image-dimensions.js +142 -0
  62. package/dist/image-dimensions.js.map +1 -0
  63. package/dist/index.d.ts +62 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +216 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/lookup.d.ts +43 -0
  68. package/dist/lookup.d.ts.map +1 -0
  69. package/dist/lookup.js +154 -0
  70. package/dist/lookup.js.map +1 -0
  71. package/dist/magic-bytes.d.ts +4 -0
  72. package/dist/magic-bytes.d.ts.map +1 -0
  73. package/dist/magic-bytes.js +107 -0
  74. package/dist/magic-bytes.js.map +1 -0
  75. package/dist/mcp-tools.d.ts +10 -0
  76. package/dist/mcp-tools.d.ts.map +1 -0
  77. package/dist/mcp-tools.js +43 -0
  78. package/dist/mcp-tools.js.map +1 -0
  79. package/dist/media-blocks.d.ts +3 -0
  80. package/dist/media-blocks.d.ts.map +1 -0
  81. package/dist/media-blocks.js +10 -0
  82. package/dist/media-blocks.js.map +1 -0
  83. package/dist/meta.d.ts +12 -0
  84. package/dist/meta.d.ts.map +1 -0
  85. package/dist/meta.js +20 -0
  86. package/dist/meta.js.map +1 -0
  87. package/dist/mime.d.ts +3 -0
  88. package/dist/mime.d.ts.map +1 -0
  89. package/dist/mime.js +58 -0
  90. package/dist/mime.js.map +1 -0
  91. package/dist/read-service.d.ts +66 -0
  92. package/dist/read-service.d.ts.map +1 -0
  93. package/dist/read-service.js +156 -0
  94. package/dist/read-service.js.map +1 -0
  95. package/dist/rpc.d.ts +7 -0
  96. package/dist/rpc.d.ts.map +1 -0
  97. package/dist/rpc.js +352 -0
  98. package/dist/rpc.js.map +1 -0
  99. package/dist/serve-route.d.ts +19 -0
  100. package/dist/serve-route.d.ts.map +1 -0
  101. package/dist/serve-route.js +122 -0
  102. package/dist/serve-route.js.map +1 -0
  103. package/dist/upload-route.d.ts +17 -0
  104. package/dist/upload-route.d.ts.map +1 -0
  105. package/dist/upload-route.js +106 -0
  106. package/dist/upload-route.js.map +1 -0
  107. package/package.json +97 -0
@@ -0,0 +1,142 @@
1
+ // Intrinsic pixel-dimension probe for the raster image formats the media
2
+ // plugin accepts. Runs at confirm time on the same leading bytes we read for
3
+ // the magic-byte sniff, so uploaded images carry width/height without a
4
+ // separate decode pass. Returns null whenever dimensions can't be determined
5
+ // — a vector format (SVG), an unknown mime, or a sample too short to reach the
6
+ // header — and callers treat that as "no dimensions" rather than an error.
7
+ // Bytes to read for the dimension probe. Larger than the magic-byte sample
8
+ // because a JPEG's SOF marker can sit past a big APP1/EXIF block; 64 KiB
9
+ // covers the header region of the formats we probe without reading whole files.
10
+ export const DIMENSION_SAMPLE_SIZE = 65536;
11
+ function readPng(v) {
12
+ // Signature (8) + IHDR length (4) + "IHDR" (4), then width/height as BE u32.
13
+ if (v.byteLength < 24)
14
+ return null;
15
+ return { width: v.getUint32(16), height: v.getUint32(20) };
16
+ }
17
+ function readGif(v) {
18
+ // "GIF87a"/"GIF89a" (6), then width/height as LE u16 in the screen descriptor.
19
+ if (v.byteLength < 10)
20
+ return null;
21
+ return { width: v.getUint16(6, true), height: v.getUint16(8, true) };
22
+ }
23
+ // Start-Of-Frame markers carry the frame dimensions. Excludes 0xC4 (DHT),
24
+ // 0xC8 (JPG), 0xCC (DAC), which share the 0xCn range but aren't frame headers.
25
+ function isSofMarker(marker) {
26
+ return (marker >= 0xc0 &&
27
+ marker <= 0xcf &&
28
+ marker !== 0xc4 &&
29
+ marker !== 0xc8 &&
30
+ marker !== 0xcc);
31
+ }
32
+ function readJpeg(v) {
33
+ const len = v.byteLength;
34
+ if (len < 2 || v.getUint8(0) !== 0xff || v.getUint8(1) !== 0xd8)
35
+ return null;
36
+ let pos = 2;
37
+ while (pos + 1 < len) {
38
+ if (v.getUint8(pos) !== 0xff || v.getUint8(pos + 1) === 0xff) {
39
+ pos++; // stray fill byte or padding before the next marker
40
+ continue;
41
+ }
42
+ const marker = v.getUint8(pos + 1);
43
+ pos += 2;
44
+ // Standalone markers (SOI/EOI/TEM/RSTn) carry no length segment.
45
+ if (marker === 0xd8 || marker === 0xd9 || marker === 0x01)
46
+ continue;
47
+ if (marker >= 0xd0 && marker <= 0xd7)
48
+ continue;
49
+ if (pos + 1 >= len)
50
+ return null;
51
+ if (isSofMarker(marker)) {
52
+ // length(2), precision(1), then height/width as BE u16.
53
+ if (pos + 6 >= len)
54
+ return null;
55
+ return { width: v.getUint16(pos + 5), height: v.getUint16(pos + 3) };
56
+ }
57
+ const segLen = v.getUint16(pos);
58
+ if (segLen < 2)
59
+ return null;
60
+ pos += segLen;
61
+ }
62
+ return null;
63
+ }
64
+ function fourccAt(v, offset) {
65
+ return String.fromCharCode(v.getUint8(offset), v.getUint8(offset + 1), v.getUint8(offset + 2), v.getUint8(offset + 3));
66
+ }
67
+ function readWebp(v) {
68
+ // RIFF(4) + size(4) + "WEBP"(4), then a variant chunk whose fourcc at
69
+ // offset 12 selects the dimension layout.
70
+ if (v.byteLength < 16)
71
+ return null;
72
+ const fourcc = fourccAt(v, 12);
73
+ if (fourcc === "VP8X") {
74
+ // flags(1) + reserved(3), then canvas (width-1)/(height-1) as LE u24.
75
+ if (v.byteLength < 30)
76
+ return null;
77
+ const w = v.getUint16(24, true) | (v.getUint8(26) << 16);
78
+ const h = v.getUint16(27, true) | (v.getUint8(29) << 16);
79
+ return { width: w + 1, height: h + 1 };
80
+ }
81
+ if (fourcc === "VP8 ") {
82
+ // frame tag(3) + start code 0x9d012a(3), then 14-bit width/height as LE u16.
83
+ // Require the start code so a non-key-frame or corrupt chunk yields null
84
+ // rather than a masked-but-meaningless number.
85
+ if (v.byteLength < 30 ||
86
+ v.getUint8(23) !== 0x9d ||
87
+ v.getUint8(24) !== 0x01 ||
88
+ v.getUint8(25) !== 0x2a) {
89
+ return null;
90
+ }
91
+ return {
92
+ width: v.getUint16(26, true) & 0x3fff,
93
+ height: v.getUint16(28, true) & 0x3fff,
94
+ };
95
+ }
96
+ if (fourcc === "VP8L") {
97
+ // 0x2f signature at 20, then 14-bit (width-1)/(height-1) bit-packed.
98
+ if (v.byteLength < 25 || v.getUint8(20) !== 0x2f)
99
+ return null;
100
+ const bits = v.getUint32(21, true);
101
+ return {
102
+ width: (bits & 0x3fff) + 1,
103
+ height: ((bits >>> 14) & 0x3fff) + 1,
104
+ };
105
+ }
106
+ return null;
107
+ }
108
+ function readAvif(v) {
109
+ // ISOBMFF nests the ImageSpatialExtents box (`ispe`) several boxes deep;
110
+ // scan for its fourcc rather than walking the box tree, then read the BE
111
+ // u32 width/height that follow the 4-byte version/flags field. Require the
112
+ // preceding u32 to be the fixed 20-byte box size (size(4) + type(4) +
113
+ // version/flags(4) + w(4) + h(4)) so a stray `ispe` in some other box's
114
+ // payload can't be mistaken for a real spatial-extents box.
115
+ for (let i = 4; i + 16 <= v.byteLength; i++) {
116
+ if (fourccAt(v, i) === "ispe" && v.getUint32(i - 4) === 0x14) {
117
+ const width = v.getUint32(i + 8);
118
+ const height = v.getUint32(i + 12);
119
+ if (width > 0 && height > 0)
120
+ return { width, height };
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ export function readImageDimensions(bytes, mime) {
126
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
127
+ switch (mime) {
128
+ case "image/png":
129
+ return readPng(v);
130
+ case "image/gif":
131
+ return readGif(v);
132
+ case "image/jpeg":
133
+ return readJpeg(v);
134
+ case "image/webp":
135
+ return readWebp(v);
136
+ case "image/avif":
137
+ return readAvif(v);
138
+ default:
139
+ return null;
140
+ }
141
+ }
142
+ //# sourceMappingURL=image-dimensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-dimensions.js","sourceRoot":"","sources":["../src/image-dimensions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { PluginDescriptor } from "plumix/plugin";
2
+ import { DEFAULT_ACCEPTED_TYPES } from "./mime.js";
3
+ export type { MediaFieldScope } from "./lookup.js";
4
+ export { DEFAULT_ACCEPTED_TYPES };
5
+ /** Default max upload size — 25 MiB. */
6
+ export declare const DEFAULT_MAX_UPLOAD_SIZE: number;
7
+ export { mediaBlocks } from "./media-blocks.js";
8
+ interface MediaPluginOptions {
9
+ /**
10
+ * MIME types accepted by `media.createUploadUrl`. The browser sends a
11
+ * proposed `contentType`; anything outside the allowlist is rejected
12
+ * before a presigned URL is minted. Defaults to {@link DEFAULT_ACCEPTED_TYPES}.
13
+ */
14
+ readonly acceptedTypes?: readonly string[];
15
+ /**
16
+ * Maximum upload size in bytes. The browser-declared `size` in
17
+ * `media.createUploadUrl` is rejected up front if it exceeds this
18
+ * cap, the value is signed into presigned PUTs as `Content-Length`,
19
+ * and the worker-routed upload counts actual bytes streamed and
20
+ * aborts past the cap. Defaults to 25 MiB.
21
+ */
22
+ readonly maxUploadSize?: number;
23
+ }
24
+ /**
25
+ * Media plugin — registers the `media` entry type, the `media.*` RPC
26
+ * router, and the admin Media Library page.
27
+ *
28
+ * Uploads use the two-phase signed-URL flow:
29
+ *
30
+ * 1. `media.createUploadUrl({ filename, contentType, size })` →
31
+ * creates a `draft` entry, returns a presigned PUT URL the browser
32
+ * uses to upload bytes directly to storage.
33
+ * 2. `media.confirm({ id })` → flips the entry from `draft` to
34
+ * `published` once the browser-PUT succeeded.
35
+ *
36
+ * Bytes never traverse the worker. Requires a storage adapter whose
37
+ * `presignPut` is implemented (e.g. `r2({ s3: { ... } })`).
38
+ *
39
+ * Pairs with `imageDelivery:` for on-the-fly resizing.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { media } from "@plumix/plugin-media";
44
+ *
45
+ * plumix({
46
+ * storage: r2({
47
+ * binding: "MEDIA",
48
+ * publicUrlBase: "https://media.example.com",
49
+ * s3: {
50
+ * bucket: "plumix-media",
51
+ * accountId: env.CF_ACCOUNT_ID,
52
+ * accessKeyId: env.R2_ACCESS_KEY_ID,
53
+ * secretAccessKey: env.R2_SECRET_ACCESS_KEY,
54
+ * },
55
+ * }),
56
+ * imageDelivery: images({ zone: "media.example.com" }),
57
+ * plugins: [media()],
58
+ * });
59
+ * ```
60
+ */
61
+ export declare function media(options?: MediaPluginOptions): PluginDescriptor<undefined>;
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAKnD,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC,wCAAwC;AACxC,eAAO,MAAM,uBAAuB,QAAmB,CAAC;AAgExD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,UAAU,kBAAkB;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAYD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,wBAAgB,KAAK,CACnB,OAAO,GAAE,kBAAuB,GAC/B,gBAAgB,CAAC,SAAS,CAAC,CA8G7B"}
package/dist/index.js ADDED
@@ -0,0 +1,216 @@
1
+ import { definePlugin } from "plumix/plugin";
2
+ import { fileBlock } from "./blocks/file/index.js";
3
+ import { imageBlock } from "./blocks/image/index.js";
4
+ import { mediaLookupAdapter } from "./lookup.js";
5
+ import { mediaGetTool, mediaListTool } from "./mcp-tools.js";
6
+ import { DEFAULT_ACCEPTED_TYPES } from "./mime.js";
7
+ import { createMediaRouter } from "./rpc.js";
8
+ import { handleMediaServe } from "./serve-route.js";
9
+ import { handleWorkerUpload } from "./upload-route.js";
10
+ export { DEFAULT_ACCEPTED_TYPES };
11
+ /** Default max upload size — 25 MiB. */
12
+ export const DEFAULT_MAX_UPLOAD_SIZE = 25 * 1024 * 1024;
13
+ // Per-entity label table; `satisfies EntryTypeLabels` catches typo-
14
+ // renames at compile time. See `plugin-pages/src/index.ts` for the
15
+ // "no Babel macro server-side" rationale on the literal `{id,message}`
16
+ // shape.
17
+ const MEDIA_LABELS = {
18
+ singular: { id: "plugin.media.media.singular", message: "Asset" },
19
+ plural: { id: "plugin.media.media.plural", message: "Media" },
20
+ addNew: { id: "plugin.media.media.addNew", message: "Add New" },
21
+ addNewItem: {
22
+ id: "plugin.media.media.addNewItem",
23
+ message: "Upload Media",
24
+ },
25
+ editItem: {
26
+ id: "plugin.media.media.editItem",
27
+ message: "Edit Media",
28
+ },
29
+ newItem: {
30
+ id: "plugin.media.media.newItem",
31
+ message: "New Media",
32
+ },
33
+ viewItem: {
34
+ id: "plugin.media.media.viewItem",
35
+ message: "View Media",
36
+ },
37
+ searchItems: {
38
+ id: "plugin.media.media.searchItems",
39
+ message: "Search media…",
40
+ },
41
+ notFound: {
42
+ id: "plugin.media.media.notFound",
43
+ message: "No media yet",
44
+ },
45
+ notFoundInTrash: {
46
+ id: "plugin.media.media.notFoundInTrash",
47
+ message: "Trash is empty",
48
+ },
49
+ allItems: {
50
+ id: "plugin.media.media.allItems",
51
+ message: "All Media",
52
+ },
53
+ untitledItem: {
54
+ id: "plugin.media.media.untitledItem",
55
+ message: "Untitled Media",
56
+ },
57
+ moveToTrash: {
58
+ id: "plugin.media.media.moveToTrash",
59
+ message: "Move media to trash?",
60
+ },
61
+ };
62
+ // Admin-page chrome (separate from per-type labels because the
63
+ // "Media Library" page heading isn't an entry-type label).
64
+ const MEDIA_LIBRARY_LABEL = {
65
+ id: "plugin.media.adminPage.title",
66
+ message: "Media Library",
67
+ };
68
+ const LIBRARY_GROUP_LABEL = {
69
+ id: "core.adminNav.library",
70
+ message: "Library",
71
+ };
72
+ // Re-export the canonical list from `media-blocks.ts` (server-clean).
73
+ export { mediaBlocks } from "./media-blocks.js";
74
+ /**
75
+ * Lexical path the plumix vite plugin uses to locate this package's admin
76
+ * chunk. Stays inside the consumer's project tree (`node_modules/...`) so
77
+ * the build-time containment check passes; esbuild follows the symlink to
78
+ * the workspace source under the hood. Override `adminEntry` in
79
+ * `definePlugin` if your install layout differs (e.g. pnpm hoisting tweaks).
80
+ */
81
+ const ADMIN_ENTRY_PATH = "node_modules/@plumix/plugin-media/dist/admin/index.js";
82
+ /**
83
+ * Media plugin — registers the `media` entry type, the `media.*` RPC
84
+ * router, and the admin Media Library page.
85
+ *
86
+ * Uploads use the two-phase signed-URL flow:
87
+ *
88
+ * 1. `media.createUploadUrl({ filename, contentType, size })` →
89
+ * creates a `draft` entry, returns a presigned PUT URL the browser
90
+ * uses to upload bytes directly to storage.
91
+ * 2. `media.confirm({ id })` → flips the entry from `draft` to
92
+ * `published` once the browser-PUT succeeded.
93
+ *
94
+ * Bytes never traverse the worker. Requires a storage adapter whose
95
+ * `presignPut` is implemented (e.g. `r2({ s3: { ... } })`).
96
+ *
97
+ * Pairs with `imageDelivery:` for on-the-fly resizing.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { media } from "@plumix/plugin-media";
102
+ *
103
+ * plumix({
104
+ * storage: r2({
105
+ * binding: "MEDIA",
106
+ * publicUrlBase: "https://media.example.com",
107
+ * s3: {
108
+ * bucket: "plumix-media",
109
+ * accountId: env.CF_ACCOUNT_ID,
110
+ * accessKeyId: env.R2_ACCESS_KEY_ID,
111
+ * secretAccessKey: env.R2_SECRET_ACCESS_KEY,
112
+ * },
113
+ * }),
114
+ * imageDelivery: images({ zone: "media.example.com" }),
115
+ * plugins: [media()],
116
+ * });
117
+ * ```
118
+ */
119
+ export function media(options = {}) {
120
+ const acceptedTypes = options.acceptedTypes ?? DEFAULT_ACCEPTED_TYPES;
121
+ const maxUploadSize = options.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
122
+ return definePlugin("media", (ctx) => {
123
+ // Media blocks contributed under the `media/` namespace. Order is
124
+ // just for readability; the registry is keyed by name.
125
+ ctx.registerBlock(imageBlock);
126
+ ctx.registerBlock(fileBlock);
127
+ ctx.registerEntryType("media", {
128
+ label: MEDIA_LABELS.plural,
129
+ labels: MEDIA_LABELS,
130
+ description: "Uploaded files — images, video, documents",
131
+ supports: ["title", "excerpt"],
132
+ // `isPublic: false` cascades to `showUI: false` and
133
+ // `showInSidebar: false`. Both are load-bearing here:
134
+ // the plugin renders its own admin page (registered below)
135
+ // — we don't want the generic entries list, sidebar item,
136
+ // or dashboard quick-card auto-registered too.
137
+ isPublic: false,
138
+ excludeFromGenericRpc: false,
139
+ hasArchive: false,
140
+ menuIcon: "image",
141
+ });
142
+ ctx.registerRpcRouter(createMediaRouter({ acceptedTypes, maxUploadSize }));
143
+ // Media's own MCP read tools, contributed through the plugin seam —
144
+ // they ship from the plugin that owns media, not core.
145
+ ctx.registerMcpTool(mediaListTool);
146
+ ctx.registerMcpTool(mediaGetTool);
147
+ // Reference-field surface: any `media({ ... })` field calls
148
+ // through `lookup.list({ kind: "media", ids })` for write
149
+ // validation + read-time orphan filter, and through the same
150
+ // RPC for picker label resolution. Capability matches the
151
+ // existing media library page gate.
152
+ ctx.registerLookupAdapter({
153
+ kind: "media",
154
+ adapter: mediaLookupAdapter,
155
+ capability: "entry:media:read",
156
+ });
157
+ // Worker-routed upload fallback. `media.createUploadUrl` returns
158
+ // this URL when `storage.presignPut` isn't configured (e.g. the
159
+ // R2 binding is attached but no S3 credentials are wired up). The
160
+ // browser PUTs bytes here, the dispatcher authenticates the
161
+ // session, and the handler streams them through to storage.
162
+ ctx.registerRoute({
163
+ method: "PUT",
164
+ path: "/upload/*",
165
+ auth: "authenticated",
166
+ handler: handleWorkerUpload,
167
+ });
168
+ // Worker-proxied media serve. When the storage adapter has no
169
+ // public URL base (private bucket without a custom domain),
170
+ // `r2.url()` returns a relative URL pointing here. Public so
171
+ // published media can be embedded in pages/posts.
172
+ ctx.registerRoute({
173
+ method: "GET",
174
+ path: "/serve/*",
175
+ auth: "public",
176
+ handler: handleMediaServe,
177
+ });
178
+ ctx.registerAdminPage({
179
+ path: "/media",
180
+ title: MEDIA_LIBRARY_LABEL,
181
+ capability: "entry:media:read",
182
+ nav: {
183
+ // Own group between Entries (priority 100) and Taxonomies
184
+ // (priority 200). Media isn't a content surface like Posts/
185
+ // Pages — putting it under "Entries" reads as nesting, which
186
+ // doesn't match the WordPress mental model.
187
+ group: {
188
+ id: "library",
189
+ label: LIBRARY_GROUP_LABEL,
190
+ priority: 150,
191
+ },
192
+ label: MEDIA_LIBRARY_LABEL,
193
+ order: 50,
194
+ keywords: [
195
+ { id: "plugin.media.keyword.images", message: "images" },
196
+ { id: "plugin.media.keyword.files", message: "files" },
197
+ { id: "plugin.media.keyword.uploads", message: "uploads" },
198
+ { id: "plugin.media.keyword.photos", message: "photos" },
199
+ { id: "plugin.media.keyword.assets", message: "assets" },
200
+ ],
201
+ },
202
+ component: "MediaLibrary",
203
+ });
204
+ }, {
205
+ adminEntry: ADMIN_ENTRY_PATH,
206
+ // Browser-safe block specs for the editor canvas (the iframe ships no
207
+ // server code). Mirrors the `ctx.registerBlock` calls above.
208
+ editorBlocksModule: "@plumix/plugin-media/blocks",
209
+ i18n: {
210
+ sourceLocale: "en",
211
+ locales: ["en"],
212
+ catalogPath: "./locales",
213
+ },
214
+ });
215
+ }
216
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAIvD,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC,wCAAwC;AACxC,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAExD,oEAAoE;AACpE,mEAAmE;AACnE,uEAAuE;AACvE,SAAS;AACT,MAAM,YAAY,GAAG;IACnB,QAAQ,EAAE,EAAE,EAAE,EAAE,6BAA6B,EAAE,OAAO,EAAE,OAAO,EAAE;IACjE,MAAM,EAAE,EAAE,EAAE,EAAE,2BAA2B,EAAE,OAAO,EAAE,OAAO,EAAE;IAC7D,MAAM,EAAE,EAAE,EAAE,EAAE,2BAA2B,EAAE,OAAO,EAAE,SAAS,EAAE;IAC/D,UAAU,EAAE;QACV,EAAE,EAAE,+BAA+B;QACnC,OAAO,EAAE,cAAc;KACxB;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,6BAA6B;QACjC,OAAO,EAAE,YAAY;KACtB;IACD,OAAO,EAAE;QACP,EAAE,EAAE,4BAA4B;QAChC,OAAO,EAAE,WAAW;KACrB;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,6BAA6B;QACjC,OAAO,EAAE,YAAY;KACtB;IACD,WAAW,EAAE;QACX,EAAE,EAAE,gCAAgC;QACpC,OAAO,EAAE,eAAe;KACzB;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,6BAA6B;QACjC,OAAO,EAAE,cAAc;KACxB;IACD,eAAe,EAAE;QACf,EAAE,EAAE,oCAAoC;QACxC,OAAO,EAAE,gBAAgB;KAC1B;IACD,QAAQ,EAAE;QACR,EAAE,EAAE,6BAA6B;QACjC,OAAO,EAAE,WAAW;KACrB;IACD,YAAY,EAAE;QACZ,EAAE,EAAE,iCAAiC;QACrC,OAAO,EAAE,gBAAgB;KAC1B;IACD,WAAW,EAAE;QACX,EAAE,EAAE,gCAAgC;QACpC,OAAO,EAAE,sBAAsB;KAChC;CACwB,CAAC;AAE5B,+DAA+D;AAC/D,2DAA2D;AAC3D,MAAM,mBAAmB,GAAU;IACjC,EAAE,EAAE,8BAA8B;IAClC,OAAO,EAAE,eAAe;CACzB,CAAC;AACF,MAAM,mBAAmB,GAAU;IACjC,EAAE,EAAE,uBAAuB;IAC3B,OAAO,EAAE,SAAS;CACnB,CAAC;AAEF,sEAAsE;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmBhD;;;;;;GAMG;AACH,MAAM,gBAAgB,GACpB,uDAAuD,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,KAAK,CACnB,UAA8B,EAAE;IAEhC,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAC;IACtE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,uBAAuB,CAAC;IAEvE,OAAO,YAAY,CACjB,OAAO,EACP,CAAC,GAAG,EAAE,EAAE;QACN,kEAAkE;QAClE,uDAAuD;QACvD,GAAG,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAC9B,GAAG,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAE7B,GAAG,CAAC,iBAAiB,CAAC,OAAO,EAAE;YAC7B,KAAK,EAAE,YAAY,CAAC,MAAM;YAC1B,MAAM,EAAE,YAAY;YACpB,WAAW,EAAE,2CAA2C;YACxD,QAAQ,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;YAC9B,oDAAoD;YACpD,sDAAsD;YACtD,2DAA2D;YAC3D,0DAA0D;YAC1D,+CAA+C;YAC/C,QAAQ,EAAE,KAAK;YACf,qBAAqB,EAAE,KAAK;YAC5B,UAAU,EAAE,KAAK;YACjB,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC;QAEH,GAAG,CAAC,iBAAiB,CACnB,iBAAiB,CAAC,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC,CACpD,CAAC;QAEF,oEAAoE;QACpE,uDAAuD;QACvD,GAAG,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QACnC,GAAG,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QAElC,4DAA4D;QAC5D,0DAA0D;QAC1D,6DAA6D;QAC7D,0DAA0D;QAC1D,oCAAoC;QACpC,GAAG,CAAC,qBAAqB,CAAC;YACxB,IAAI,EAAE,OAAO;YACb,OAAO,EAAE,kBAAkB;YAC3B,UAAU,EAAE,kBAAkB;SAC/B,CAAC,CAAC;QAEH,iEAAiE;QACjE,gEAAgE;QAChE,kEAAkE;QAClE,4DAA4D;QAC5D,4DAA4D;QAC5D,GAAG,CAAC,aAAa,CAAC;YAChB,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,WAAW;YACjB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,kBAAkB;SAC5B,CAAC,CAAC;QAEH,8DAA8D;QAC9D,4DAA4D;QAC5D,6DAA6D;QAC7D,kDAAkD;QAClD,GAAG,CAAC,aAAa,CAAC;YAChB,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,gBAAgB;SAC1B,CAAC,CAAC;QAEH,GAAG,CAAC,iBAAiB,CAAC;YACpB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,mBAAmB;YAC1B,UAAU,EAAE,kBAAkB;YAC9B,GAAG,EAAE;gBACH,0DAA0D;gBAC1D,4DAA4D;gBAC5D,6DAA6D;gBAC7D,4CAA4C;gBAC5C,KAAK,EAAE;oBACL,EAAE,EAAE,SAAS;oBACb,KAAK,EAAE,mBAAmB;oBAC1B,QAAQ,EAAE,GAAG;iBACd;gBACD,KAAK,EAAE,mBAAmB;gBAC1B,KAAK,EAAE,EAAE;gBACT,QAAQ,EAAE;oBACR,EAAE,EAAE,EAAE,6BAA6B,EAAE,OAAO,EAAE,QAAQ,EAAE;oBACxD,EAAE,EAAE,EAAE,4BAA4B,EAAE,OAAO,EAAE,OAAO,EAAE;oBACtD,EAAE,EAAE,EAAE,8BAA8B,EAAE,OAAO,EAAE,SAAS,EAAE;oBAC1D,EAAE,EAAE,EAAE,6BAA6B,EAAE,OAAO,EAAE,QAAQ,EAAE;oBACxD,EAAE,EAAE,EAAE,6BAA6B,EAAE,OAAO,EAAE,QAAQ,EAAE;iBACzD;aACF;YACD,SAAS,EAAE,cAAc;SAC1B,CAAC,CAAC;IACL,CAAC,EACD;QACE,UAAU,EAAE,gBAAgB;QAC5B,sEAAsE;QACtE,6DAA6D;QAC7D,kBAAkB,EAAE,6BAA6B;QACjD,IAAI,EAAE;YACJ,YAAY,EAAE,IAAI;YAClB,OAAO,EAAE,CAAC,IAAI,CAAC;YACf,WAAW,EAAE,WAAW;SACzB;KACF,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,43 @@
1
+ import type { LookupAdapter } from "plumix/plugin";
2
+ /**
3
+ * Public scope shape for the `media` reference field. Carried on the
4
+ * field's `referenceTarget.scope`; the media `LookupAdapter` consumes
5
+ * it for write-time validation, picker filtering, and read-time
6
+ * orphan resolution.
7
+ *
8
+ * `accept` is either a single MIME prefix string (`"image/"` matches
9
+ * `image/png`, `image/jpeg`, …) or a readonly array of exact MIME
10
+ * matches. Drop HTML's `image/*` wildcard syntax — the trailing slash
11
+ * already conveys "category" and avoids the `image/*` vs `image/`
12
+ * ambiguity.
13
+ */
14
+ export interface MediaFieldScope {
15
+ readonly accept?: string | readonly string[];
16
+ }
17
+ /**
18
+ * Server-side adapter for the `media` reference field. Storage is the
19
+ * cached-object shape (`{ id, mime, filename }`) — the meta pipeline
20
+ * pulls `cached` from `LookupResult` and merges it into the stored
21
+ * value on every write so reads render thumbnails without an extra
22
+ * resolve round-trip.
23
+ *
24
+ * Queries:
25
+ * - `list({ ids })`: PK lookup via `inArray(entries.id, …)` + the
26
+ * `entries_type_status_published_idx` partition. SQLite picks the
27
+ * PK as the most selective; the type+status filter is post-applied
28
+ * to a small rowset.
29
+ * - `list({ query })`: leverages `entries_type_status_published_idx`
30
+ * for the `(type, status)` prefix; ordered by `desc(publishedAt)`
31
+ * matches the index's third column (no extra sort).
32
+ * - MIME `accept` filter: post-filter in JS against the meta JSON.
33
+ * The narrowed rowset (type+status or PK) keeps this tractable
34
+ * without a generated column.
35
+ *
36
+ * Drafts and trashed media are invisible to the picker — only
37
+ * `status = "published"` rows surface. A draft media entry exists
38
+ * between `media.createUploadUrl` (writes a draft row) and
39
+ * `media.confirm` (flips to published) — referencing one would point
40
+ * at an asset whose bytes haven't been verified.
41
+ */
42
+ export declare const mediaLookupAdapter: LookupAdapter<MediaFieldScope>;
43
+ //# sourceMappingURL=lookup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lookup.d.ts","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAqB,MAAM,eAAe,CAAC;AAgBtE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;CAC9C;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,eAAe,CA4D7D,CAAC"}
package/dist/lookup.js ADDED
@@ -0,0 +1,154 @@
1
+ import { and, desc, entries, eq, inArray, like, sql } from "plumix/plugin";
2
+ import { parseMediaMeta } from "./meta.js";
3
+ const MEDIA_ENTRY_TYPE = "media";
4
+ const DEFAULT_LIST_LIMIT = 24;
5
+ const MAX_LIST_LIMIT = 100;
6
+ const MEDIA_ROW_COLUMNS = {
7
+ id: entries.id,
8
+ title: entries.title,
9
+ meta: entries.meta,
10
+ };
11
+ /**
12
+ * Server-side adapter for the `media` reference field. Storage is the
13
+ * cached-object shape (`{ id, mime, filename }`) — the meta pipeline
14
+ * pulls `cached` from `LookupResult` and merges it into the stored
15
+ * value on every write so reads render thumbnails without an extra
16
+ * resolve round-trip.
17
+ *
18
+ * Queries:
19
+ * - `list({ ids })`: PK lookup via `inArray(entries.id, …)` + the
20
+ * `entries_type_status_published_idx` partition. SQLite picks the
21
+ * PK as the most selective; the type+status filter is post-applied
22
+ * to a small rowset.
23
+ * - `list({ query })`: leverages `entries_type_status_published_idx`
24
+ * for the `(type, status)` prefix; ordered by `desc(publishedAt)`
25
+ * matches the index's third column (no extra sort).
26
+ * - MIME `accept` filter: post-filter in JS against the meta JSON.
27
+ * The narrowed rowset (type+status or PK) keeps this tractable
28
+ * without a generated column.
29
+ *
30
+ * Drafts and trashed media are invisible to the picker — only
31
+ * `status = "published"` rows surface. A draft media entry exists
32
+ * between `media.createUploadUrl` (writes a draft row) and
33
+ * `media.confirm` (flips to published) — referencing one would point
34
+ * at an asset whose bytes haven't been verified.
35
+ */
36
+ export const mediaLookupAdapter = {
37
+ async list(ctx, options) {
38
+ const conditions = [
39
+ eq(entries.type, MEDIA_ENTRY_TYPE),
40
+ eq(entries.status, "published"),
41
+ ];
42
+ const acceptCondition = buildAcceptCondition(options.scope?.accept);
43
+ if (acceptCondition)
44
+ conditions.push(acceptCondition);
45
+ let limit;
46
+ if (options.ids !== undefined) {
47
+ const numericIds = options.ids
48
+ .map((id) => parseMediaId(id))
49
+ .filter((id) => id !== null);
50
+ if (numericIds.length === 0)
51
+ return [];
52
+ conditions.push(inArray(entries.id, numericIds));
53
+ limit = numericIds.length;
54
+ }
55
+ else {
56
+ const trimmedQuery = options.query?.trim();
57
+ if (trimmedQuery) {
58
+ conditions.push(like(entries.title, `%${trimmedQuery}%`));
59
+ }
60
+ limit = clampLimit(options.limit);
61
+ }
62
+ const rows = await ctx.db
63
+ .select(MEDIA_ROW_COLUMNS)
64
+ .from(entries)
65
+ .where(and(...conditions))
66
+ .orderBy(desc(entries.publishedAt), desc(entries.id))
67
+ .limit(limit);
68
+ const results = [];
69
+ for (const row of rows) {
70
+ const meta = parseMediaMeta(row.meta);
71
+ if (!meta)
72
+ continue;
73
+ results.push(toLookupResult(row.id, row.title, meta.mime));
74
+ }
75
+ return results;
76
+ },
77
+ async resolve(ctx, id, scope) {
78
+ const numericId = parseMediaId(id);
79
+ if (numericId === null)
80
+ return null;
81
+ const conditions = [
82
+ eq(entries.id, numericId),
83
+ eq(entries.type, MEDIA_ENTRY_TYPE),
84
+ eq(entries.status, "published"),
85
+ ];
86
+ const acceptCondition = buildAcceptCondition(scope?.accept);
87
+ if (acceptCondition)
88
+ conditions.push(acceptCondition);
89
+ const [row] = await ctx.db
90
+ .select(MEDIA_ROW_COLUMNS)
91
+ .from(entries)
92
+ .where(and(...conditions))
93
+ .limit(1);
94
+ if (!row)
95
+ return null;
96
+ const meta = parseMediaMeta(row.meta);
97
+ if (!meta)
98
+ return null;
99
+ return toLookupResult(row.id, row.title, meta.mime);
100
+ },
101
+ };
102
+ function parseMediaId(id) {
103
+ if (!/^[1-9]\d{0,15}$/.test(id))
104
+ return null;
105
+ const parsed = Number(id);
106
+ return Number.isSafeInteger(parsed) ? parsed : null;
107
+ }
108
+ function clampLimit(requested) {
109
+ if (requested === undefined)
110
+ return DEFAULT_LIST_LIMIT;
111
+ if (!Number.isFinite(requested) || requested <= 0)
112
+ return DEFAULT_LIST_LIMIT;
113
+ return Math.min(Math.floor(requested), MAX_LIST_LIMIT);
114
+ }
115
+ // Translate the `accept` scope into a SQL predicate against the
116
+ // extracted JSON `mime` field. Pushing the filter into SQL means the
117
+ // browse path's LIMIT clause counts only rows that pass the accept
118
+ // filter — post-filtering in JS would silently under-fill the picker
119
+ // grid for fields whose accept rejects a chunk of the recent uploads.
120
+ //
121
+ // `meta.mime` isn't indexed (no generated column), but the type+status
122
+ // filter has already narrowed the rowset before this predicate is
123
+ // applied, so the cost is bounded. Real MIME strings don't contain
124
+ // `%`/`_`, and plugin-supplied prefixes are trusted (set at field-
125
+ // build time, not user input), so no LIKE escaping needed.
126
+ function buildAcceptCondition(accept) {
127
+ if (accept === undefined)
128
+ return undefined;
129
+ const mimeExpr = sql `json_extract(${entries.meta}, '$.mime')`;
130
+ if (typeof accept === "string") {
131
+ if (accept === "")
132
+ return undefined;
133
+ return like(mimeExpr, `${accept}%`);
134
+ }
135
+ if (accept.length === 0)
136
+ return undefined;
137
+ return inArray(mimeExpr, accept);
138
+ }
139
+ function toLookupResult(id, title, mime) {
140
+ // Mirror the entry adapter's `null` contract for empty/whitespace
141
+ // titles so admin pickers render a localized "Untitled" descriptor
142
+ // rather than an empty `<p>`. `cached.filename` keeps the raw value
143
+ // (deletion-resilient consumers may still want the original token).
144
+ const trimmedTitle = title.trim();
145
+ const label = trimmedTitle !== "" ? trimmedTitle : null;
146
+ return {
147
+ id: String(id),
148
+ label,
149
+ targetType: MEDIA_ENTRY_TYPE,
150
+ subtitle: mime,
151
+ cached: { mime, filename: title },
152
+ };
153
+ }
154
+ //# sourceMappingURL=lookup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lookup.js","sourceRoot":"","sources":["../src/lookup.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /** Buffer size we ask storage to read for the magic-byte sniff. */
2
+ export declare const MAGIC_BYTE_SAMPLE_SIZE = 64;
3
+ export declare function looksLikeMime(bytes: Uint8Array, claimedMime: string): boolean;
4
+ //# sourceMappingURL=magic-bytes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-bytes.d.ts","sourceRoot":"","sources":["../src/magic-bytes.ts"],"names":[],"mappings":"AAgIA,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAI7E"}