@shapio/local 0.5.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.
- package/LICENSE +201 -0
- package/dist/advisoryLocks-COlJQc1r.js +4181 -0
- package/dist/advisoryLocks-COlJQc1r.js.map +1 -0
- package/dist/errors-SK0PwPKC.js +26 -0
- package/dist/errors-SK0PwPKC.js.map +1 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +4481 -0
- package/dist/index.js.map +1 -0
- package/dist/migrator-CIKnDfZy.js +4152 -0
- package/dist/migrator-CIKnDfZy.js.map +1 -0
- package/dist/next.d.ts +10 -0
- package/dist/next.js +40 -0
- package/dist/next.js.map +1 -0
- package/dist/nodeRuntime-DzuiHUhY.js +12 -0
- package/dist/nodeRuntime-DzuiHUhY.js.map +1 -0
- package/dist/s3Adapter-BqCKtI3Y.js +143 -0
- package/dist/s3Adapter-BqCKtI3Y.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "node:crypto";
|
|
2
|
+
//#region ../../apps/api/src/media/keys.ts
|
|
3
|
+
const STORAGE_KEY_PATTERN = new RegExp(`^(public|private)(/[A-Za-z0-9][A-Za-z0-9._-]{0,127}){2,5}$`);
|
|
4
|
+
/** Relative keys only, made of safe segments under a visibility prefix: no `..`, no absolute paths. */
|
|
5
|
+
const isValidStorageKey = (key) => STORAGE_KEY_PATTERN.test(key);
|
|
6
|
+
/** Percent-encodes each segment for use in a URL path. */
|
|
7
|
+
const encodeKeyPath = (key) => key.split("/").map(encodeURIComponent).join("/");
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region ../../apps/api/src/media/errors.ts
|
|
10
|
+
/** Thrown by adapters when a key has no object, so callers can tell "missing" from "storage is down". */
|
|
11
|
+
var ObjectNotFoundError = class extends Error {
|
|
12
|
+
constructor(key, options) {
|
|
13
|
+
super(`No stored object at ${key}`, options);
|
|
14
|
+
this.name = "ObjectNotFoundError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var InvalidStorageKeyError = class extends Error {
|
|
18
|
+
constructor(key) {
|
|
19
|
+
super(`Not a valid media storage key: ${JSON.stringify(key)}`);
|
|
20
|
+
this.name = "InvalidStorageKeyError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { isValidStorageKey as i, ObjectNotFoundError as n, encodeKeyPath as r, InvalidStorageKeyError as t };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=errors-SK0PwPKC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-SK0PwPKC.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;;AAuBvF,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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ShapioApiError, ShapioDeliveryClient } from "@shapio/client";
|
|
2
|
+
import { Logger } from "pino";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The public types of `@shapio/local`, declared here so its declaration files stand alone (the runtime is
|
|
6
|
+
* bundled from `@shapio/cms` source, whose types are not part of this package). `typeChecks.ts` keeps them
|
|
7
|
+
* identical to the runtime's own.
|
|
8
|
+
*/
|
|
9
|
+
type LocalClientOptions = {
|
|
10
|
+
/** The Shapio database: a `postgres://` or `mysql://` URL. SQLite is single-process and refused. */
|
|
11
|
+
databaseUrl: string;
|
|
12
|
+
/** A delivery API token; without one, reads are anonymous (the site's `public` role). */
|
|
13
|
+
token?: string;
|
|
14
|
+
/** The site key; default the token's site, else the primary site. */
|
|
15
|
+
site?: string;
|
|
16
|
+
/** Pooled connections (default 4). Serverless functions: 1 or 2. `next build` opens one pool per worker. */
|
|
17
|
+
poolMax?: number;
|
|
18
|
+
/** How long a read waits for a pooled connection before it fails with 503 (default 10 s). */
|
|
19
|
+
acquireTimeoutMs?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Signs URLs of private media stored on the server's disk. Default: the secret the server generated and
|
|
22
|
+
* stored; pass the server's SESSION_SECRET when it sets one.
|
|
23
|
+
*/
|
|
24
|
+
signingSecret?: string;
|
|
25
|
+
/** Presigns URLs of private media on S3. Default: the AWS SDK's own credential chain. */
|
|
26
|
+
s3Credentials?: {
|
|
27
|
+
accessKeyId: string;
|
|
28
|
+
secretAccessKey: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* When the server runs another Shapio release: `throw` (default) a `ShapioVersionSkewError`, or `http`: read
|
|
32
|
+
* through the HTTP API at `fallbackUrl` until the releases match again.
|
|
33
|
+
*/
|
|
34
|
+
onVersionSkew?: 'throw' | 'http';
|
|
35
|
+
/** The Shapio server's URL (with BASE_PATH), for `onVersionSkew: 'http'`. */
|
|
36
|
+
fallbackUrl?: string;
|
|
37
|
+
/** Where failures are logged (pino). Default: warnings and errors to stdout. */
|
|
38
|
+
logger?: Logger;
|
|
39
|
+
};
|
|
40
|
+
/** The server runs another Shapio release (503 `VERSION_SKEW`); null when the database predates this one. */
|
|
41
|
+
type ShapioVersionSkewError$1 = ShapioApiError & {
|
|
42
|
+
readonly serverRelease: string | null;
|
|
43
|
+
readonly libraryRelease: string;
|
|
44
|
+
};
|
|
45
|
+
/** A local client cannot be created for these options (SQLite, an unknown URL, a second database). */
|
|
46
|
+
type DeliveryRuntimeError$1 = Error;
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/index.d.ts
|
|
49
|
+
/** The Shapio release this package reads; the server must run the same one. */
|
|
50
|
+
export declare const SHAPIO_VERSION: string;
|
|
51
|
+
/** Thrown (or 503 `VERSION_SKEW` caught as `ShapioApiError`) when the server runs another release. */
|
|
52
|
+
export declare const ShapioVersionSkewError: {
|
|
53
|
+
new (serverRelease: string | null, libraryRelease: string): ShapioVersionSkewError$1;
|
|
54
|
+
readonly prototype: ShapioVersionSkewError$1;
|
|
55
|
+
};
|
|
56
|
+
export type ShapioVersionSkewError = ShapioVersionSkewError$1;
|
|
57
|
+
/** Thrown by `createLocalClient` for options it cannot serve (SQLite, an unknown URL, a second database). */
|
|
58
|
+
export declare const DeliveryRuntimeError: {
|
|
59
|
+
new (message: string): DeliveryRuntimeError$1;
|
|
60
|
+
readonly prototype: DeliveryRuntimeError$1;
|
|
61
|
+
};
|
|
62
|
+
export type DeliveryRuntimeError = DeliveryRuntimeError$1;
|
|
63
|
+
export type LocalClient = ShapioDeliveryClient & {
|
|
64
|
+
/**
|
|
65
|
+
* Stops this client. The database pool is shared by every local client of the process and closes when the
|
|
66
|
+
* last one is closed. Long-running servers never need to call it; scripts and tests do.
|
|
67
|
+
*/
|
|
68
|
+
close: () => Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Shapio's delivery API as function calls in this process: the `delivery`, `site` and `snapshots` groups of
|
|
72
|
+
* `@shapio/client`, with the same paths, permissions, response shapes and `ShapioApiError`s, answered from the
|
|
73
|
+
* database without an HTTP hop. Reads as `token` (a delivery API token) or, without one, as an anonymous
|
|
74
|
+
* caller (the site's `public` role). Model changes made in the admin show on the next call.
|
|
75
|
+
*
|
|
76
|
+
* Needs PostgreSQL or MySQL, the Node.js runtime, and the same Shapio release as the server (`SHAPIO_VERSION`);
|
|
77
|
+
* see `onVersionSkew`. Nothing is read until the first call.
|
|
78
|
+
*/
|
|
79
|
+
export declare const createLocalClient: ({ token, site, ...runtimeOptions }: LocalClientOptions) => LocalClient;
|
|
80
|
+
//#endregion
|
|
81
|
+
export type { LocalClientOptions };
|
|
82
|
+
//# sourceMappingURL=index.d.ts.map
|