@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,107 @@
1
+ // Lightweight magic-byte detector for the MIME types the media plugin
2
+ // accepts. The presigned PUT pins a `Content-Type` the bucket stores
3
+ // verbatim — but R2 doesn't sniff bytes, so a malicious user can claim
4
+ // `image/png` and upload arbitrary content (HTML, executables, polyglot
5
+ // payloads). We re-fetch the first ~64 bytes after upload and verify
6
+ // the header matches the claimed type. On mismatch the caller deletes
7
+ // the object and rejects the confirm.
8
+ //
9
+ // Returns `true` when the buffer's prefix matches the claimed MIME, OR
10
+ // when we have no matcher for that MIME (e.g. `text/plain` — no reliable
11
+ // magic bytes). Returns `false` only when a known signature mismatches.
12
+ const sigAt = (offset, ...sig) => (b) => {
13
+ if (b.length < offset + sig.length)
14
+ return false;
15
+ for (let i = 0; i < sig.length; i++) {
16
+ if (b[offset + i] !== sig[i])
17
+ return false;
18
+ }
19
+ return true;
20
+ };
21
+ const startsWith = (...sig) => sigAt(0, ...sig);
22
+ const anyOf = (...m) => (b) => m.some((f) => f(b));
23
+ // `RIFF...<tag>` containers — `tag` lives at offset 8 after `RIFF` + size.
24
+ // Used by image/webp + audio/wav.
25
+ const riffWith = (...tag) => {
26
+ const head = startsWith(0x52, 0x49, 0x46, 0x46); // RIFF
27
+ const tail = sigAt(8, ...tag);
28
+ return (b) => head(b) && tail(b);
29
+ };
30
+ const isXmlOrSvg = (b) => {
31
+ // SVG/XML: tolerate UTF-8 BOM + leading whitespace, then `<?xml` or `<svg`.
32
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(b.subarray(0, Math.min(b.length, 256)));
33
+ // Strip a leading UTF-8 BOM (U+FEFF) before checking the prefix.
34
+ const trimmed = text.replace(/^\uFEFF/, "").trimStart();
35
+ return trimmed.startsWith("<?xml") || trimmed.startsWith("<svg");
36
+ };
37
+ // `text/*` payloads must not look like HTML. Authors stash HTML in
38
+ // `.txt` to bypass the image allowlist and have CDNs serve it as
39
+ // content. Magic bytes can't fully prove "this is plain text", but we
40
+ // can reject obvious HTML markers and require UTF-8 decodability.
41
+ const isPlainText = (b) => {
42
+ let text;
43
+ try {
44
+ text = new TextDecoder("utf-8", { fatal: true }).decode(b.subarray(0, Math.min(b.length, 256)));
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ const trimmed = text
50
+ .replace(/^\uFEFF/, "")
51
+ .trimStart()
52
+ .toLowerCase();
53
+ return !(trimmed.startsWith("<!doctype") ||
54
+ trimmed.startsWith("<html") ||
55
+ trimmed.startsWith("<?xml") ||
56
+ trimmed.startsWith("<svg") ||
57
+ trimmed.startsWith("<script") ||
58
+ trimmed.startsWith("<iframe"));
59
+ };
60
+ // `ftyp` at offset 4 covers MP4, MOV, AVIF, HEIC, etc. We only need to
61
+ // detect "is this an isobmff container", not which brand exactly.
62
+ const isISOBMFF = sigAt(4, 0x66, 0x74, 0x79, 0x70);
63
+ // PKZIP local-file header — also matches DOCX/XLSX/PPTX (they're zip
64
+ // archives) and odt/ods/odp. We sniff "is this a zip container", NOT
65
+ // "is this the right Office subtype". A `.docx` claimed as xlsx still
66
+ // passes — verifying via the OOXML `[Content_Types].xml` is out of
67
+ // scope; the inline-safe allowlist excludes all of these so the
68
+ // serve route force-downloads regardless of subtype.
69
+ const isZipContainer = startsWith(0x50, 0x4b, 0x03, 0x04);
70
+ // OLE2 compound document — legacy Office (.doc/.xls/.ppt).
71
+ const isOle2 = startsWith(0xd0, 0xcf, 0x11, 0xe0);
72
+ const MATCHERS = {
73
+ "image/jpeg": startsWith(0xff, 0xd8, 0xff),
74
+ "image/png": startsWith(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a),
75
+ "image/gif": anyOf(startsWith(0x47, 0x49, 0x46, 0x38, 0x37, 0x61), // GIF87a
76
+ startsWith(0x47, 0x49, 0x46, 0x38, 0x39, 0x61)),
77
+ "image/webp": riffWith(0x57, 0x45, 0x42, 0x50), // RIFF…WEBP
78
+ "image/avif": isISOBMFF,
79
+ "image/svg+xml": isXmlOrSvg,
80
+ "application/pdf": startsWith(0x25, 0x50, 0x44, 0x46), // %PDF
81
+ "application/zip": isZipContainer,
82
+ "application/msword": isOle2,
83
+ "application/vnd.ms-excel": isOle2,
84
+ "application/vnd.ms-powerpoint": isOle2,
85
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": isZipContainer,
86
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": isZipContainer,
87
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": isZipContainer,
88
+ "audio/mpeg": anyOf(startsWith(0x49, 0x44, 0x33), // ID3v2 tag
89
+ startsWith(0xff, 0xfb), startsWith(0xff, 0xf3), startsWith(0xff, 0xf2)),
90
+ "audio/wav": riffWith(0x57, 0x41, 0x56, 0x45), // RIFF…WAVE
91
+ "audio/ogg": startsWith(0x4f, 0x67, 0x67, 0x53), // OggS
92
+ "video/mp4": isISOBMFF,
93
+ "video/webm": startsWith(0x1a, 0x45, 0xdf, 0xa3), // EBML
94
+ "video/quicktime": isISOBMFF,
95
+ "text/plain": isPlainText,
96
+ "text/markdown": isPlainText,
97
+ "text/csv": isPlainText,
98
+ };
99
+ /** Buffer size we ask storage to read for the magic-byte sniff. */
100
+ export const MAGIC_BYTE_SAMPLE_SIZE = 64;
101
+ export function looksLikeMime(bytes, claimedMime) {
102
+ const matcher = MATCHERS[claimedMime];
103
+ if (!matcher)
104
+ return true; // unknown / text — can't verify; accept.
105
+ return matcher(bytes);
106
+ }
107
+ //# sourceMappingURL=magic-bytes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magic-bytes.js","sourceRoot":"","sources":["../src/magic-bytes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { McpTool } from "plumix/plugin";
2
+ import * as v from "valibot";
3
+ import { mediaListInputSchema } from "./read-service.js";
4
+ declare const mediaGetInputSchema: v.ObjectSchema<{
5
+ readonly id: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
6
+ }, undefined>;
7
+ export declare const mediaListTool: McpTool<typeof mediaListInputSchema>;
8
+ export declare const mediaGetTool: McpTool<typeof mediaGetInputSchema>;
9
+ export {};
10
+ //# sourceMappingURL=mcp-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-tools.d.ts","sourceRoot":"","sources":["../src/mcp-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAE7B,OAAO,EAGL,oBAAoB,EAErB,MAAM,mBAAmB,CAAC;AAE3B,QAAA,MAAM,mBAAmB;;aAEvB,CAAC;AAYH,eAAO,MAAM,aAAa,EAAE,OAAO,CAAC,OAAO,oBAAoB,CAY9D,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,OAAO,mBAAmB,CAW5D,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { McpToolError } from "plumix/plugin";
2
+ import * as v from "valibot";
3
+ import { getMedia, listMedia, mediaListInputSchema, MediaReadError, } from "./read-service.js";
4
+ const mediaGetInputSchema = v.object({
5
+ id: v.pipe(v.number(), v.integer(), v.minValue(1)),
6
+ });
7
+ function asMcpToolError(error) {
8
+ if (!(error instanceof MediaReadError))
9
+ throw error;
10
+ switch (error.data.code) {
11
+ case "not_found":
12
+ return McpToolError.notFound(error.message);
13
+ case "forbidden":
14
+ return McpToolError.forbidden(error.message);
15
+ }
16
+ }
17
+ export const mediaListTool = {
18
+ name: "media_list",
19
+ description: "List published media (images, files) with optional MIME and filename filters, paginated. Clamped to what your token may read.",
20
+ inputSchema: mediaListInputSchema,
21
+ async run(ctx, input) {
22
+ try {
23
+ return await listMedia(ctx, input);
24
+ }
25
+ catch (error) {
26
+ throw asMcpToolError(error);
27
+ }
28
+ },
29
+ };
30
+ export const mediaGetTool = {
31
+ name: "media_get",
32
+ description: "Read a single published media item in full by its id.",
33
+ inputSchema: mediaGetInputSchema,
34
+ async run(ctx, input) {
35
+ try {
36
+ return await getMedia(ctx, input);
37
+ }
38
+ catch (error) {
39
+ throw asMcpToolError(error);
40
+ }
41
+ },
42
+ };
43
+ //# sourceMappingURL=mcp-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-tools.js","sourceRoot":"","sources":["../src/mcp-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAE7B,OAAO,EACL,QAAQ,EACR,SAAS,EACT,oBAAoB,EACpB,cAAc,GACf,MAAM,mBAAmB,CAAC;AAE3B,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;CACnD,CAAC,CAAC;AAEH,SAAS,cAAc,CAAC,KAAc;IACpC,IAAI,CAAC,CAAC,KAAK,YAAY,cAAc,CAAC;QAAE,MAAM,KAAK,CAAC;IACpD,QAAQ,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,WAAW;YACd,OAAO,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9C,KAAK,WAAW;YACd,OAAO,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAyC;IACjE,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,+HAA+H;IACjI,WAAW,EAAE,oBAAoB;IACjC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK;QAClB,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAwC;IAC/D,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,uDAAuD;IACpE,WAAW,EAAE,mBAAmB;IAChC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK;QAClB,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,cAAc,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { BlockSpec } from "plumix/blocks";
2
+ export declare const mediaBlocks: readonly BlockSpec[];
3
+ //# sourceMappingURL=media-blocks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-blocks.d.ts","sourceRoot":"","sources":["../src/media-blocks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAQ/C,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAG1C,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { fileBlock } from "./blocks/file/index.js";
2
+ import { imageBlock } from "./blocks/image/index.js";
3
+ // Browser-clean canonical list — no server-side imports — so the
4
+ // admin chunk can pull it without transitively dragging in the
5
+ // plugin's RPC routes, upload handler, or `definePlugin`.
6
+ export const mediaBlocks = Object.freeze([
7
+ imageBlock,
8
+ fileBlock,
9
+ ]);
10
+ //# sourceMappingURL=media-blocks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-blocks.js","sourceRoot":"","sources":["../src/media-blocks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,iEAAiE;AACjE,+DAA+D;AAC/D,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAyB,MAAM,CAAC,MAAM,CAAC;IAC7D,UAAU;IACV,SAAS;CACV,CAAC,CAAC"}
package/dist/meta.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ interface MediaMeta {
2
+ readonly mime: string;
3
+ readonly size: number;
4
+ readonly storageKey: string;
5
+ readonly originalName: string | null;
6
+ readonly alt: string | null;
7
+ readonly width: number | null;
8
+ readonly height: number | null;
9
+ }
10
+ export declare function parseMediaMeta(raw: unknown): MediaMeta | null;
11
+ export {};
12
+ //# sourceMappingURL=meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAAA,UAAU,SAAS;IACjB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAG5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAmB7D"}
package/dist/meta.js ADDED
@@ -0,0 +1,20 @@
1
+ export function parseMediaMeta(raw) {
2
+ if (!raw || typeof raw !== "object")
3
+ return null;
4
+ const r = raw;
5
+ if (typeof r.storageKey !== "string" ||
6
+ typeof r.mime !== "string" ||
7
+ typeof r.size !== "number") {
8
+ return null;
9
+ }
10
+ return {
11
+ storageKey: r.storageKey,
12
+ mime: r.mime,
13
+ size: r.size,
14
+ originalName: typeof r.originalName === "string" ? r.originalName : null,
15
+ alt: typeof r.alt === "string" ? r.alt : null,
16
+ width: typeof r.width === "number" ? r.width : null,
17
+ height: typeof r.height === "number" ? r.height : null,
18
+ };
19
+ }
20
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../src/meta.ts"],"names":[],"mappings":"AAYA,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,IACE,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ;QAChC,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;QAC1B,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAC1B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO;QACL,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,YAAY,EAAE,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI;QACxE,GAAG,EAAE,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;QAC7C,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI;QACnD,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;KACvD,CAAC;AACJ,CAAC"}
package/dist/mime.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_ACCEPTED_TYPES: readonly string[];
2
+ export declare function extensionForMime(mime: string): string | undefined;
3
+ //# sourceMappingURL=mime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AA0DA,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAEnD,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE"}
package/dist/mime.js ADDED
@@ -0,0 +1,58 @@
1
+ // Single source of truth for the MIME types this plugin accepts and the
2
+ // extension we'll use for each in storage keys. Both `DEFAULT_ACCEPTED_TYPES`
3
+ // (the upload allowlist) and the rpc layer's extension picker derive from
4
+ // here, so the two stay in lockstep.
5
+ //
6
+ // SECURITY:
7
+ // - The bucket stores whatever bytes the client uploads; the worker
8
+ // verifies them against the claimed mime via magic-byte sniff in
9
+ // `media.confirm` (defense-in-depth — we don't trust the signed
10
+ // Content-Type header at upload time).
11
+ // - `image/svg+xml` is intentionally NOT in the default allowlist:
12
+ // SVG is XML and can carry executable `<script>`/`onload=` payloads.
13
+ // A magic-byte sniff can confirm the bytes start with `<svg`/`<?xml`
14
+ // but cannot prove they're safe to render. Operators who need SVG
15
+ // uploads must opt in explicitly via `media({ acceptedTypes: [...] })`
16
+ // AND serve from a separate cookie domain (so any embedded script
17
+ // can't reach admin cookies).
18
+ // - Same precaution applies to `text/*` mimes: bytes are stored verbatim,
19
+ // and a malicious actor could disguise HTML/JS as `text/plain`. Always
20
+ // serve uploads from a domain distinct from the admin.
21
+ const MEDIA_MIME_REGISTRY = {
22
+ // images
23
+ "image/jpeg": "jpg",
24
+ "image/png": "png",
25
+ "image/gif": "gif",
26
+ "image/webp": "webp",
27
+ "image/avif": "avif",
28
+ // SVG kept in the registry so opt-in consumers get the right extension,
29
+ // but excluded from DEFAULT_ACCEPTED_TYPES below.
30
+ "image/svg+xml": "svg",
31
+ // documents
32
+ "application/pdf": "pdf",
33
+ "application/msword": "doc",
34
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
35
+ "application/vnd.ms-excel": "xls",
36
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
37
+ "application/vnd.ms-powerpoint": "ppt",
38
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
39
+ "text/plain": "txt",
40
+ "text/markdown": "md",
41
+ "text/csv": "csv",
42
+ // audio
43
+ "audio/mpeg": "mp3",
44
+ "audio/wav": "wav",
45
+ "audio/ogg": "ogg",
46
+ // video
47
+ "video/mp4": "mp4",
48
+ "video/webm": "webm",
49
+ "video/quicktime": "mov",
50
+ // archives
51
+ "application/zip": "zip",
52
+ };
53
+ const SVG_MIME = "image/svg+xml";
54
+ export const DEFAULT_ACCEPTED_TYPES = Object.freeze(Object.keys(MEDIA_MIME_REGISTRY).filter((m) => m !== SVG_MIME));
55
+ export function extensionForMime(mime) {
56
+ return MEDIA_MIME_REGISTRY[mime];
57
+ }
58
+ //# sourceMappingURL=mime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mime.js","sourceRoot":"","sources":["../src/mime.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,8EAA8E;AAC9E,0EAA0E;AAC1E,qCAAqC;AACrC,EAAE;AACF,YAAY;AACZ,oEAAoE;AACpE,mEAAmE;AACnE,kEAAkE;AAClE,yCAAyC;AACzC,mEAAmE;AACnE,uEAAuE;AACvE,uEAAuE;AACvE,oEAAoE;AACpE,yEAAyE;AACzE,oEAAoE;AACpE,gCAAgC;AAChC,0EAA0E;AAC1E,yEAAyE;AACzE,yDAAyD;AAEzD,MAAM,mBAAmB,GAAqC;IAC5D,SAAS;IACT,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;IACpB,wEAAwE;IACxE,kDAAkD;IAClD,eAAe,EAAE,KAAK;IACtB,YAAY;IACZ,iBAAiB,EAAE,KAAK;IACxB,oBAAoB,EAAE,KAAK;IAC3B,yEAAyE,EACvE,MAAM;IACR,0BAA0B,EAAE,KAAK;IACjC,mEAAmE,EAAE,MAAM;IAC3E,+BAA+B,EAAE,KAAK;IACtC,2EAA2E,EACzE,MAAM;IACR,YAAY,EAAE,KAAK;IACnB,eAAe,EAAE,IAAI;IACrB,UAAU,EAAE,KAAK;IACjB,QAAQ;IACR,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,QAAQ;IACR,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,iBAAiB,EAAE,KAAK;IACxB,WAAW;IACX,iBAAiB,EAAE,KAAK;CACzB,CAAC;AAEF,MAAM,QAAQ,GAAG,eAAe,CAAC;AAEjC,MAAM,CAAC,MAAM,sBAAsB,GAAsB,MAAM,CAAC,MAAM,CACpE,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAC/D,CAAC;AAEF,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,66 @@
1
+ import type { AppContext } from "plumix/plugin";
2
+ import * as v from "valibot";
3
+ export declare const MEDIA_ENTRY_TYPE = "media";
4
+ export declare const mediaListInputSchema: v.ObjectSchema<{
5
+ readonly limit: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 100, undefined>]>, 24>;
6
+ readonly offset: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, 0>;
7
+ readonly accept: v.OptionalSchema<v.UnionSchema<[v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 64, undefined>]>, v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MaxLengthAction<string, 64, undefined>]>, undefined>, v.MaxLengthAction<string[], 32, undefined>]>], undefined>, undefined>;
8
+ readonly search: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.TrimAction, v.MaxLengthAction<string, 200, undefined>]>, undefined>;
9
+ }, undefined>;
10
+ type MediaListInput = v.InferOutput<typeof mediaListInputSchema>;
11
+ interface MediaItem {
12
+ readonly id: number;
13
+ readonly title: string;
14
+ readonly mime: string;
15
+ readonly size: number;
16
+ readonly storageKey: string;
17
+ readonly alt: string | null;
18
+ readonly url: string;
19
+ readonly thumbnailUrl: string;
20
+ readonly uploadedAt: string;
21
+ readonly uploadedById: number;
22
+ readonly width: number | null;
23
+ readonly height: number | null;
24
+ }
25
+ interface MediaListResult {
26
+ readonly items: readonly MediaItem[];
27
+ readonly hasMore: boolean;
28
+ }
29
+ /**
30
+ * Domain error the media read path throws. Each transport maps it to its own
31
+ * shape (oRPC typed error / MCP envelope), keeping the service transport-neutral.
32
+ */
33
+ export declare class MediaReadError extends Error {
34
+ readonly data: {
35
+ readonly code: "forbidden";
36
+ readonly capability: string;
37
+ } | {
38
+ readonly code: "not_found";
39
+ readonly id: number;
40
+ };
41
+ private constructor();
42
+ static forbidden(capability: string): MediaReadError;
43
+ static notFound(id: number): MediaReadError;
44
+ }
45
+ /**
46
+ * List published media the caller may read, with MIME / filename filters and
47
+ * offset pagination. Fetches one extra row to report `hasMore` without a
48
+ * second COUNT query. Throws {@link MediaReadError}.
49
+ */
50
+ export declare function listMedia(ctx: AppContext, input: MediaListInput): Promise<MediaListResult>;
51
+ /**
52
+ * Read a single published media item by id. A missing row, a non-media row, a
53
+ * draft, or corrupt meta all collapse to `not_found` so existence stays hidden.
54
+ */
55
+ export declare function getMedia(ctx: AppContext, input: {
56
+ readonly id: number;
57
+ }): Promise<MediaItem>;
58
+ export declare function thumbnailFor(ctx: AppContext, url: string, mime: string): string;
59
+ /**
60
+ * Resolve a publicly-fetchable URL for a media row. Prefers the storage
61
+ * adapter's native URL; falls back to the worker-proxied serve route keyed on
62
+ * the entry id (not the storageKey — the serve route enforces `published`).
63
+ */
64
+ export declare function resolveMediaUrl(storage: NonNullable<AppContext["storage"]>, storageKey: string, entryId: number, basePath?: string): Promise<string>;
65
+ export {};
66
+ //# sourceMappingURL=read-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-service.d.ts","sourceRoot":"","sources":["../src/read-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAO,MAAM,eAAe,CAAC;AAYrD,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAI7B,eAAO,MAAM,gBAAgB,UAAU,CAAC;AASxC,eAAO,MAAM,oBAAoB;;;;;aAgB/B,CAAC;AAEH,KAAK,cAAc,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAEjE,UAAU,SAAS;IACjB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,qBAAa,cAAe,SAAQ,KAAK;IAKvC,QAAQ,CAAC,IAAI,EACT;QAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAC3D;QAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAExD,OAAO;IAKP,MAAM,CAAC,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc;IAOpD,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,cAAc;CAM5C;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,eAAe,CAAC,CAoC1B;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,UAAU,EACf,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC7B,OAAO,CAAC,SAAS,CAAC,CAapB;AA4BD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,MAAM,CAOR;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EAC3C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,QAAQ,SAAK,GACZ,OAAO,CAAC,MAAM,CAAC,CAKjB"}
@@ -0,0 +1,156 @@
1
+ import { and, desc, entries, eq, escapeLikePattern, inArray, like, sql, withBasePath, } from "plumix/plugin";
2
+ import * as v from "valibot";
3
+ import { parseMediaMeta } from "./meta.js";
4
+ export const MEDIA_ENTRY_TYPE = "media";
5
+ const MEDIA_READ_CAPABILITY = "entry:media:read";
6
+ const DEFAULT_PAGE_SIZE = 24;
7
+ const MAX_PAGE_SIZE = 100;
8
+ // 320px-wide auto-format thumbnail — enough for the 160px admin card at 2× DPI.
9
+ const THUMBNAIL_OPTS = { width: 320, format: "auto", fit: "cover" };
10
+ export const mediaListInputSchema = v.object({
11
+ limit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(MAX_PAGE_SIZE)), DEFAULT_PAGE_SIZE),
12
+ offset: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0)), 0),
13
+ // MIME filter: string form (`"image/"`) does a `LIKE 'image/%'` prefix match;
14
+ // array form matches the listed MIMEs exactly. Pushed into SQL so `limit`
15
+ // counts only matching rows.
16
+ accept: v.optional(v.union([
17
+ v.pipe(v.string(), v.maxLength(64)),
18
+ v.pipe(v.array(v.pipe(v.string(), v.maxLength(64))), v.maxLength(32)),
19
+ ])),
20
+ search: v.optional(v.pipe(v.string(), v.trim(), v.maxLength(200))),
21
+ });
22
+ /**
23
+ * Domain error the media read path throws. Each transport maps it to its own
24
+ * shape (oRPC typed error / MCP envelope), keeping the service transport-neutral.
25
+ */
26
+ export class MediaReadError extends Error {
27
+ static {
28
+ MediaReadError.prototype.name = "MediaReadError";
29
+ }
30
+ data;
31
+ constructor(data, message) {
32
+ super(message);
33
+ this.data = data;
34
+ }
35
+ static forbidden(capability) {
36
+ return new MediaReadError({ code: "forbidden", capability }, `missing capability: ${capability}`);
37
+ }
38
+ static notFound(id) {
39
+ return new MediaReadError({ code: "not_found", id }, `media ${id} not found`);
40
+ }
41
+ }
42
+ /**
43
+ * List published media the caller may read, with MIME / filename filters and
44
+ * offset pagination. Fetches one extra row to report `hasMore` without a
45
+ * second COUNT query. Throws {@link MediaReadError}.
46
+ */
47
+ export async function listMedia(ctx, input) {
48
+ if (!ctx.auth.can(MEDIA_READ_CAPABILITY)) {
49
+ throw MediaReadError.forbidden(MEDIA_READ_CAPABILITY);
50
+ }
51
+ const conditions = [
52
+ eq(entries.type, MEDIA_ENTRY_TYPE),
53
+ eq(entries.status, "published"),
54
+ ];
55
+ const acceptCondition = buildAcceptCondition(input.accept);
56
+ if (acceptCondition)
57
+ conditions.push(acceptCondition);
58
+ if (input.search) {
59
+ const pattern = `%${escapeLikePattern(input.search)}%`;
60
+ conditions.push(sql `(
61
+ ${entries.title} LIKE ${pattern} ESCAPE '\\'
62
+ OR COALESCE(json_extract(${entries.meta}, '$.alt'), '') LIKE ${pattern} ESCAPE '\\'
63
+ )`);
64
+ }
65
+ const rows = await ctx.db
66
+ .select()
67
+ .from(entries)
68
+ .where(and(...conditions))
69
+ .orderBy(desc(entries.updatedAt), desc(entries.id))
70
+ .limit(input.limit + 1)
71
+ .offset(input.offset);
72
+ const hasMore = rows.length > input.limit;
73
+ const visibleRows = hasMore ? rows.slice(0, input.limit) : rows;
74
+ const items = [];
75
+ for (const row of visibleRows) {
76
+ const meta = parseMediaMeta(row.meta);
77
+ if (!meta)
78
+ continue;
79
+ items.push(await buildMediaItem(ctx, row, meta));
80
+ }
81
+ return { items, hasMore };
82
+ }
83
+ /**
84
+ * Read a single published media item by id. A missing row, a non-media row, a
85
+ * draft, or corrupt meta all collapse to `not_found` so existence stays hidden.
86
+ */
87
+ export async function getMedia(ctx, input) {
88
+ if (!ctx.auth.can(MEDIA_READ_CAPABILITY)) {
89
+ throw MediaReadError.forbidden(MEDIA_READ_CAPABILITY);
90
+ }
91
+ const row = await ctx.db.query.entries.findFirst({
92
+ where: eq(entries.id, input.id),
93
+ });
94
+ const meta = row ? parseMediaMeta(row.meta) : null;
95
+ if (row?.type !== MEDIA_ENTRY_TYPE || row.status !== "published" || !meta) {
96
+ throw MediaReadError.notFound(input.id);
97
+ }
98
+ return buildMediaItem(ctx, row, meta);
99
+ }
100
+ async function buildMediaItem(ctx, row, meta) {
101
+ const url = ctx.storage
102
+ ? await resolveMediaUrl(ctx.storage, meta.storageKey, row.id, ctx.basePath)
103
+ : meta.storageKey;
104
+ // `publishedAt` is the "uploaded on" source of truth; fall back to createdAt.
105
+ const uploadedAt = (row.publishedAt ?? row.createdAt).toISOString();
106
+ return {
107
+ id: row.id,
108
+ title: row.title,
109
+ mime: meta.mime,
110
+ size: meta.size,
111
+ storageKey: meta.storageKey,
112
+ alt: meta.alt,
113
+ url,
114
+ thumbnailUrl: thumbnailFor(ctx, url, meta.mime),
115
+ uploadedAt,
116
+ uploadedById: row.authorId,
117
+ width: meta.width,
118
+ height: meta.height,
119
+ };
120
+ }
121
+ export function thumbnailFor(ctx, url, mime) {
122
+ // imageDelivery transforms need an absolute, publicly-reachable source — the
123
+ // transform CDN fetches it itself. The worker-proxied serve fallback is
124
+ // relative, so skip transforms there.
125
+ if (!mime.startsWith("image/") || !ctx.imageDelivery)
126
+ return url;
127
+ if (!url.startsWith("http://") && !url.startsWith("https://"))
128
+ return url;
129
+ return ctx.imageDelivery.url(url, THUMBNAIL_OPTS);
130
+ }
131
+ /**
132
+ * Resolve a publicly-fetchable URL for a media row. Prefers the storage
133
+ * adapter's native URL; falls back to the worker-proxied serve route keyed on
134
+ * the entry id (not the storageKey — the serve route enforces `published`).
135
+ */
136
+ export async function resolveMediaUrl(storage, storageKey, entryId, basePath = "") {
137
+ const direct = await storage.url(storageKey);
138
+ return (direct ?? withBasePath(`/_plumix/media/serve/${String(entryId)}`, basePath));
139
+ }
140
+ // Translate `accept` into a SQL predicate against the JSON `mime` field. Real
141
+ // MIME strings don't contain LIKE wildcards and the input is plugin/agent
142
+ // supplied, so no escaping is needed.
143
+ function buildAcceptCondition(accept) {
144
+ if (accept === undefined)
145
+ return undefined;
146
+ const mimeExpr = sql `json_extract(${entries.meta}, '$.mime')`;
147
+ if (typeof accept === "string") {
148
+ if (accept === "")
149
+ return undefined;
150
+ return like(mimeExpr, `${accept}%`);
151
+ }
152
+ if (accept.length === 0)
153
+ return undefined;
154
+ return inArray(mimeExpr, accept);
155
+ }
156
+ //# sourceMappingURL=read-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-service.js","sourceRoot":"","sources":["../src/read-service.ts"],"names":[],"mappings":"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"}
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ interface MediaRpcOptions {
2
+ readonly acceptedTypes: readonly string[];
3
+ readonly maxUploadSize: number;
4
+ }
5
+ export declare function createMediaRouter(options: MediaRpcOptions): Record<string, unknown>;
6
+ export {};
7
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AA2BA,UAAU,eAAe;IACvB,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAChC;AAyED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,eAAe,GACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0UzB"}