@ibanzajoe/uploader 0.3.0 → 1.2.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/README.md +46 -21
- package/dist/{chunk-EM6NZZSU.js → chunk-6AJTZNOI.js} +152 -51
- package/dist/chunk-6AJTZNOI.js.map +1 -0
- package/dist/chunk-DJK2SQ5I.js +56 -0
- package/dist/chunk-DJK2SQ5I.js.map +1 -0
- package/dist/client-CUmYuQ7Z.d.cts +154 -0
- package/dist/client-CUmYuQ7Z.d.ts +154 -0
- package/dist/core.cjs +158 -3
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -1
- package/dist/core.d.ts +2 -1
- package/dist/core.js +8 -4
- package/dist/index.cjs +158 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +79 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +54 -0
- package/dist/server.d.ts +54 -0
- package/dist/server.js +26 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +2 -2
- package/dist/transform-BTZ0kodO.d.cts +130 -0
- package/dist/transform-BTZ0kodO.d.ts +130 -0
- package/package.json +12 -2
- package/dist/chunk-EM6NZZSU.js.map +0 -1
- package/dist/transform-ybCOCWBG.d.cts +0 -225
- package/dist/transform-ybCOCWBG.d.ts +0 -225
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
getSignedDeliveryUrl: () => getSignedDeliveryUrl
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(server_exports);
|
|
26
|
+
var import_node_crypto = require("crypto");
|
|
27
|
+
|
|
28
|
+
// src/core/transform.ts
|
|
29
|
+
function serializeOp(op) {
|
|
30
|
+
switch (op.name) {
|
|
31
|
+
case "resize": {
|
|
32
|
+
const parts = [];
|
|
33
|
+
if (op.params.w !== void 0) parts.push(`w:${op.params.w}`);
|
|
34
|
+
if (op.params.h !== void 0) parts.push(`h:${op.params.h}`);
|
|
35
|
+
if (op.params.fit !== void 0) parts.push(`fit:${op.params.fit}`);
|
|
36
|
+
return `resize=${parts.join(",")}`;
|
|
37
|
+
}
|
|
38
|
+
case "crop":
|
|
39
|
+
return `crop=dim:${op.params.dim}`;
|
|
40
|
+
case "rotate":
|
|
41
|
+
return `rotate=deg:${op.params.deg}`;
|
|
42
|
+
case "flip":
|
|
43
|
+
return "flip";
|
|
44
|
+
case "flop":
|
|
45
|
+
return "flop";
|
|
46
|
+
case "quality":
|
|
47
|
+
return `quality=n:${op.params.n}`;
|
|
48
|
+
case "output":
|
|
49
|
+
return `output=format:${op.params.format}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function transformUrl({ handle, ops, apiUrl = "" }) {
|
|
53
|
+
const base = apiUrl.replace(/\/$/, "");
|
|
54
|
+
const chain = ops.map(serializeOp).join("/");
|
|
55
|
+
return chain ? `${base}/${chain}/${handle}` : `${base}/${handle}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/server/index.ts
|
|
59
|
+
function encodePolicy(spec) {
|
|
60
|
+
return Buffer.from(JSON.stringify(spec)).toString("base64url");
|
|
61
|
+
}
|
|
62
|
+
function getSignedDeliveryUrl(options) {
|
|
63
|
+
const { handle, secret, baseUrl, expiresIn = 300, ops } = options;
|
|
64
|
+
if (!handle) throw new Error("getSignedDeliveryUrl: `handle` is required");
|
|
65
|
+
if (!secret) throw new Error("getSignedDeliveryUrl: `secret` is required");
|
|
66
|
+
if (!baseUrl) throw new Error("getSignedDeliveryUrl: `baseUrl` is required");
|
|
67
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
68
|
+
const path = ops && ops.length > 0 ? transformUrl({ handle, ops, apiUrl: base }) : `${base}/file/${handle}`;
|
|
69
|
+
const expiry = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
70
|
+
const policy = encodePolicy({ expiry, call: ["read"], handle });
|
|
71
|
+
const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(policy).digest("hex");
|
|
72
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
73
|
+
return `${path}${sep}policy=${encodeURIComponent(policy)}&signature=${encodeURIComponent(signature)}`;
|
|
74
|
+
}
|
|
75
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
76
|
+
0 && (module.exports = {
|
|
77
|
+
getSignedDeliveryUrl
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=server.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/index.ts","../src/core/transform.ts"],"sourcesContent":["/**\n * Server-only helpers for `@ibanzajoe/uploader`.\n *\n * These require the account's api-key **secret** and use `node:crypto`, so they\n * MUST run on your backend — never ship the secret to the browser. That is why\n * they live in a dedicated entry point (`@ibanzajoe/uploader/server`) that the\n * browser/React bundles never import.\n *\n * The signing format here mirrors `@uploader/shared` (base64url policy JSON +\n * hex HMAC-SHA256) exactly, so the URLs it produces verify against the same\n * delivery guard the platform runs (`call: ['read']`, handle-bound).\n */\nimport { createHmac } from 'node:crypto'\nimport type { TransformOp } from '../core/transform.js'\nimport { transformUrl } from '../core/transform.js'\n\nexport type GetSignedDeliveryUrlOptions = {\n /** File handle from an upload (`FileResult.handle`). */\n handle: string\n /**\n * The account's api-key **secret** (server-only — never expose to the browser).\n * The signature is an HMAC over this secret; the guard verifies against the\n * file owner's secret.\n */\n secret: string\n /**\n * Delivery base URL — the host that serves this file. For a `signed` file this\n * is your edge Worker host, i.e. the origin of `FileResult.url`\n * (e.g. `https://edge.postila.app`).\n */\n baseUrl: string\n /**\n * Seconds until the URL expires. Keep it short and mint a fresh URL per view.\n * Default: 300 (5 minutes).\n */\n expiresIn?: number\n /**\n * Optional transform chain to apply (e.g. `[resize({ w: 400 }), output({ format: 'webp' })]`).\n * Omit for the original file.\n */\n ops?: TransformOp[]\n}\n\n/** Base64url-encode a policy spec as JSON (matches `@uploader/shared` encodePolicy). */\nfunction encodePolicy(spec: Record<string, unknown>): string {\n return Buffer.from(JSON.stringify(spec)).toString('base64url')\n}\n\n/**\n * Build a ready-to-use **signed delivery URL** for a `signed`-protected file, in\n * one call. Returns `<baseUrl>/file/<handle>?policy=…&signature=…` (or a\n * `<baseUrl>/<chain>/<handle>?…` URL when `ops` is given), signed with\n * HMAC-SHA256 over the account secret in the exact format the edge delivery\n * guard verifies.\n *\n * SERVER-ONLY: needs the account secret. Call it from your backend, per view,\n * with a short `expiresIn`, and hand the returned URL to your frontend\n * (e.g. as an `<img src>`).\n *\n * @example\n * import { getSignedDeliveryUrl } from '@ibanzajoe/uploader/server'\n *\n * // in an authenticated backend route:\n * const url = getSignedDeliveryUrl({\n * handle: file.handle, // from the upload response\n * secret: process.env.UPLOADER_API_SECRET!,\n * baseUrl: 'https://edge.postila.app',\n * expiresIn: 300,\n * })\n * // → \"https://edge.postila.app/file/<handle>?policy=…&signature=…\"\n */\nexport function getSignedDeliveryUrl(options: GetSignedDeliveryUrlOptions): string {\n const { handle, secret, baseUrl, expiresIn = 300, ops } = options\n if (!handle) throw new Error('getSignedDeliveryUrl: `handle` is required')\n if (!secret) throw new Error('getSignedDeliveryUrl: `secret` is required')\n if (!baseUrl) throw new Error('getSignedDeliveryUrl: `baseUrl` is required')\n\n const base = baseUrl.replace(/\\/$/, '')\n // Original → /file/<handle>; transform → /<chain>/<handle> (edge route grammar).\n const path =\n ops && ops.length > 0 ? transformUrl({ handle, ops, apiUrl: base }) : `${base}/file/${handle}`\n\n const expiry = Math.floor(Date.now() / 1000) + expiresIn\n const policy = encodePolicy({ expiry, call: ['read'], handle })\n const signature = createHmac('sha256', secret).update(policy).digest('hex')\n\n const sep = path.includes('?') ? '&' : '?'\n return `${path}${sep}policy=${encodeURIComponent(policy)}&signature=${encodeURIComponent(signature)}`\n}\n","/**\n * Delivery / transform URL builder.\n *\n * The API serves transformed derivatives at `GET /<chain>/<handle>`, where\n * <chain> is a slash-joined list of ops (e.g. `resize=w:200,h:200,fit:crop`).\n * These op builders + `transformUrl()` construct that URL from an uploaded file\n * handle, so consumers render an image at any size/format/quality without\n * hand-assembling URL strings.\n *\n * The op/param types and serialization mirror @uploader/shared EXACTLY — the\n * API's `parseTransformChain` is the other half of this contract, and\n * `contract.conformance.ts` asserts at typecheck time that they stay in sync.\n */\n\n// ─── Op vocabulary (mirrors @uploader/shared) ────────────────────────────────\n\nexport type TransformOpName =\n | 'resize'\n | 'crop'\n | 'rotate'\n | 'flip'\n | 'flop'\n | 'quality'\n | 'output'\n\n/** Parameters for the resize operation. */\nexport type ResizeParams = {\n w?: number\n h?: number\n fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' | 'crop'\n}\n\n/** Parameters for the crop operation. */\nexport type CropParams = {\n /** \"x,y,w,h\" notation. */\n dim: string\n x?: number\n y?: number\n w?: number\n h?: number\n}\n\n/** Parameters for the rotate operation. */\nexport type RotateParams = { deg: number }\n\n/** Parameters for the quality operation. */\nexport type QualityParams = { n: number }\n\n/** Parameters for the output operation. */\nexport type OutputParams = { format: string }\n\n/** A single transform operation with its typed params. */\nexport type TransformOp =\n | { name: 'resize'; params: ResizeParams }\n | { name: 'crop'; params: CropParams }\n | { name: 'rotate'; params: RotateParams }\n | { name: 'flip'; params: Record<string, never> }\n | { name: 'flop'; params: Record<string, never> }\n | { name: 'quality'; params: QualityParams }\n | { name: 'output'; params: OutputParams }\n\n/** An ordered list of transform operations. */\nexport type TransformChain = { ops: TransformOp[] }\n\n// ─── Op builders ─────────────────────────────────────────────────────────────\n\n/** Resize to a width and/or height with an optional fit mode. */\nexport const resize = (params: ResizeParams): TransformOp => ({ name: 'resize', params })\n\n/** Crop a rectangular region: origin (x, y) and size w×h, in source pixels. */\nexport const crop = (rect: { x: number; y: number; w: number; h: number }): TransformOp => ({\n name: 'crop',\n params: { dim: `${rect.x},${rect.y},${rect.w},${rect.h}`, ...rect },\n})\n\n/** Rotate clockwise by `deg` degrees. */\nexport const rotate = (params: RotateParams): TransformOp => ({ name: 'rotate', params })\n\n/** Flip vertically (mirror top↔bottom). */\nexport const flip = (): TransformOp => ({ name: 'flip', params: {} })\n\n/** Flop horizontally (mirror left↔right). */\nexport const flop = (): TransformOp => ({ name: 'flop', params: {} })\n\n/** Set output quality 1–100 (ignored by lossless formats). */\nexport const quality = (params: QualityParams): TransformOp => ({ name: 'quality', params })\n\n/** Convert the output format, e.g. `{ format: 'webp' }`. */\nexport const output = (params: OutputParams): TransformOp => ({ name: 'output', params })\n\n// ─── Serialization (vendored from @uploader/shared, parser-compatible) ────────\n\nfunction serializeOp(op: TransformOp): string {\n switch (op.name) {\n case 'resize': {\n const parts: string[] = []\n if (op.params.w !== undefined) parts.push(`w:${op.params.w}`)\n if (op.params.h !== undefined) parts.push(`h:${op.params.h}`)\n if (op.params.fit !== undefined) parts.push(`fit:${op.params.fit}`)\n return `resize=${parts.join(',')}`\n }\n case 'crop':\n return `crop=dim:${op.params.dim}`\n case 'rotate':\n return `rotate=deg:${op.params.deg}`\n case 'flip':\n return 'flip'\n case 'flop':\n return 'flop'\n case 'quality':\n return `quality=n:${op.params.n}`\n case 'output':\n return `output=format:${op.params.format}`\n }\n}\n\nexport type TransformUrlOptions = {\n /** File handle from an upload (`FileResult.handle`). */\n handle: string\n /** Ordered transform ops to apply. Must contain at least one op. */\n ops: TransformOp[]\n /**\n * Base API URL. Defaults to '' so the URL is relative — `/<chain>/<handle>` —\n * and resolves same-origin (e.g. proxied to the API). Pass an absolute URL to\n * point at a remote API directly.\n */\n apiUrl?: string\n}\n\n/**\n * Build a delivery URL that applies `ops` to the file identified by `handle`.\n *\n * @example\n * transformUrl({\n * handle: file.handle,\n * ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],\n * })\n * // => \"/resize=w:200,h:200,fit:crop/output=format:webp/<handle>\"\n */\nexport function transformUrl({ handle, ops, apiUrl = '' }: TransformUrlOptions): string {\n const base = apiUrl.replace(/\\/$/, '')\n const chain = ops.map(serializeOp).join('/')\n // The API's transform route requires at least one op segment before the\n // handle; for the unmodified original, use FileResult.url instead.\n return chain ? `${base}/${chain}/${handle}` : `${base}/${handle}`\n}\n\n/**\n * Append a signed read policy to a delivery/transform URL for accounts on the\n * `signed` delivery-protection tier (Private plan, docs/12).\n *\n * The `policy` + `signature` pair is produced SERVER-SIDE by the customer's\n * backend (HMAC over the account's api-key secret, via @uploader/shared). This\n * helper only assembles the URL — the SDK never signs and never sees the secret.\n *\n * @example\n * const url = withSignedPolicy(\n * transformUrl({ handle, ops: [resize({ w: 400 })], apiUrl }),\n * { policy, signature }, // from your backend\n * )\n */\nexport function withSignedPolicy(\n url: string,\n security: { policy: string; signature: string },\n): string {\n const sep = url.includes('?') ? '&' : '?'\n return `${url}${sep}policy=${encodeURIComponent(security.policy)}&signature=${encodeURIComponent(security.signature)}`\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAYA,yBAA2B;;;ACgF3B,SAAS,YAAY,IAAyB;AAC5C,UAAQ,GAAG,MAAM;AAAA,IACf,KAAK,UAAU;AACb,YAAM,QAAkB,CAAC;AACzB,UAAI,GAAG,OAAO,MAAM,OAAW,OAAM,KAAK,KAAK,GAAG,OAAO,CAAC,EAAE;AAC5D,UAAI,GAAG,OAAO,MAAM,OAAW,OAAM,KAAK,KAAK,GAAG,OAAO,CAAC,EAAE;AAC5D,UAAI,GAAG,OAAO,QAAQ,OAAW,OAAM,KAAK,OAAO,GAAG,OAAO,GAAG,EAAE;AAClE,aAAO,UAAU,MAAM,KAAK,GAAG,CAAC;AAAA,IAClC;AAAA,IACA,KAAK;AACH,aAAO,YAAY,GAAG,OAAO,GAAG;AAAA,IAClC,KAAK;AACH,aAAO,cAAc,GAAG,OAAO,GAAG;AAAA,IACpC,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,aAAa,GAAG,OAAO,CAAC;AAAA,IACjC,KAAK;AACH,aAAO,iBAAiB,GAAG,OAAO,MAAM;AAAA,EAC5C;AACF;AAyBO,SAAS,aAAa,EAAE,QAAQ,KAAK,SAAS,GAAG,GAAgC;AACtF,QAAM,OAAO,OAAO,QAAQ,OAAO,EAAE;AACrC,QAAM,QAAQ,IAAI,IAAI,WAAW,EAAE,KAAK,GAAG;AAG3C,SAAO,QAAQ,GAAG,IAAI,IAAI,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI,IAAI,MAAM;AACjE;;;ADrGA,SAAS,aAAa,MAAuC;AAC3D,SAAO,OAAO,KAAK,KAAK,UAAU,IAAI,CAAC,EAAE,SAAS,WAAW;AAC/D;AAyBO,SAAS,qBAAqB,SAA8C;AACjF,QAAM,EAAE,QAAQ,QAAQ,SAAS,YAAY,KAAK,IAAI,IAAI;AAC1D,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4CAA4C;AACzE,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4CAA4C;AACzE,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,6CAA6C;AAE3E,QAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAEtC,QAAM,OACJ,OAAO,IAAI,SAAS,IAAI,aAAa,EAAE,QAAQ,KAAK,QAAQ,KAAK,CAAC,IAAI,GAAG,IAAI,SAAS,MAAM;AAE9F,QAAM,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AAC/C,QAAM,SAAS,aAAa,EAAE,QAAQ,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC;AAC9D,QAAM,gBAAY,+BAAW,UAAU,MAAM,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAE1E,QAAM,MAAM,KAAK,SAAS,GAAG,IAAI,MAAM;AACvC,SAAO,GAAG,IAAI,GAAG,GAAG,UAAU,mBAAmB,MAAM,CAAC,cAAc,mBAAmB,SAAS,CAAC;AACrG;","names":[]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { b as TransformOp } from './transform-BTZ0kodO.cjs';
|
|
2
|
+
|
|
3
|
+
type GetSignedDeliveryUrlOptions = {
|
|
4
|
+
/** File handle from an upload (`FileResult.handle`). */
|
|
5
|
+
handle: string;
|
|
6
|
+
/**
|
|
7
|
+
* The account's api-key **secret** (server-only — never expose to the browser).
|
|
8
|
+
* The signature is an HMAC over this secret; the guard verifies against the
|
|
9
|
+
* file owner's secret.
|
|
10
|
+
*/
|
|
11
|
+
secret: string;
|
|
12
|
+
/**
|
|
13
|
+
* Delivery base URL — the host that serves this file. For a `signed` file this
|
|
14
|
+
* is your edge Worker host, i.e. the origin of `FileResult.url`
|
|
15
|
+
* (e.g. `https://edge.postila.app`).
|
|
16
|
+
*/
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* Seconds until the URL expires. Keep it short and mint a fresh URL per view.
|
|
20
|
+
* Default: 300 (5 minutes).
|
|
21
|
+
*/
|
|
22
|
+
expiresIn?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optional transform chain to apply (e.g. `[resize({ w: 400 }), output({ format: 'webp' })]`).
|
|
25
|
+
* Omit for the original file.
|
|
26
|
+
*/
|
|
27
|
+
ops?: TransformOp[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build a ready-to-use **signed delivery URL** for a `signed`-protected file, in
|
|
31
|
+
* one call. Returns `<baseUrl>/file/<handle>?policy=…&signature=…` (or a
|
|
32
|
+
* `<baseUrl>/<chain>/<handle>?…` URL when `ops` is given), signed with
|
|
33
|
+
* HMAC-SHA256 over the account secret in the exact format the edge delivery
|
|
34
|
+
* guard verifies.
|
|
35
|
+
*
|
|
36
|
+
* SERVER-ONLY: needs the account secret. Call it from your backend, per view,
|
|
37
|
+
* with a short `expiresIn`, and hand the returned URL to your frontend
|
|
38
|
+
* (e.g. as an `<img src>`).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* import { getSignedDeliveryUrl } from '@ibanzajoe/uploader/server'
|
|
42
|
+
*
|
|
43
|
+
* // in an authenticated backend route:
|
|
44
|
+
* const url = getSignedDeliveryUrl({
|
|
45
|
+
* handle: file.handle, // from the upload response
|
|
46
|
+
* secret: process.env.UPLOADER_API_SECRET!,
|
|
47
|
+
* baseUrl: 'https://edge.postila.app',
|
|
48
|
+
* expiresIn: 300,
|
|
49
|
+
* })
|
|
50
|
+
* // → "https://edge.postila.app/file/<handle>?policy=…&signature=…"
|
|
51
|
+
*/
|
|
52
|
+
declare function getSignedDeliveryUrl(options: GetSignedDeliveryUrlOptions): string;
|
|
53
|
+
|
|
54
|
+
export { type GetSignedDeliveryUrlOptions, getSignedDeliveryUrl };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { b as TransformOp } from './transform-BTZ0kodO.js';
|
|
2
|
+
|
|
3
|
+
type GetSignedDeliveryUrlOptions = {
|
|
4
|
+
/** File handle from an upload (`FileResult.handle`). */
|
|
5
|
+
handle: string;
|
|
6
|
+
/**
|
|
7
|
+
* The account's api-key **secret** (server-only — never expose to the browser).
|
|
8
|
+
* The signature is an HMAC over this secret; the guard verifies against the
|
|
9
|
+
* file owner's secret.
|
|
10
|
+
*/
|
|
11
|
+
secret: string;
|
|
12
|
+
/**
|
|
13
|
+
* Delivery base URL — the host that serves this file. For a `signed` file this
|
|
14
|
+
* is your edge Worker host, i.e. the origin of `FileResult.url`
|
|
15
|
+
* (e.g. `https://edge.postila.app`).
|
|
16
|
+
*/
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
/**
|
|
19
|
+
* Seconds until the URL expires. Keep it short and mint a fresh URL per view.
|
|
20
|
+
* Default: 300 (5 minutes).
|
|
21
|
+
*/
|
|
22
|
+
expiresIn?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Optional transform chain to apply (e.g. `[resize({ w: 400 }), output({ format: 'webp' })]`).
|
|
25
|
+
* Omit for the original file.
|
|
26
|
+
*/
|
|
27
|
+
ops?: TransformOp[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build a ready-to-use **signed delivery URL** for a `signed`-protected file, in
|
|
31
|
+
* one call. Returns `<baseUrl>/file/<handle>?policy=…&signature=…` (or a
|
|
32
|
+
* `<baseUrl>/<chain>/<handle>?…` URL when `ops` is given), signed with
|
|
33
|
+
* HMAC-SHA256 over the account secret in the exact format the edge delivery
|
|
34
|
+
* guard verifies.
|
|
35
|
+
*
|
|
36
|
+
* SERVER-ONLY: needs the account secret. Call it from your backend, per view,
|
|
37
|
+
* with a short `expiresIn`, and hand the returned URL to your frontend
|
|
38
|
+
* (e.g. as an `<img src>`).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* import { getSignedDeliveryUrl } from '@ibanzajoe/uploader/server'
|
|
42
|
+
*
|
|
43
|
+
* // in an authenticated backend route:
|
|
44
|
+
* const url = getSignedDeliveryUrl({
|
|
45
|
+
* handle: file.handle, // from the upload response
|
|
46
|
+
* secret: process.env.UPLOADER_API_SECRET!,
|
|
47
|
+
* baseUrl: 'https://edge.postila.app',
|
|
48
|
+
* expiresIn: 300,
|
|
49
|
+
* })
|
|
50
|
+
* // → "https://edge.postila.app/file/<handle>?policy=…&signature=…"
|
|
51
|
+
*/
|
|
52
|
+
declare function getSignedDeliveryUrl(options: GetSignedDeliveryUrlOptions): string;
|
|
53
|
+
|
|
54
|
+
export { type GetSignedDeliveryUrlOptions, getSignedDeliveryUrl };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {
|
|
2
|
+
transformUrl
|
|
3
|
+
} from "./chunk-DJK2SQ5I.js";
|
|
4
|
+
|
|
5
|
+
// src/server/index.ts
|
|
6
|
+
import { createHmac } from "crypto";
|
|
7
|
+
function encodePolicy(spec) {
|
|
8
|
+
return Buffer.from(JSON.stringify(spec)).toString("base64url");
|
|
9
|
+
}
|
|
10
|
+
function getSignedDeliveryUrl(options) {
|
|
11
|
+
const { handle, secret, baseUrl, expiresIn = 300, ops } = options;
|
|
12
|
+
if (!handle) throw new Error("getSignedDeliveryUrl: `handle` is required");
|
|
13
|
+
if (!secret) throw new Error("getSignedDeliveryUrl: `secret` is required");
|
|
14
|
+
if (!baseUrl) throw new Error("getSignedDeliveryUrl: `baseUrl` is required");
|
|
15
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
16
|
+
const path = ops && ops.length > 0 ? transformUrl({ handle, ops, apiUrl: base }) : `${base}/file/${handle}`;
|
|
17
|
+
const expiry = Math.floor(Date.now() / 1e3) + expiresIn;
|
|
18
|
+
const policy = encodePolicy({ expiry, call: ["read"], handle });
|
|
19
|
+
const signature = createHmac("sha256", secret).update(policy).digest("hex");
|
|
20
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
21
|
+
return `${path}${sep}policy=${encodeURIComponent(policy)}&signature=${encodeURIComponent(signature)}`;
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
getSignedDeliveryUrl
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server/index.ts"],"sourcesContent":["/**\n * Server-only helpers for `@ibanzajoe/uploader`.\n *\n * These require the account's api-key **secret** and use `node:crypto`, so they\n * MUST run on your backend — never ship the secret to the browser. That is why\n * they live in a dedicated entry point (`@ibanzajoe/uploader/server`) that the\n * browser/React bundles never import.\n *\n * The signing format here mirrors `@uploader/shared` (base64url policy JSON +\n * hex HMAC-SHA256) exactly, so the URLs it produces verify against the same\n * delivery guard the platform runs (`call: ['read']`, handle-bound).\n */\nimport { createHmac } from 'node:crypto'\nimport type { TransformOp } from '../core/transform.js'\nimport { transformUrl } from '../core/transform.js'\n\nexport type GetSignedDeliveryUrlOptions = {\n /** File handle from an upload (`FileResult.handle`). */\n handle: string\n /**\n * The account's api-key **secret** (server-only — never expose to the browser).\n * The signature is an HMAC over this secret; the guard verifies against the\n * file owner's secret.\n */\n secret: string\n /**\n * Delivery base URL — the host that serves this file. For a `signed` file this\n * is your edge Worker host, i.e. the origin of `FileResult.url`\n * (e.g. `https://edge.postila.app`).\n */\n baseUrl: string\n /**\n * Seconds until the URL expires. Keep it short and mint a fresh URL per view.\n * Default: 300 (5 minutes).\n */\n expiresIn?: number\n /**\n * Optional transform chain to apply (e.g. `[resize({ w: 400 }), output({ format: 'webp' })]`).\n * Omit for the original file.\n */\n ops?: TransformOp[]\n}\n\n/** Base64url-encode a policy spec as JSON (matches `@uploader/shared` encodePolicy). */\nfunction encodePolicy(spec: Record<string, unknown>): string {\n return Buffer.from(JSON.stringify(spec)).toString('base64url')\n}\n\n/**\n * Build a ready-to-use **signed delivery URL** for a `signed`-protected file, in\n * one call. Returns `<baseUrl>/file/<handle>?policy=…&signature=…` (or a\n * `<baseUrl>/<chain>/<handle>?…` URL when `ops` is given), signed with\n * HMAC-SHA256 over the account secret in the exact format the edge delivery\n * guard verifies.\n *\n * SERVER-ONLY: needs the account secret. Call it from your backend, per view,\n * with a short `expiresIn`, and hand the returned URL to your frontend\n * (e.g. as an `<img src>`).\n *\n * @example\n * import { getSignedDeliveryUrl } from '@ibanzajoe/uploader/server'\n *\n * // in an authenticated backend route:\n * const url = getSignedDeliveryUrl({\n * handle: file.handle, // from the upload response\n * secret: process.env.UPLOADER_API_SECRET!,\n * baseUrl: 'https://edge.postila.app',\n * expiresIn: 300,\n * })\n * // → \"https://edge.postila.app/file/<handle>?policy=…&signature=…\"\n */\nexport function getSignedDeliveryUrl(options: GetSignedDeliveryUrlOptions): string {\n const { handle, secret, baseUrl, expiresIn = 300, ops } = options\n if (!handle) throw new Error('getSignedDeliveryUrl: `handle` is required')\n if (!secret) throw new Error('getSignedDeliveryUrl: `secret` is required')\n if (!baseUrl) throw new Error('getSignedDeliveryUrl: `baseUrl` is required')\n\n const base = baseUrl.replace(/\\/$/, '')\n // Original → /file/<handle>; transform → /<chain>/<handle> (edge route grammar).\n const path =\n ops && ops.length > 0 ? transformUrl({ handle, ops, apiUrl: base }) : `${base}/file/${handle}`\n\n const expiry = Math.floor(Date.now() / 1000) + expiresIn\n const policy = encodePolicy({ expiry, call: ['read'], handle })\n const signature = createHmac('sha256', secret).update(policy).digest('hex')\n\n const sep = path.includes('?') ? '&' : '?'\n return `${path}${sep}policy=${encodeURIComponent(policy)}&signature=${encodeURIComponent(signature)}`\n}\n"],"mappings":";;;;;AAYA,SAAS,kBAAkB;AAgC3B,SAAS,aAAa,MAAuC;AAC3D,SAAO,OAAO,KAAK,KAAK,UAAU,IAAI,CAAC,EAAE,SAAS,WAAW;AAC/D;AAyBO,SAAS,qBAAqB,SAA8C;AACjF,QAAM,EAAE,QAAQ,QAAQ,SAAS,YAAY,KAAK,IAAI,IAAI;AAC1D,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4CAA4C;AACzE,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4CAA4C;AACzE,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,6CAA6C;AAE3E,QAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAEtC,QAAM,OACJ,OAAO,IAAI,SAAS,IAAI,aAAa,EAAE,QAAQ,KAAK,QAAQ,KAAK,CAAC,IAAI,GAAG,IAAI,SAAS,MAAM;AAE9F,QAAM,SAAS,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI;AAC/C,QAAM,SAAS,aAAa,EAAE,QAAQ,MAAM,CAAC,MAAM,GAAG,OAAO,CAAC;AAC9D,QAAM,YAAY,WAAW,UAAU,MAAM,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AAE1E,QAAM,MAAM,KAAK,SAAS,GAAG,IAAI,MAAM;AACvC,SAAO,GAAG,IAAI,GAAG,GAAG,UAAU,mBAAmB,MAAM,CAAC,cAAc,mBAAmB,SAAS,CAAC;AACrG;","names":[]}
|
package/dist/styles.css
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @uploader
|
|
2
|
+
* @ibanzajoe/uploader — component styles
|
|
3
3
|
*
|
|
4
4
|
* Themeable via CSS custom properties. Override any variable in your own
|
|
5
5
|
* stylesheet or on a parent element. Light / dark modes are handled via the
|
|
6
6
|
* `data-theme="dark"` attribute on any ancestor.
|
|
7
7
|
*
|
|
8
8
|
* Import once in your app:
|
|
9
|
-
* import '@uploader/
|
|
9
|
+
* import '@ibanzajoe/uploader/styles.css'
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/* ─── Design tokens ─────────────────────────────────────────────────────────── */
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery / transform URL builder.
|
|
3
|
+
*
|
|
4
|
+
* The API serves transformed derivatives at `GET /<chain>/<handle>`, where
|
|
5
|
+
* <chain> is a slash-joined list of ops (e.g. `resize=w:200,h:200,fit:crop`).
|
|
6
|
+
* These op builders + `transformUrl()` construct that URL from an uploaded file
|
|
7
|
+
* handle, so consumers render an image at any size/format/quality without
|
|
8
|
+
* hand-assembling URL strings.
|
|
9
|
+
*
|
|
10
|
+
* The op/param types and serialization mirror @uploader/shared EXACTLY — the
|
|
11
|
+
* API's `parseTransformChain` is the other half of this contract, and
|
|
12
|
+
* `contract.conformance.ts` asserts at typecheck time that they stay in sync.
|
|
13
|
+
*/
|
|
14
|
+
type TransformOpName = 'resize' | 'crop' | 'rotate' | 'flip' | 'flop' | 'quality' | 'output';
|
|
15
|
+
/** Parameters for the resize operation. */
|
|
16
|
+
type ResizeParams = {
|
|
17
|
+
w?: number;
|
|
18
|
+
h?: number;
|
|
19
|
+
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' | 'crop';
|
|
20
|
+
};
|
|
21
|
+
/** Parameters for the crop operation. */
|
|
22
|
+
type CropParams = {
|
|
23
|
+
/** "x,y,w,h" notation. */
|
|
24
|
+
dim: string;
|
|
25
|
+
x?: number;
|
|
26
|
+
y?: number;
|
|
27
|
+
w?: number;
|
|
28
|
+
h?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Parameters for the rotate operation. */
|
|
31
|
+
type RotateParams = {
|
|
32
|
+
deg: number;
|
|
33
|
+
};
|
|
34
|
+
/** Parameters for the quality operation. */
|
|
35
|
+
type QualityParams = {
|
|
36
|
+
n: number;
|
|
37
|
+
};
|
|
38
|
+
/** Parameters for the output operation. */
|
|
39
|
+
type OutputParams = {
|
|
40
|
+
format: string;
|
|
41
|
+
};
|
|
42
|
+
/** A single transform operation with its typed params. */
|
|
43
|
+
type TransformOp = {
|
|
44
|
+
name: 'resize';
|
|
45
|
+
params: ResizeParams;
|
|
46
|
+
} | {
|
|
47
|
+
name: 'crop';
|
|
48
|
+
params: CropParams;
|
|
49
|
+
} | {
|
|
50
|
+
name: 'rotate';
|
|
51
|
+
params: RotateParams;
|
|
52
|
+
} | {
|
|
53
|
+
name: 'flip';
|
|
54
|
+
params: Record<string, never>;
|
|
55
|
+
} | {
|
|
56
|
+
name: 'flop';
|
|
57
|
+
params: Record<string, never>;
|
|
58
|
+
} | {
|
|
59
|
+
name: 'quality';
|
|
60
|
+
params: QualityParams;
|
|
61
|
+
} | {
|
|
62
|
+
name: 'output';
|
|
63
|
+
params: OutputParams;
|
|
64
|
+
};
|
|
65
|
+
/** An ordered list of transform operations. */
|
|
66
|
+
type TransformChain = {
|
|
67
|
+
ops: TransformOp[];
|
|
68
|
+
};
|
|
69
|
+
/** Resize to a width and/or height with an optional fit mode. */
|
|
70
|
+
declare const resize: (params: ResizeParams) => TransformOp;
|
|
71
|
+
/** Crop a rectangular region: origin (x, y) and size w×h, in source pixels. */
|
|
72
|
+
declare const crop: (rect: {
|
|
73
|
+
x: number;
|
|
74
|
+
y: number;
|
|
75
|
+
w: number;
|
|
76
|
+
h: number;
|
|
77
|
+
}) => TransformOp;
|
|
78
|
+
/** Rotate clockwise by `deg` degrees. */
|
|
79
|
+
declare const rotate: (params: RotateParams) => TransformOp;
|
|
80
|
+
/** Flip vertically (mirror top↔bottom). */
|
|
81
|
+
declare const flip: () => TransformOp;
|
|
82
|
+
/** Flop horizontally (mirror left↔right). */
|
|
83
|
+
declare const flop: () => TransformOp;
|
|
84
|
+
/** Set output quality 1–100 (ignored by lossless formats). */
|
|
85
|
+
declare const quality: (params: QualityParams) => TransformOp;
|
|
86
|
+
/** Convert the output format, e.g. `{ format: 'webp' }`. */
|
|
87
|
+
declare const output: (params: OutputParams) => TransformOp;
|
|
88
|
+
type TransformUrlOptions = {
|
|
89
|
+
/** File handle from an upload (`FileResult.handle`). */
|
|
90
|
+
handle: string;
|
|
91
|
+
/** Ordered transform ops to apply. Must contain at least one op. */
|
|
92
|
+
ops: TransformOp[];
|
|
93
|
+
/**
|
|
94
|
+
* Base API URL. Defaults to '' so the URL is relative — `/<chain>/<handle>` —
|
|
95
|
+
* and resolves same-origin (e.g. proxied to the API). Pass an absolute URL to
|
|
96
|
+
* point at a remote API directly.
|
|
97
|
+
*/
|
|
98
|
+
apiUrl?: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Build a delivery URL that applies `ops` to the file identified by `handle`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* transformUrl({
|
|
105
|
+
* handle: file.handle,
|
|
106
|
+
* ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],
|
|
107
|
+
* })
|
|
108
|
+
* // => "/resize=w:200,h:200,fit:crop/output=format:webp/<handle>"
|
|
109
|
+
*/
|
|
110
|
+
declare function transformUrl({ handle, ops, apiUrl }: TransformUrlOptions): string;
|
|
111
|
+
/**
|
|
112
|
+
* Append a signed read policy to a delivery/transform URL for accounts on the
|
|
113
|
+
* `signed` delivery-protection tier (Private plan, docs/12).
|
|
114
|
+
*
|
|
115
|
+
* The `policy` + `signature` pair is produced SERVER-SIDE by the customer's
|
|
116
|
+
* backend (HMAC over the account's api-key secret, via @uploader/shared). This
|
|
117
|
+
* helper only assembles the URL — the SDK never signs and never sees the secret.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* const url = withSignedPolicy(
|
|
121
|
+
* transformUrl({ handle, ops: [resize({ w: 400 })], apiUrl }),
|
|
122
|
+
* { policy, signature }, // from your backend
|
|
123
|
+
* )
|
|
124
|
+
*/
|
|
125
|
+
declare function withSignedPolicy(url: string, security: {
|
|
126
|
+
policy: string;
|
|
127
|
+
signature: string;
|
|
128
|
+
}): string;
|
|
129
|
+
|
|
130
|
+
export { type CropParams as C, type OutputParams as O, type QualityParams as Q, type ResizeParams as R, type TransformChain as T, type RotateParams as a, type TransformOp as b, type TransformOpName as c, type TransformUrlOptions as d, crop as e, flip as f, flop as g, rotate as h, output as o, quality as q, resize as r, transformUrl as t, withSignedPolicy as w };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delivery / transform URL builder.
|
|
3
|
+
*
|
|
4
|
+
* The API serves transformed derivatives at `GET /<chain>/<handle>`, where
|
|
5
|
+
* <chain> is a slash-joined list of ops (e.g. `resize=w:200,h:200,fit:crop`).
|
|
6
|
+
* These op builders + `transformUrl()` construct that URL from an uploaded file
|
|
7
|
+
* handle, so consumers render an image at any size/format/quality without
|
|
8
|
+
* hand-assembling URL strings.
|
|
9
|
+
*
|
|
10
|
+
* The op/param types and serialization mirror @uploader/shared EXACTLY — the
|
|
11
|
+
* API's `parseTransformChain` is the other half of this contract, and
|
|
12
|
+
* `contract.conformance.ts` asserts at typecheck time that they stay in sync.
|
|
13
|
+
*/
|
|
14
|
+
type TransformOpName = 'resize' | 'crop' | 'rotate' | 'flip' | 'flop' | 'quality' | 'output';
|
|
15
|
+
/** Parameters for the resize operation. */
|
|
16
|
+
type ResizeParams = {
|
|
17
|
+
w?: number;
|
|
18
|
+
h?: number;
|
|
19
|
+
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' | 'crop';
|
|
20
|
+
};
|
|
21
|
+
/** Parameters for the crop operation. */
|
|
22
|
+
type CropParams = {
|
|
23
|
+
/** "x,y,w,h" notation. */
|
|
24
|
+
dim: string;
|
|
25
|
+
x?: number;
|
|
26
|
+
y?: number;
|
|
27
|
+
w?: number;
|
|
28
|
+
h?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Parameters for the rotate operation. */
|
|
31
|
+
type RotateParams = {
|
|
32
|
+
deg: number;
|
|
33
|
+
};
|
|
34
|
+
/** Parameters for the quality operation. */
|
|
35
|
+
type QualityParams = {
|
|
36
|
+
n: number;
|
|
37
|
+
};
|
|
38
|
+
/** Parameters for the output operation. */
|
|
39
|
+
type OutputParams = {
|
|
40
|
+
format: string;
|
|
41
|
+
};
|
|
42
|
+
/** A single transform operation with its typed params. */
|
|
43
|
+
type TransformOp = {
|
|
44
|
+
name: 'resize';
|
|
45
|
+
params: ResizeParams;
|
|
46
|
+
} | {
|
|
47
|
+
name: 'crop';
|
|
48
|
+
params: CropParams;
|
|
49
|
+
} | {
|
|
50
|
+
name: 'rotate';
|
|
51
|
+
params: RotateParams;
|
|
52
|
+
} | {
|
|
53
|
+
name: 'flip';
|
|
54
|
+
params: Record<string, never>;
|
|
55
|
+
} | {
|
|
56
|
+
name: 'flop';
|
|
57
|
+
params: Record<string, never>;
|
|
58
|
+
} | {
|
|
59
|
+
name: 'quality';
|
|
60
|
+
params: QualityParams;
|
|
61
|
+
} | {
|
|
62
|
+
name: 'output';
|
|
63
|
+
params: OutputParams;
|
|
64
|
+
};
|
|
65
|
+
/** An ordered list of transform operations. */
|
|
66
|
+
type TransformChain = {
|
|
67
|
+
ops: TransformOp[];
|
|
68
|
+
};
|
|
69
|
+
/** Resize to a width and/or height with an optional fit mode. */
|
|
70
|
+
declare const resize: (params: ResizeParams) => TransformOp;
|
|
71
|
+
/** Crop a rectangular region: origin (x, y) and size w×h, in source pixels. */
|
|
72
|
+
declare const crop: (rect: {
|
|
73
|
+
x: number;
|
|
74
|
+
y: number;
|
|
75
|
+
w: number;
|
|
76
|
+
h: number;
|
|
77
|
+
}) => TransformOp;
|
|
78
|
+
/** Rotate clockwise by `deg` degrees. */
|
|
79
|
+
declare const rotate: (params: RotateParams) => TransformOp;
|
|
80
|
+
/** Flip vertically (mirror top↔bottom). */
|
|
81
|
+
declare const flip: () => TransformOp;
|
|
82
|
+
/** Flop horizontally (mirror left↔right). */
|
|
83
|
+
declare const flop: () => TransformOp;
|
|
84
|
+
/** Set output quality 1–100 (ignored by lossless formats). */
|
|
85
|
+
declare const quality: (params: QualityParams) => TransformOp;
|
|
86
|
+
/** Convert the output format, e.g. `{ format: 'webp' }`. */
|
|
87
|
+
declare const output: (params: OutputParams) => TransformOp;
|
|
88
|
+
type TransformUrlOptions = {
|
|
89
|
+
/** File handle from an upload (`FileResult.handle`). */
|
|
90
|
+
handle: string;
|
|
91
|
+
/** Ordered transform ops to apply. Must contain at least one op. */
|
|
92
|
+
ops: TransformOp[];
|
|
93
|
+
/**
|
|
94
|
+
* Base API URL. Defaults to '' so the URL is relative — `/<chain>/<handle>` —
|
|
95
|
+
* and resolves same-origin (e.g. proxied to the API). Pass an absolute URL to
|
|
96
|
+
* point at a remote API directly.
|
|
97
|
+
*/
|
|
98
|
+
apiUrl?: string;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Build a delivery URL that applies `ops` to the file identified by `handle`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* transformUrl({
|
|
105
|
+
* handle: file.handle,
|
|
106
|
+
* ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],
|
|
107
|
+
* })
|
|
108
|
+
* // => "/resize=w:200,h:200,fit:crop/output=format:webp/<handle>"
|
|
109
|
+
*/
|
|
110
|
+
declare function transformUrl({ handle, ops, apiUrl }: TransformUrlOptions): string;
|
|
111
|
+
/**
|
|
112
|
+
* Append a signed read policy to a delivery/transform URL for accounts on the
|
|
113
|
+
* `signed` delivery-protection tier (Private plan, docs/12).
|
|
114
|
+
*
|
|
115
|
+
* The `policy` + `signature` pair is produced SERVER-SIDE by the customer's
|
|
116
|
+
* backend (HMAC over the account's api-key secret, via @uploader/shared). This
|
|
117
|
+
* helper only assembles the URL — the SDK never signs and never sees the secret.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* const url = withSignedPolicy(
|
|
121
|
+
* transformUrl({ handle, ops: [resize({ w: 400 })], apiUrl }),
|
|
122
|
+
* { policy, signature }, // from your backend
|
|
123
|
+
* )
|
|
124
|
+
*/
|
|
125
|
+
declare function withSignedPolicy(url: string, security: {
|
|
126
|
+
policy: string;
|
|
127
|
+
signature: string;
|
|
128
|
+
}): string;
|
|
129
|
+
|
|
130
|
+
export { type CropParams as C, type OutputParams as O, type QualityParams as Q, type ResizeParams as R, type TransformChain as T, type RotateParams as a, type TransformOp as b, type TransformOpName as c, type TransformUrlOptions as d, crop as e, flip as f, flop as g, rotate as h, output as o, quality as q, resize as r, transformUrl as t, withSignedPolicy as w };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibanzajoe/uploader",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Drop-in React file-upload picker (drag-and-drop + dialog), headless upload client, in-picker image editor, and transform-URL builder.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ibanzajoe",
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
"import": "./dist/core.js",
|
|
42
42
|
"require": "./dist/core.cjs"
|
|
43
43
|
},
|
|
44
|
+
"./server": {
|
|
45
|
+
"types": "./dist/server.d.ts",
|
|
46
|
+
"import": "./dist/server.js",
|
|
47
|
+
"require": "./dist/server.cjs"
|
|
48
|
+
},
|
|
44
49
|
"./styles.css": "./dist/styles.css"
|
|
45
50
|
},
|
|
46
51
|
"files": [
|
|
@@ -49,7 +54,12 @@
|
|
|
49
54
|
"scripts": {
|
|
50
55
|
"build": "tsup",
|
|
51
56
|
"typecheck": "tsc --noEmit",
|
|
52
|
-
"prepublishOnly": "npm run build"
|
|
57
|
+
"prepublishOnly": "npm run build",
|
|
58
|
+
"release:dry": "npm publish --dry-run",
|
|
59
|
+
"release": "npm publish",
|
|
60
|
+
"release:patch": "npm version patch --no-git-tag-version && npm publish",
|
|
61
|
+
"release:minor": "npm version minor --no-git-tag-version && npm publish",
|
|
62
|
+
"release:major": "npm version major --no-git-tag-version && npm publish"
|
|
53
63
|
},
|
|
54
64
|
"peerDependencies": {
|
|
55
65
|
"react": "^18 || ^19",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/errors.ts","../src/core/chunk.ts","../src/core/client.ts","../src/core/transform.ts"],"sourcesContent":["/**\n * Typed errors for UploaderClient.\n *\n * UploaderError is thrown by upload / uploadAll on any non-retried failure.\n * The caller can narrow on `err.code` for structured handling.\n */\n\nexport type UploaderErrorCode =\n | 'ABORTED' // AbortSignal fired\n | 'NETWORK_ERROR' // fetch() threw (no response)\n | 'SERVER_ERROR' // 5xx after all retries exhausted\n | 'CLIENT_ERROR' // 4xx (not retried)\n | 'INVALID_RESPONSE' // response body did not match expected shape\n\nexport class UploaderError extends Error {\n readonly code: UploaderErrorCode\n /** HTTP status code when available (undefined for ABORTED / NETWORK_ERROR). */\n readonly statusCode?: number\n\n constructor(code: UploaderErrorCode, message: string, statusCode?: number) {\n super(message)\n this.name = 'UploaderError'\n this.code = code\n this.statusCode = statusCode\n }\n}\n","/**\n * Chunk planner for multipart uploads.\n *\n * Decides single-shot vs multipart by comparing the file size against\n * MULTIPART_THRESHOLD. For multipart files, slices the Blob into parts of\n * `chunkSize` bytes.\n */\n\n/** Files ≤ this size use single-shot POST /api/store. */\nexport const MULTIPART_THRESHOLD = 5 * 1024 * 1024 // 5 MB\n\n/** Default part size for multipart uploads. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024 // 5 MB\n\nexport type ChunkPlan =\n | { mode: 'single' }\n | { mode: 'multipart'; parts: Blob[]; partSize: number }\n\n/**\n * Build a chunk plan for a file.\n *\n * @param file The File or Blob to upload.\n * @param chunkSize Desired part size in bytes (default DEFAULT_CHUNK_SIZE).\n * @returns A plan describing whether to use single-shot or multipart.\n */\nexport function planChunks(file: File | Blob, chunkSize = DEFAULT_CHUNK_SIZE): ChunkPlan {\n if (file.size <= MULTIPART_THRESHOLD) {\n return { mode: 'single' }\n }\n\n const parts: Blob[] = []\n let offset = 0\n while (offset < file.size) {\n parts.push(file.slice(offset, offset + chunkSize))\n offset += chunkSize\n }\n return { mode: 'multipart', parts, partSize: chunkSize }\n}\n","/**\n * UploaderClient — headless upload client.\n *\n * Chooses single-shot (POST /api/store, multipart/form-data) vs multipart\n * (start/part/complete) based on file size relative to MULTIPART_THRESHOLD\n * (5 MB). Parts are retried individually with exponential backoff. Progress\n * is emitted as a 0–100 integer. AbortSignal cancels in-flight work.\n */\n\nimport type {\n FileResult,\n UploadStartResponse,\n UploadPartResponse,\n UploaderClientOptions,\n UploadAllOptions,\n UploadOptions,\n} from './types.js'\nimport { UploaderError } from './errors.js'\nimport { planChunks } from './chunk.js'\n\n// ─── Constants ────────────────────────────────────────────────────────────────\n\nconst MAX_RETRIES = 3\nconst RETRY_BASE_MS = 200\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\n/** Sleep for `ms` milliseconds, resolving early if signal fires. */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new UploaderError('ABORTED', 'Upload aborted'))\n return\n }\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener('abort', () => {\n clearTimeout(timer)\n reject(new UploaderError('ABORTED', 'Upload aborted'))\n }, { once: true })\n })\n}\n\n/** Throw UploaderError(ABORTED) if signal has already fired. */\nfunction checkAbort(signal?: AbortSignal): void {\n if (signal?.aborted) {\n throw new UploaderError('ABORTED', 'Upload aborted')\n }\n}\n\n/**\n * Fetch with retry on network errors and 5xx responses.\n * 4xx responses are not retried — they surface immediately as CLIENT_ERROR.\n */\nasync function fetchWithRetry(\n url: string,\n init: RequestInit,\n signal?: AbortSignal,\n maxRetries = MAX_RETRIES,\n): Promise<Response> {\n let lastErr: unknown\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n checkAbort(signal)\n try {\n const res = await fetch(url, { ...init, signal })\n if (res.status >= 400 && res.status < 500) {\n // Client error — do not retry\n const body = await res.text().catch(() => '')\n throw new UploaderError(\n 'CLIENT_ERROR',\n `HTTP ${res.status}: ${body}`,\n res.status,\n )\n }\n if (res.status >= 500) {\n // Server error — retry with backoff\n lastErr = new UploaderError(\n 'SERVER_ERROR',\n `HTTP ${res.status}`,\n res.status,\n )\n if (attempt < maxRetries - 1) {\n await sleep(RETRY_BASE_MS * 2 ** attempt, signal)\n }\n continue\n }\n return res\n } catch (err) {\n if (err instanceof UploaderError) {\n if (err.code === 'CLIENT_ERROR' || err.code === 'ABORTED') throw err\n lastErr = err\n } else {\n // fetch() threw (network failure, CORS, etc.)\n lastErr = new UploaderError(\n 'NETWORK_ERROR',\n err instanceof Error ? err.message : String(err),\n )\n }\n if (attempt < maxRetries - 1) {\n await sleep(RETRY_BASE_MS * 2 ** attempt, signal)\n }\n }\n }\n throw lastErr\n}\n\n// ─── UploaderClient ────────────────────────────────────────────────────────────\n\nexport class UploaderClient {\n readonly apikey: string\n readonly apiUrl: string\n readonly security: UploaderClientOptions['security']\n\n constructor(options: UploaderClientOptions) {\n this.apikey = options.apikey\n this.apiUrl = (options.apiUrl ?? 'https://api.uploaderhq.io').replace(/\\/$/, '')\n this.security = options.security\n }\n\n // ─── Auth headers ──────────────────────────────────────────────────────────\n\n /**\n * Build the auth headers shared by all requests.\n * Attaches the API key and, when present, the signed policy pair.\n */\n #authHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'X-Uploader-Key': this.apikey,\n }\n if (this.security) {\n headers['X-Uploader-Policy'] = this.security.policy\n headers['X-Uploader-Signature'] = this.security.signature\n }\n return headers\n }\n\n // ─── Single-shot upload ────────────────────────────────────────────────────\n\n /**\n * POST /api/store — multipart/form-data for files ≤ MULTIPART_THRESHOLD.\n */\n async #uploadSingleShot(\n file: File | Blob,\n opts: UploadOptions,\n ): Promise<FileResult> {\n const { onProgress, filename, signal } = opts\n\n onProgress?.(0)\n checkAbort(signal)\n\n const form = new FormData()\n form.append('file', file, filename ?? (file instanceof File ? file.name : 'upload'))\n if (filename) form.append('filename', filename)\n\n const res = await fetchWithRetry(\n `${this.apiUrl}/api/store`,\n {\n method: 'POST',\n headers: this.#authHeaders(),\n body: form,\n },\n signal,\n )\n\n const body = await res.json() as unknown\n onProgress?.(100)\n return this.#parseFileResult(body)\n }\n\n // ─── Multipart upload ──────────────────────────────────────────────────────\n\n /**\n * start → parts (parallel with progress) → complete for files > MULTIPART_THRESHOLD.\n */\n async #uploadMultipart(\n file: File | Blob,\n parts: Blob[],\n opts: UploadOptions,\n ): Promise<FileResult> {\n const { onProgress, filename, signal } = opts\n const name = filename ?? (file instanceof File ? file.name : 'upload')\n const mime = file instanceof File ? file.type : 'application/octet-stream'\n\n onProgress?.(0)\n checkAbort(signal)\n\n // 1. Start\n const startRes = await fetchWithRetry(\n `${this.apiUrl}/api/upload/start`,\n {\n method: 'POST',\n headers: { ...this.#authHeaders(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ filename: name, mimetype: mime, size: file.size }),\n },\n signal,\n )\n const { uploadId } = await startRes.json() as UploadStartResponse\n\n // 2. Upload parts sequentially (retried individually)\n const etags: { partNumber: number; etag: string }[] = []\n let uploadedBytes = 0\n\n for (let i = 0; i < parts.length; i++) {\n checkAbort(signal)\n const partBlob = parts[i]!\n const partNumber = i + 1\n\n const partForm = new FormData()\n partForm.append('uploadId', uploadId)\n partForm.append('partNumber', String(partNumber))\n partForm.append('part', partBlob)\n\n const partRes = await fetchWithRetry(\n `${this.apiUrl}/api/upload/part`,\n {\n method: 'POST',\n headers: this.#authHeaders(),\n body: partForm,\n },\n signal,\n )\n const { etag } = await partRes.json() as UploadPartResponse\n\n etags.push({ partNumber, etag })\n uploadedBytes += partBlob.size\n onProgress?.(Math.round((uploadedBytes / file.size) * 95)) // reserve 5% for complete\n }\n\n // 3. Complete\n checkAbort(signal)\n const completeRes = await fetchWithRetry(\n `${this.apiUrl}/api/upload/complete`,\n {\n method: 'POST',\n headers: { ...this.#authHeaders(), 'Content-Type': 'application/json' },\n body: JSON.stringify({ uploadId, parts: etags }),\n },\n signal,\n )\n const body = await completeRes.json() as unknown\n onProgress?.(100)\n return this.#parseFileResult(body)\n }\n\n // ─── Response parser ───────────────────────────────────────────────────────\n\n #parseFileResult(body: unknown): FileResult {\n if (\n typeof body !== 'object' ||\n body === null ||\n typeof (body as Record<string, unknown>)['handle'] !== 'string' ||\n typeof (body as Record<string, unknown>)['url'] !== 'string'\n ) {\n throw new UploaderError('INVALID_RESPONSE', 'Unexpected response shape from upload API')\n }\n return body as FileResult\n }\n\n // ─── Public API ───────────────────────────────────────────────────────────\n\n /**\n * Upload a single file.\n *\n * Automatically selects single-shot vs multipart upload based on file size.\n * Emits progress via `opts.onProgress` (0–100). Respects `opts.signal` for\n * cancellation. Retries network errors and 5xx responses up to 3 times with\n * exponential backoff; 4xx errors are surfaced immediately.\n *\n * @throws {UploaderError} with code ABORTED | NETWORK_ERROR | SERVER_ERROR |\n * CLIENT_ERROR | INVALID_RESPONSE\n */\n async upload(\n file: File | Blob,\n opts: UploadOptions = {},\n ): Promise<FileResult> {\n const plan = planChunks(file, opts.chunkSize)\n\n if (plan.mode === 'single') {\n return this.#uploadSingleShot(file, opts)\n }\n return this.#uploadMultipart(file, plan.parts, opts)\n }\n\n /**\n * Upload multiple files with concurrency limiting.\n *\n * Resolves once all uploads settle (fulfilled or rejected). The returned\n * array preserves input order. `opts.concurrency` caps simultaneous\n * in-flight uploads (default 3). Each file shares the same opts\n * (including onProgress — the callback fires per-file, not aggregate).\n *\n * @returns Array of PromiseSettledResult in input order.\n */\n async uploadAll(\n files: Array<File | Blob>,\n opts: UploadAllOptions = {},\n ): Promise<PromiseSettledResult<FileResult>[]> {\n const concurrency = opts.concurrency ?? 3\n const results: PromiseSettledResult<FileResult>[] = new Array(files.length)\n\n let index = 0\n\n async function worker(client: UploaderClient): Promise<void> {\n while (index < files.length) {\n const i = index++\n const file = files[i]!\n try {\n results[i] = { status: 'fulfilled', value: await client.upload(file, opts) }\n } catch (err) {\n results[i] = { status: 'rejected', reason: err }\n }\n }\n }\n\n const workers = Array.from({ length: Math.min(concurrency, files.length) }, () =>\n worker(this),\n )\n await Promise.all(workers)\n return results\n }\n}\n","/**\n * Delivery / transform URL builder.\n *\n * The API serves transformed derivatives at `GET /<chain>/<handle>`, where\n * <chain> is a slash-joined list of ops (e.g. `resize=w:200,h:200,fit:crop`).\n * These op builders + `transformUrl()` construct that URL from an uploaded file\n * handle, so consumers render an image at any size/format/quality without\n * hand-assembling URL strings.\n *\n * The op/param types and serialization mirror @uploader/shared EXACTLY — the\n * API's `parseTransformChain` is the other half of this contract, and\n * `contract.conformance.ts` asserts at typecheck time that they stay in sync.\n */\n\n// ─── Op vocabulary (mirrors @uploader/shared) ────────────────────────────────\n\nexport type TransformOpName =\n | 'resize'\n | 'crop'\n | 'rotate'\n | 'flip'\n | 'flop'\n | 'quality'\n | 'output'\n\n/** Parameters for the resize operation. */\nexport type ResizeParams = {\n w?: number\n h?: number\n fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside' | 'crop'\n}\n\n/** Parameters for the crop operation. */\nexport type CropParams = {\n /** \"x,y,w,h\" notation. */\n dim: string\n x?: number\n y?: number\n w?: number\n h?: number\n}\n\n/** Parameters for the rotate operation. */\nexport type RotateParams = { deg: number }\n\n/** Parameters for the quality operation. */\nexport type QualityParams = { n: number }\n\n/** Parameters for the output operation. */\nexport type OutputParams = { format: string }\n\n/** A single transform operation with its typed params. */\nexport type TransformOp =\n | { name: 'resize'; params: ResizeParams }\n | { name: 'crop'; params: CropParams }\n | { name: 'rotate'; params: RotateParams }\n | { name: 'flip'; params: Record<string, never> }\n | { name: 'flop'; params: Record<string, never> }\n | { name: 'quality'; params: QualityParams }\n | { name: 'output'; params: OutputParams }\n\n/** An ordered list of transform operations. */\nexport type TransformChain = { ops: TransformOp[] }\n\n// ─── Op builders ─────────────────────────────────────────────────────────────\n\n/** Resize to a width and/or height with an optional fit mode. */\nexport const resize = (params: ResizeParams): TransformOp => ({ name: 'resize', params })\n\n/** Crop a rectangular region: origin (x, y) and size w×h, in source pixels. */\nexport const crop = (rect: { x: number; y: number; w: number; h: number }): TransformOp => ({\n name: 'crop',\n params: { dim: `${rect.x},${rect.y},${rect.w},${rect.h}`, ...rect },\n})\n\n/** Rotate clockwise by `deg` degrees. */\nexport const rotate = (params: RotateParams): TransformOp => ({ name: 'rotate', params })\n\n/** Flip vertically (mirror top↔bottom). */\nexport const flip = (): TransformOp => ({ name: 'flip', params: {} })\n\n/** Flop horizontally (mirror left↔right). */\nexport const flop = (): TransformOp => ({ name: 'flop', params: {} })\n\n/** Set output quality 1–100 (ignored by lossless formats). */\nexport const quality = (params: QualityParams): TransformOp => ({ name: 'quality', params })\n\n/** Convert the output format, e.g. `{ format: 'webp' }`. */\nexport const output = (params: OutputParams): TransformOp => ({ name: 'output', params })\n\n// ─── Serialization (vendored from @uploader/shared, parser-compatible) ────────\n\nfunction serializeOp(op: TransformOp): string {\n switch (op.name) {\n case 'resize': {\n const parts: string[] = []\n if (op.params.w !== undefined) parts.push(`w:${op.params.w}`)\n if (op.params.h !== undefined) parts.push(`h:${op.params.h}`)\n if (op.params.fit !== undefined) parts.push(`fit:${op.params.fit}`)\n return `resize=${parts.join(',')}`\n }\n case 'crop':\n return `crop=dim:${op.params.dim}`\n case 'rotate':\n return `rotate=deg:${op.params.deg}`\n case 'flip':\n return 'flip'\n case 'flop':\n return 'flop'\n case 'quality':\n return `quality=n:${op.params.n}`\n case 'output':\n return `output=format:${op.params.format}`\n }\n}\n\nexport type TransformUrlOptions = {\n /** File handle from an upload (`FileResult.handle`). */\n handle: string\n /** Ordered transform ops to apply. Must contain at least one op. */\n ops: TransformOp[]\n /**\n * Base API URL. Defaults to '' so the URL is relative — `/<chain>/<handle>` —\n * and resolves same-origin (e.g. proxied to the API). Pass an absolute URL to\n * point at a remote API directly.\n */\n apiUrl?: string\n}\n\n/**\n * Build a delivery URL that applies `ops` to the file identified by `handle`.\n *\n * @example\n * transformUrl({\n * handle: file.handle,\n * ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],\n * })\n * // => \"/resize=w:200,h:200,fit:crop/output=format:webp/<handle>\"\n */\nexport function transformUrl({ handle, ops, apiUrl = '' }: TransformUrlOptions): string {\n const base = apiUrl.replace(/\\/$/, '')\n const chain = ops.map(serializeOp).join('/')\n // The API's transform route requires at least one op segment before the\n // handle; for the unmodified original, use FileResult.url instead.\n return chain ? `${base}/${chain}/${handle}` : `${base}/${handle}`\n}\n"],"mappings":"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","names":[]}
|