@socialtip/asset-proxy-url-parser 0.5.1 → 0.7.1

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.
package/src/info-parse.ts DELETED
@@ -1,275 +0,0 @@
1
- import { z } from "zod/v4";
2
- import { decryptSourceUrl } from "./crypto.js";
3
- import { HTTPError } from "./error.js";
4
-
5
- const zBool = z
6
- .string()
7
- .transform((v) => v === "1" || v === "t" || v === "true");
8
-
9
- const INFO_SHORTHANDS: Record<string, string> = {
10
- cs: "colorspace",
11
- b: "bands",
12
- sf: "sample_format",
13
- pn: "pages_number",
14
- a: "alpha",
15
- p: "palette",
16
- avg: "average",
17
- dc: "dominant_colors",
18
- bh: "blurhash",
19
- chs: "calc_hashsums",
20
- pg: "page",
21
- };
22
-
23
- const hashsumType = z.enum(["md5", "sha1", "sha256", "sha512"]);
24
-
25
- const rawInfoOptionsSchema = z.object({
26
- exif: zBool.optional(),
27
- iptc: zBool.optional(),
28
- xmp: zBool.optional(),
29
- colorspace: zBool.optional(),
30
- bands: zBool.optional(),
31
- sample_format: zBool.optional(),
32
- pages_number: zBool.optional(),
33
- alpha: zBool.optional(),
34
- palette: z.coerce.number().int().min(0).max(256).optional(),
35
- average: z
36
- .string()
37
- .transform((v) => {
38
- const parts = v.split(":");
39
- const enabled =
40
- parts[0] === "1" || parts[0] === "t" || parts[0] === "true";
41
- const ignoreTransparent =
42
- parts[1] === "1" || parts[1] === "t" || parts[1] === "true";
43
- return enabled ? { ignoreTransparent } : undefined;
44
- })
45
- .optional(),
46
- dominant_colors: zBool.optional(),
47
- blurhash: z
48
- .string()
49
- .transform((v) => {
50
- const [x, y] = v.split(":").map(Number);
51
- if (!x || !y) return undefined;
52
- return { xComponents: x, yComponents: y };
53
- })
54
- .optional(),
55
- calc_hashsums: z
56
- .string()
57
- .transform((v) =>
58
- v.split(":").filter((t) => hashsumType.safeParse(t).success),
59
- )
60
- .optional(),
61
- page: z.coerce.number().int().min(0).optional(),
62
- });
63
-
64
- const infoOptionsSchema = rawInfoOptionsSchema.transform((data) => ({
65
- exif: data.exif,
66
- iptc: data.iptc,
67
- xmp: data.xmp,
68
- colorspace: data.colorspace,
69
- bands: data.bands,
70
- sampleFormat: data.sample_format,
71
- pagesNumber: data.pages_number,
72
- alpha: data.alpha,
73
- palette: data.palette,
74
- average: data.average,
75
- dominantColors: data.dominant_colors,
76
- blurhash: data.blurhash,
77
- calcHashsums: data.calc_hashsums,
78
- page: data.page,
79
- }));
80
-
81
- /** Parsed info endpoint options that control which additional metadata is returned. */
82
- export interface InfoOptions {
83
- /** Include EXIF metadata in the response. */
84
- exif?: boolean;
85
- /** Include IPTC metadata in the response. */
86
- iptc?: boolean;
87
- /** Include XMP metadata organised by namespace in the response. */
88
- xmp?: boolean;
89
- /** Include the image colour space (e.g. `gbr`, `bt709`). */
90
- colorspace?: boolean;
91
- /** Include the number of image bands/channels. */
92
- bands?: boolean;
93
- /** Include the sample format (uchar, ushort, float). */
94
- sampleFormat?: boolean;
95
- /** Include the page/frame count. */
96
- pagesNumber?: boolean;
97
- /** Include alpha channel information. */
98
- alpha?: boolean;
99
- /** Return an RGBA colour palette with this many colours (2-256). 0 or undefined to disable. */
100
- palette?: number;
101
- /** Return the average image colour. */
102
- average?: { ignoreTransparent: boolean };
103
- /** Return six dominant colour categories (vibrant, muted, light/dark variants). */
104
- dominantColors?: boolean;
105
- /** Return a BlurHash string with the given x and y components. */
106
- blurhash?: { xComponents: number; yComponents: number };
107
- /** Calculate and return hashsums of the source file. List of types: md5, sha1, sha256, sha512. */
108
- calcHashsums?: Array<"md5" | "sha1" | "sha256" | "sha512">;
109
- /** Which page to analyse for multi-page images (0-indexed). */
110
- page?: number;
111
- }
112
-
113
- /** Zod schema for runtime validation of parsed info options. The `InfoOptions` interface is the authoritative type definition; this schema validates against it at compile time via `satisfies`. */
114
- export const parsedInfoOptionsSchema = z.object({
115
- exif: z.boolean().optional(),
116
- iptc: z.boolean().optional(),
117
- xmp: z.boolean().optional(),
118
- colorspace: z.boolean().optional(),
119
- bands: z.boolean().optional(),
120
- sampleFormat: z.boolean().optional(),
121
- pagesNumber: z.boolean().optional(),
122
- alpha: z.boolean().optional(),
123
- palette: z.number().optional(),
124
- average: z.object({ ignoreTransparent: z.boolean() }).optional(),
125
- dominantColors: z.boolean().optional(),
126
- blurhash: z
127
- .object({ xComponents: z.number(), yComponents: z.number() })
128
- .optional(),
129
- calcHashsums: z.array(hashsumType).optional(),
130
- page: z.number().optional(),
131
- }) satisfies z.ZodType<InfoOptions>;
132
-
133
- const CONTROL_SHORTHANDS: Record<string, string> = {
134
- exp: "expires",
135
- hs: "hashsum",
136
- msfs: "max_src_file_size",
137
- msr: "max_src_resolution",
138
- cb: "cache_buster",
139
- };
140
-
141
- const ALL_SHORTHANDS: Record<string, string> = {
142
- ...INFO_SHORTHANDS,
143
- ...CONTROL_SHORTHANDS,
144
- };
145
-
146
- const ALL_OPTION_NAMES = new Set([
147
- ...Object.keys(ALL_SHORTHANDS),
148
- ...Object.values(ALL_SHORTHANDS),
149
- "exif",
150
- "iptc",
151
- "xmp",
152
- ]);
153
-
154
- const rawControlOptionsSchema = z.object({
155
- expires: z.coerce.number().int().optional(),
156
- hashsum: z
157
- .string()
158
- .transform((v) => {
159
- const idx = v.indexOf(":");
160
- if (idx === -1) return undefined;
161
- return { type: v.slice(0, idx), hash: v.slice(idx + 1) };
162
- })
163
- .optional(),
164
- max_src_file_size: z.coerce.number().int().positive().optional(),
165
- max_src_resolution: z.coerce.number().positive().optional(),
166
- cache_buster: z.string().optional(),
167
- });
168
-
169
- const controlOptionsSchema = rawControlOptionsSchema.transform((data) => ({
170
- expires: data.expires,
171
- hashsum: data.hashsum,
172
- maxSrcFileSize: data.max_src_file_size,
173
- maxSrcResolution: data.max_src_resolution,
174
- }));
175
-
176
- /** Control options parsed from an info URL (security, limits). */
177
- export interface ControlOptions {
178
- /** Unix timestamp after which the URL returns 404. */
179
- expires?: number;
180
- /** Expected checksum of the source file. */
181
- hashsum?: { type: string; hash: string };
182
- /** Max source file size in bytes. */
183
- maxSrcFileSize?: number;
184
- /** Max source resolution in megapixels. */
185
- maxSrcResolution?: number;
186
- }
187
-
188
- /** Zod schema for runtime validation of parsed control options. The `ControlOptions` interface is the authoritative type definition; this schema validates against it at compile time via `satisfies`. */
189
- export const parsedControlOptionsSchema = z.object({
190
- expires: z.number().optional(),
191
- hashsum: z.object({ type: z.string(), hash: z.string() }).optional(),
192
- maxSrcFileSize: z.number().optional(),
193
- maxSrcResolution: z.number().optional(),
194
- }) satisfies z.ZodType<ControlOptions>;
195
-
196
- /** Parsed result from an info URL. */
197
- export interface ParsedInfoUrl extends ControlOptions {
198
- sourceUrl: string;
199
- infoOptions: InfoOptions;
200
- }
201
-
202
- export interface InfoParseOptions {
203
- encryptionKey?: Buffer;
204
- }
205
-
206
- /** Parses an info URL path (after signature has been stripped). Extracts the source URL, info options, and control options (expires, hashsum, source limits). */
207
- export function parseInfoUrl(
208
- path: string,
209
- options?: InfoParseOptions,
210
- ): ParsedInfoUrl {
211
- const withoutPrefix = path.replace(/^\//, "");
212
-
213
- let optionsPart: string;
214
- let sourceUrl: string;
215
- let encrypted = false;
216
-
217
- const plainIdx = withoutPrefix.indexOf("plain/");
218
- const encIdx = withoutPrefix.indexOf("enc/");
219
-
220
- if (plainIdx !== -1 && (encIdx === -1 || plainIdx <= encIdx)) {
221
- optionsPart = withoutPrefix.slice(0, plainIdx).replace(/\/$/, "");
222
- sourceUrl = withoutPrefix.slice(plainIdx + "plain/".length);
223
- } else if (encIdx !== -1) {
224
- optionsPart = withoutPrefix.slice(0, encIdx).replace(/\/$/, "");
225
- sourceUrl = withoutPrefix.slice(encIdx + "enc/".length);
226
- encrypted = true;
227
- } else {
228
- throw new HTTPError(
229
- "Unsupported URL format: expected /plain/ or /enc/ source URL",
230
- { code: "BAD_REQUEST" },
231
- );
232
- }
233
-
234
- if (!sourceUrl) {
235
- throw new HTTPError("Missing source URL", { code: "BAD_REQUEST" });
236
- }
237
-
238
- if (encrypted) {
239
- if (!options?.encryptionKey) {
240
- throw new HTTPError(
241
- "Encrypted source URLs are not supported: no encryption key provided",
242
- { code: "BAD_REQUEST" },
243
- );
244
- }
245
- sourceUrl = decryptSourceUrl(sourceUrl, options.encryptionKey);
246
- }
247
-
248
- const controlCanonicals = new Set(Object.values(CONTROL_SHORTHANDS));
249
- const infoRaw: Record<string, string> = {};
250
- const controlRaw: Record<string, string> = {};
251
-
252
- for (const seg of optionsPart.split("/").filter(Boolean)) {
253
- const colonIdx = seg.indexOf(":");
254
- const name = colonIdx === -1 ? seg : seg.slice(0, colonIdx);
255
- const value = colonIdx === -1 ? "" : seg.slice(colonIdx + 1);
256
- const canonical = ALL_SHORTHANDS[name] ?? name;
257
-
258
- if (!ALL_OPTION_NAMES.has(name)) continue;
259
-
260
- if (controlCanonicals.has(canonical)) {
261
- controlRaw[canonical] = value;
262
- } else {
263
- infoRaw[canonical] = value;
264
- }
265
- }
266
-
267
- const infoOptions = parsedInfoOptionsSchema.parse(
268
- infoOptionsSchema.parse(infoRaw),
269
- );
270
- const control = parsedControlOptionsSchema.parse(
271
- controlOptionsSchema.parse(controlRaw),
272
- );
273
-
274
- return { sourceUrl, infoOptions, ...control };
275
- }