@shapio/local 0.5.8 → 0.5.10

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.
@@ -1 +1 @@
1
- {"version":3,"file":"errors-nQvt9bs7.js","names":[],"sources":["../../../apps/api/src/media/keys.ts","../../../apps/api/src/media/errors.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\n\nexport type MediaVisibility = 'public' | 'private';\n\nconst MAX_FILENAME_LENGTH = 200;\nconst MAX_SLUG_LENGTH = 80;\n/** Characters Windows, macOS or shells treat specially, plus every control character. */\n// eslint-disable-next-line no-control-regex\nconst UNSAFE_FILENAME_CHARACTERS = /[\\u0000-\\u001f\\u007f<>:\"/\\\\|?*]+/g;\nconst SEGMENT = '[A-Za-z0-9][A-Za-z0-9._-]{0,127}';\nconst STORAGE_KEY_PATTERN = new RegExp(`^(public|private)(/${SEGMENT}){2,5}$`);\n\nconst splitExtension = (name: string): { stem: string; extension: string } => {\n const dot = name.lastIndexOf('.');\n return dot > 0 && name.length - dot <= 16\n ? { stem: name.slice(0, dot), extension: name.slice(dot) }\n : { stem: name, extension: '' };\n};\n\n/**\n * The display name kept for an upload: the last path segment, NFC-normalised, without control or\n * path characters, at most 200 characters with the extension preserved. Never used as a storage path.\n */\nexport const sanitizeFilename = (input: string): string => {\n const base = input.split(/[/\\\\]/).pop() ?? '';\n const cleaned = base\n .normalize('NFC')\n .replace(UNSAFE_FILENAME_CHARACTERS, '-')\n .replace(/\\s+/g, ' ')\n .replace(/^[\\s.-]+|[\\s.]+$/g, '');\n if (cleaned === '') {\n return 'file';\n }\n if (cleaned.length <= MAX_FILENAME_LENGTH) {\n return cleaned;\n }\n const { stem, extension } = splitExtension(cleaned);\n return `${stem.slice(0, MAX_FILENAME_LENGTH - extension.length)}${extension}`;\n};\n\n/** An ASCII, lower-case, URL-safe version of a file name for storage keys: `Été 2026.JPG` → `ete-2026.jpg`. */\nexport const toStorageSlug = (filename: string): string => {\n const { stem, extension } = splitExtension(filename);\n const slugify = (value: string) =>\n value\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n const slugStem = slugify(stem).slice(0, MAX_SLUG_LENGTH) || 'file';\n const slugExtension = slugify(extension).slice(0, 15);\n return slugExtension ? `${slugStem}.${slugExtension}` : slugStem;\n};\n\n/** Relative keys only, made of safe segments under a visibility prefix: no `..`, no absolute paths. */\nexport const isValidStorageKey = (key: string): boolean => STORAGE_KEY_PATTERN.test(key);\n\n/**\n * `public/<assetId>/<token>/<slug>`. The visibility prefix lets an operator expose only `public/` through a\n * CDN or bucket policy; the random token gives every upload (and replacement) a new, cacheable URL.\n */\nexport const buildAssetKey = (visibility: MediaVisibility, assetId: string, filename: string): string =>\n `${visibility}/${assetId}/${randomBytes(12).toString('hex')}/${toStorageSlug(filename)}`;\n\n/** Variants sit beside their original: `public/<assetId>/<token>/v/<name>.<extension>`. */\nexport const buildVariantKey = (assetKey: string, name: string, extension: string): string =>\n `${assetKey.slice(0, assetKey.lastIndexOf('/'))}/v/${name}.${extension}`;\n\n/** The same key under the other visibility prefix (visibility changes move objects). */\nexport const withVisibility = (key: string, visibility: MediaVisibility): string =>\n `${visibility}${key.slice(key.indexOf('/'))}`;\n\nexport const visibilityOfKey = (key: string): MediaVisibility | undefined => {\n const prefix = key.slice(0, key.indexOf('/'));\n return prefix === 'public' || prefix === 'private' ? prefix : undefined;\n};\n\n/** Percent-encodes each segment for use in a URL path. */\nexport const encodeKeyPath = (key: string): string => key.split('/').map(encodeURIComponent).join('/');\n","/** Thrown by adapters when a key has no object, so callers can tell \"missing\" from \"storage is down\". */\nexport class ObjectNotFoundError extends Error {\n constructor(key: string, options?: ErrorOptions) {\n super(`No stored object at ${key}`, options);\n this.name = 'ObjectNotFoundError';\n }\n}\n\nexport class InvalidStorageKeyError extends Error {\n constructor(key: string) {\n super(`Not a valid media storage key: ${JSON.stringify(key)}`);\n this.name = 'InvalidStorageKeyError';\n }\n}\n"],"mappings":";;AAUA,MAAM,sBAAsB,IAAI,OAAO,4DAAsC;;AA8C7E,MAAa,qBAAqB,QAAyB,oBAAoB,KAAK,GAAG;;AAcvF,MAAa,kBAAkB,KAAa,eAC1C,GAAG,aAAa,IAAI,MAAM,IAAI,QAAQ,GAAG,CAAC;AAE5C,MAAa,mBAAmB,QAA6C;CAC3E,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,CAAC;CAC5C,OAAO,WAAW,YAAY,WAAW,YAAY,SAAS,KAAA;AAChE;;AAGA,MAAa,iBAAiB,QAAwB,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,KAAK,GAAG;;;;AC9ErG,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,KAAa,SAAwB;EAC/C,MAAM,uBAAuB,OAAO,OAAO;EAC3C,KAAK,OAAO;CACd;AACF;AAEA,IAAa,yBAAb,cAA4C,MAAM;CAChD,YAAY,KAAa;EACvB,MAAM,kCAAkC,KAAK,UAAU,GAAG,GAAG;EAC7D,KAAK,OAAO;CACd;AACF"}
1
+ {"version":3,"file":"errors-nQvt9bs7.js","names":[],"sources":["../../../apps/api/src/media/keys.ts","../../../apps/api/src/media/errors.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\n\nexport type MediaVisibility = 'public' | 'private';\n\nconst MAX_FILENAME_LENGTH = 200;\nconst MAX_SLUG_LENGTH = 80;\n/** Characters Windows, macOS or shells treat specially, plus every control character. */\n// eslint-disable-next-line no-control-regex\nconst UNSAFE_FILENAME_CHARACTERS = /[\\u0000-\\u001f\\u007f<>:\"/\\\\|?*]+/g;\nconst SEGMENT = '[A-Za-z0-9][A-Za-z0-9._-]{0,127}';\nconst STORAGE_KEY_PATTERN = new RegExp(`^(public|private)(/${SEGMENT}){2,5}$`);\n\nconst splitExtension = (name: string): { stem: string; extension: string } => {\n const dot = name.lastIndexOf('.');\n return dot > 0 && name.length - dot <= 16\n ? { stem: name.slice(0, dot), extension: name.slice(dot) }\n : { stem: name, extension: '' };\n};\n\n/**\n * The display name kept for an upload: the last path segment, NFC-normalized, without control or\n * path characters, at most 200 characters with the extension preserved. Never used as a storage path.\n */\nexport const sanitizeFilename = (input: string): string => {\n const base = input.split(/[/\\\\]/).pop() ?? '';\n const cleaned = base\n .normalize('NFC')\n .replace(UNSAFE_FILENAME_CHARACTERS, '-')\n .replace(/\\s+/g, ' ')\n .replace(/^[\\s.-]+|[\\s.]+$/g, '');\n if (cleaned === '') {\n return 'file';\n }\n if (cleaned.length <= MAX_FILENAME_LENGTH) {\n return cleaned;\n }\n const { stem, extension } = splitExtension(cleaned);\n return `${stem.slice(0, MAX_FILENAME_LENGTH - extension.length)}${extension}`;\n};\n\n/** An ASCII, lower-case, URL-safe version of a file name for storage keys: `Été 2026.JPG` → `ete-2026.jpg`. */\nexport const toStorageSlug = (filename: string): string => {\n const { stem, extension } = splitExtension(filename);\n const slugify = (value: string) =>\n value\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n const slugStem = slugify(stem).slice(0, MAX_SLUG_LENGTH) || 'file';\n const slugExtension = slugify(extension).slice(0, 15);\n return slugExtension ? `${slugStem}.${slugExtension}` : slugStem;\n};\n\n/** Relative keys only, made of safe segments under a visibility prefix: no `..`, no absolute paths. */\nexport const isValidStorageKey = (key: string): boolean => STORAGE_KEY_PATTERN.test(key);\n\n/**\n * `public/<assetId>/<token>/<slug>`. The visibility prefix lets an operator expose only `public/` through a\n * CDN or bucket policy; the random token gives every upload (and replacement) a new, cacheable URL.\n */\nexport const buildAssetKey = (visibility: MediaVisibility, assetId: string, filename: string): string =>\n `${visibility}/${assetId}/${randomBytes(12).toString('hex')}/${toStorageSlug(filename)}`;\n\n/** Variants sit beside their original: `public/<assetId>/<token>/v/<name>.<extension>`. */\nexport const buildVariantKey = (assetKey: string, name: string, extension: string): string =>\n `${assetKey.slice(0, assetKey.lastIndexOf('/'))}/v/${name}.${extension}`;\n\n/** The same key under the other visibility prefix (visibility changes move objects). */\nexport const withVisibility = (key: string, visibility: MediaVisibility): string =>\n `${visibility}${key.slice(key.indexOf('/'))}`;\n\nexport const visibilityOfKey = (key: string): MediaVisibility | undefined => {\n const prefix = key.slice(0, key.indexOf('/'));\n return prefix === 'public' || prefix === 'private' ? prefix : undefined;\n};\n\n/** Percent-encodes each segment for use in a URL path. */\nexport const encodeKeyPath = (key: string): string => key.split('/').map(encodeURIComponent).join('/');\n","/** Thrown by adapters when a key has no object, so callers can tell \"missing\" from \"storage is down\". */\nexport class ObjectNotFoundError extends Error {\n constructor(key: string, options?: ErrorOptions) {\n super(`No stored object at ${key}`, options);\n this.name = 'ObjectNotFoundError';\n }\n}\n\nexport class InvalidStorageKeyError extends Error {\n constructor(key: string) {\n super(`Not a valid media storage key: ${JSON.stringify(key)}`);\n this.name = 'InvalidStorageKeyError';\n }\n}\n"],"mappings":";;AAUA,MAAM,sBAAsB,IAAI,OAAO,4DAAsC;;AA8C7E,MAAa,qBAAqB,QAAyB,oBAAoB,KAAK,GAAG;;AAcvF,MAAa,kBAAkB,KAAa,eAC1C,GAAG,aAAa,IAAI,MAAM,IAAI,QAAQ,GAAG,CAAC;AAE5C,MAAa,mBAAmB,QAA6C;CAC3E,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI,QAAQ,GAAG,CAAC;CAC5C,OAAO,WAAW,YAAY,WAAW,YAAY,SAAS,KAAA;AAChE;;AAGA,MAAa,iBAAiB,QAAwB,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,KAAK,GAAG;;;;AC9ErG,IAAa,sBAAb,cAAyC,MAAM;CAC7C,YAAY,KAAa,SAAwB;EAC/C,MAAM,uBAAuB,OAAO,OAAO;EAC3C,KAAK,OAAO;CACd;AACF;AAEA,IAAa,yBAAb,cAA4C,MAAM;CAChD,YAAY,KAAa;EACvB,MAAM,kCAAkC,KAAK,UAAU,GAAG,GAAG;EAC7D,KAAK,OAAO;CACd;AACF"}