@ibanzajoe/uploader 1.10.0 → 1.11.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/dist/{chunk-DJK2SQ5I.js → chunk-2KMFZQCS.js} +7 -4
- package/dist/chunk-2KMFZQCS.js.map +1 -0
- package/dist/{client-Brg4Ebyc.d.cts → client-Dwe7Zb6F.d.cts} +8 -0
- package/dist/{client-Brg4Ebyc.d.ts → client-Dwe7Zb6F.d.ts} +8 -0
- package/dist/core.cjs +6 -3
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +1 -1
- package/dist/index.cjs +6 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/server.cjs +6 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/{transform-BTZ0kodO.d.cts → transform-C6eOrkc3.d.cts} +37 -9
- package/dist/{transform-BTZ0kodO.d.ts → transform-C6eOrkc3.d.ts} +37 -9
- package/package.json +1 -1
- package/dist/chunk-DJK2SQ5I.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { D as DeliveryProtection, P as PickerResponse, a as FileResult, h as UploaderClientOptions } from './client-
|
|
2
|
-
export { F as FileManageOptions, L as ListFilesQuery, b as ListFilesResult, c as PolicySpec, S as SecurityPair, d as StoredFileRecord, e as StoredFileStatus, U as UploadAllOptions, f as UploadOptions, g as UploaderClient, i as UsageWarning } from './client-
|
|
3
|
-
export { C as CropParams, O as OutputParams, Q as QualityParams, R as ResizeParams, a as RotateParams, T as TransformChain, b as TransformOp, c as TransformOpName, d as TransformUrlOptions, e as crop, f as flip, g as flop, o as output, q as quality, r as resize, h as rotate, t as transformUrl, w as withSignedPolicy } from './transform-
|
|
1
|
+
import { D as DeliveryProtection, P as PickerResponse, a as FileResult, h as UploaderClientOptions } from './client-Dwe7Zb6F.cjs';
|
|
2
|
+
export { F as FileManageOptions, L as ListFilesQuery, b as ListFilesResult, c as PolicySpec, S as SecurityPair, d as StoredFileRecord, e as StoredFileStatus, U as UploadAllOptions, f as UploadOptions, g as UploaderClient, i as UsageWarning } from './client-Dwe7Zb6F.cjs';
|
|
3
|
+
export { C as CropParams, O as OutputParams, Q as QualityParams, R as ResizeParams, a as RotateParams, T as TransformChain, b as TransformOp, c as TransformOpName, d as TransformUrlOptions, e as crop, f as flip, g as flop, o as output, q as quality, r as resize, h as rotate, t as transformUrl, w as withSignedPolicy } from './transform-C6eOrkc3.cjs';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import React__default, { CSSProperties } from 'react';
|
|
6
6
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { D as DeliveryProtection, P as PickerResponse, a as FileResult, h as UploaderClientOptions } from './client-
|
|
2
|
-
export { F as FileManageOptions, L as ListFilesQuery, b as ListFilesResult, c as PolicySpec, S as SecurityPair, d as StoredFileRecord, e as StoredFileStatus, U as UploadAllOptions, f as UploadOptions, g as UploaderClient, i as UsageWarning } from './client-
|
|
3
|
-
export { C as CropParams, O as OutputParams, Q as QualityParams, R as ResizeParams, a as RotateParams, T as TransformChain, b as TransformOp, c as TransformOpName, d as TransformUrlOptions, e as crop, f as flip, g as flop, o as output, q as quality, r as resize, h as rotate, t as transformUrl, w as withSignedPolicy } from './transform-
|
|
1
|
+
import { D as DeliveryProtection, P as PickerResponse, a as FileResult, h as UploaderClientOptions } from './client-Dwe7Zb6F.js';
|
|
2
|
+
export { F as FileManageOptions, L as ListFilesQuery, b as ListFilesResult, c as PolicySpec, S as SecurityPair, d as StoredFileRecord, e as StoredFileStatus, U as UploadAllOptions, f as UploadOptions, g as UploaderClient, i as UsageWarning } from './client-Dwe7Zb6F.js';
|
|
3
|
+
export { C as CropParams, O as OutputParams, Q as QualityParams, R as ResizeParams, a as RotateParams, T as TransformChain, b as TransformOp, c as TransformOpName, d as TransformUrlOptions, e as crop, f as flip, g as flop, o as output, q as quality, r as resize, h as rotate, t as transformUrl, w as withSignedPolicy } from './transform-C6eOrkc3.js';
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import React__default, { CSSProperties } from 'react';
|
|
6
6
|
|
package/dist/index.js
CHANGED
package/dist/server.cjs
CHANGED
|
@@ -50,10 +50,13 @@ function serializeOp(op) {
|
|
|
50
50
|
return `output=format:${op.params.format}`;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
function transformUrl({ handle, ops, apiUrl = "" }) {
|
|
54
|
-
const
|
|
53
|
+
function transformUrl({ file, handle, ops, apiUrl = "" }) {
|
|
54
|
+
const id = file?.handle ?? handle;
|
|
55
|
+
if (!id) throw new Error("transformUrl: pass `file` or `handle`");
|
|
56
|
+
if (file && file.transformBase === null) return null;
|
|
57
|
+
const base = (file?.transformBase ?? apiUrl).replace(/\/$/, "");
|
|
55
58
|
const chain = ops.map(serializeOp).join("/");
|
|
56
|
-
return chain ? `${base}/${chain}/${
|
|
59
|
+
return chain ? `${base}/${chain}/${id}` : `${base}/${id}`;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
// src/server/index.ts
|
package/dist/server.cjs.map
CHANGED
|
@@ -1 +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 * Minimum seconds until the URL expires. Default: 300 (5 minutes).\n *\n * The actual expiry is rounded up to the next `stableWindow` boundary, so\n * the effective lifetime lands between `expiresIn` and `expiresIn +\n * stableWindow` — never shorter than you asked for.\n */\n expiresIn?: number\n /**\n * Quantize the expiry timestamp to a fixed window (seconds) so that repeated\n * calls for the same file within one window produce a **byte-identical URL**.\n * Default: the value of `expiresIn`. Pass `0` to disable.\n *\n * This matters much more than it looks. A signed URL's `?policy=` embeds an\n * absolute expiry, so computing it from \"now\" on every render yields a\n * different URL every time — and a different URL is, to a browser, a\n * different image. The browser cache can then NEVER hit, and every view of\n * every protected image pays a full network round trip no matter how well\n * the edge caches it.\n *\n * Quantizing fixes that: within a window the URL string is stable, so the\n * browser serves it straight from its own cache. The edge is unaffected\n * either way (its cache key deliberately excludes policy/signature, so all\n * valid signatures already share one entry) — this is purely about letting\n * the client cache work at all.\n *\n * Longer windows cache better; shorter windows revoke sooner. Pick per how\n * sensitive the content is.\n */\n stableWindow?: 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\nexport type GetUploadTokenOptions = {\n /**\n * The account's api-key **secret** (server-only — never expose to the browser).\n * The token is an HMAC over this secret; the upload plane verifies against the\n * file owner's secret.\n */\n secret: string\n /**\n * Seconds until the token expires. Default: 3600 (1 hour).\n *\n * Keep this SHORT. The token is handed to the browser, so a long lifetime is\n * effectively a second, un-revocable API key — mint a fresh one per upload\n * session instead. (Rotating the api-key secret is the only way to kill an\n * outstanding token before it expires, and that invalidates them all.)\n */\n expiresIn?: number\n /**\n * Which operations the token grants. Default: `['store']` — an upload token.\n * Only the verbs the API enforces are meaningful: `store`, `read`, `list`,\n * `remove`.\n */\n call?: string[]\n /** Cap the uploaded file size, in bytes (enforced at store time as a 403). */\n maxSize?: number\n /** Require a minimum uploaded file size, in bytes (enforced as a 403). */\n minSize?: number\n /**\n * Lock the token to a single file handle. Required by the API for a `read`\n * token (an unbound read policy would expose every file on the account); has\n * no effect on a plain `store` token.\n */\n handle?: string\n}\n\n/** A short-lived signed policy pair, ready to hand to the browser. */\nexport type UploadToken = {\n /** base64url policy — pass to the SDK as `security.policy`. */\n policy: string\n /** hex HMAC-SHA256 signature — pass to the SDK as `security.signature`. */\n signature: string\n /** ISO-8601 timestamp when the token stops working (for your frontend to refresh). */\n expiresAt: string\n}\n\n/**\n * Mint a short-lived **signed upload token** for an account that has\n * *Require signed policies* turned on. Returns a `{ policy, signature,\n * expiresAt }` pair to hand to your frontend, which passes `{ policy,\n * signature }` to the SDK as its `security` option.\n *\n * SERVER-ONLY: needs the account secret. Call it from an authenticated backend\n * route, per upload session — the secret never reaches the browser, and a\n * fresh short-lived token per session keeps a leaked token nearly worthless.\n *\n * @example\n * import { getUploadToken } from '@ibanzajoe/uploader/server'\n *\n * // in an authenticated backend route (e.g. GET /api/upload-token):\n * const token = getUploadToken({\n * secret: process.env.UPLOADER_API_SECRET!,\n * expiresIn: 3600, // 1 hour\n * maxSize: 25 * 1024 * 1024 // optional: cap at 25 MB\n * })\n * res.json(token) // → { policy, signature, expiresAt }\n *\n * // frontend then: new UploaderClient({ apikey, security: { policy, signature } })\n */\nexport function getUploadToken(options: GetUploadTokenOptions): UploadToken {\n const { secret, expiresIn = 3600, call = ['store'], maxSize, minSize, handle } = options\n if (!secret) throw new Error('getUploadToken: `secret` is required')\n if (!call.length) throw new Error('getUploadToken: `call` must grant at least one operation')\n if (call.includes('read') && !handle) {\n throw new Error(\"getUploadToken: a 'read' token requires `handle` (an unbound read grant would expose every file on the account)\")\n }\n\n const expirySeconds = Math.floor(Date.now() / 1000) + expiresIn\n const spec: Record<string, unknown> = { expiry: expirySeconds, call }\n if (handle) spec.handle = handle\n if (maxSize !== undefined) spec.maxSize = maxSize\n if (minSize !== undefined) spec.minSize = minSize\n\n const policy = encodePolicy(spec)\n const signature = createHmac('sha256', secret).update(policy).digest('hex')\n return { policy, signature, expiresAt: new Date(expirySeconds * 1000).toISOString() }\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 and hand the\n * returned URL to your frontend (e.g. as an `<img src>`).\n *\n * Calling it per view is fine and expected — by default the expiry is\n * quantized (`stableWindow`) so repeated calls inside one window return the\n * exact same URL string, which is what lets the browser cache the image\n * instead of re-fetching it on every render.\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: 3600, // valid ~1h, and stable for ~1h\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 const stableWindow = options.stableWindow ?? expiresIn\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 // Round the expiry UP to the next stableWindow boundary so every call within\n // a window signs the identical policy — and therefore returns the identical\n // URL, which the browser can actually cache. Rounding up (never down) means\n // the URL is always valid for at least `expiresIn`.\n const earliest = Math.floor(Date.now() / 1000) + expiresIn\n const expiry =\n stableWindow > 0 ? Math.ceil(earliest / stableWindow) * stableWindow : earliest\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":"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","names":[]}
|
|
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 * Minimum seconds until the URL expires. Default: 300 (5 minutes).\n *\n * The actual expiry is rounded up to the next `stableWindow` boundary, so\n * the effective lifetime lands between `expiresIn` and `expiresIn +\n * stableWindow` — never shorter than you asked for.\n */\n expiresIn?: number\n /**\n * Quantize the expiry timestamp to a fixed window (seconds) so that repeated\n * calls for the same file within one window produce a **byte-identical URL**.\n * Default: the value of `expiresIn`. Pass `0` to disable.\n *\n * This matters much more than it looks. A signed URL's `?policy=` embeds an\n * absolute expiry, so computing it from \"now\" on every render yields a\n * different URL every time — and a different URL is, to a browser, a\n * different image. The browser cache can then NEVER hit, and every view of\n * every protected image pays a full network round trip no matter how well\n * the edge caches it.\n *\n * Quantizing fixes that: within a window the URL string is stable, so the\n * browser serves it straight from its own cache. The edge is unaffected\n * either way (its cache key deliberately excludes policy/signature, so all\n * valid signatures already share one entry) — this is purely about letting\n * the client cache work at all.\n *\n * Longer windows cache better; shorter windows revoke sooner. Pick per how\n * sensitive the content is.\n */\n stableWindow?: 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\nexport type GetUploadTokenOptions = {\n /**\n * The account's api-key **secret** (server-only — never expose to the browser).\n * The token is an HMAC over this secret; the upload plane verifies against the\n * file owner's secret.\n */\n secret: string\n /**\n * Seconds until the token expires. Default: 3600 (1 hour).\n *\n * Keep this SHORT. The token is handed to the browser, so a long lifetime is\n * effectively a second, un-revocable API key — mint a fresh one per upload\n * session instead. (Rotating the api-key secret is the only way to kill an\n * outstanding token before it expires, and that invalidates them all.)\n */\n expiresIn?: number\n /**\n * Which operations the token grants. Default: `['store']` — an upload token.\n * Only the verbs the API enforces are meaningful: `store`, `read`, `list`,\n * `remove`.\n */\n call?: string[]\n /** Cap the uploaded file size, in bytes (enforced at store time as a 403). */\n maxSize?: number\n /** Require a minimum uploaded file size, in bytes (enforced as a 403). */\n minSize?: number\n /**\n * Lock the token to a single file handle. Required by the API for a `read`\n * token (an unbound read policy would expose every file on the account); has\n * no effect on a plain `store` token.\n */\n handle?: string\n}\n\n/** A short-lived signed policy pair, ready to hand to the browser. */\nexport type UploadToken = {\n /** base64url policy — pass to the SDK as `security.policy`. */\n policy: string\n /** hex HMAC-SHA256 signature — pass to the SDK as `security.signature`. */\n signature: string\n /** ISO-8601 timestamp when the token stops working (for your frontend to refresh). */\n expiresAt: string\n}\n\n/**\n * Mint a short-lived **signed upload token** for an account that has\n * *Require signed policies* turned on. Returns a `{ policy, signature,\n * expiresAt }` pair to hand to your frontend, which passes `{ policy,\n * signature }` to the SDK as its `security` option.\n *\n * SERVER-ONLY: needs the account secret. Call it from an authenticated backend\n * route, per upload session — the secret never reaches the browser, and a\n * fresh short-lived token per session keeps a leaked token nearly worthless.\n *\n * @example\n * import { getUploadToken } from '@ibanzajoe/uploader/server'\n *\n * // in an authenticated backend route (e.g. GET /api/upload-token):\n * const token = getUploadToken({\n * secret: process.env.UPLOADER_API_SECRET!,\n * expiresIn: 3600, // 1 hour\n * maxSize: 25 * 1024 * 1024 // optional: cap at 25 MB\n * })\n * res.json(token) // → { policy, signature, expiresAt }\n *\n * // frontend then: new UploaderClient({ apikey, security: { policy, signature } })\n */\nexport function getUploadToken(options: GetUploadTokenOptions): UploadToken {\n const { secret, expiresIn = 3600, call = ['store'], maxSize, minSize, handle } = options\n if (!secret) throw new Error('getUploadToken: `secret` is required')\n if (!call.length) throw new Error('getUploadToken: `call` must grant at least one operation')\n if (call.includes('read') && !handle) {\n throw new Error(\"getUploadToken: a 'read' token requires `handle` (an unbound read grant would expose every file on the account)\")\n }\n\n const expirySeconds = Math.floor(Date.now() / 1000) + expiresIn\n const spec: Record<string, unknown> = { expiry: expirySeconds, call }\n if (handle) spec.handle = handle\n if (maxSize !== undefined) spec.maxSize = maxSize\n if (minSize !== undefined) spec.minSize = minSize\n\n const policy = encodePolicy(spec)\n const signature = createHmac('sha256', secret).update(policy).digest('hex')\n return { policy, signature, expiresAt: new Date(expirySeconds * 1000).toISOString() }\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 and hand the\n * returned URL to your frontend (e.g. as an `<img src>`).\n *\n * Calling it per view is fine and expected — by default the expiry is\n * quantized (`stableWindow`) so repeated calls inside one window return the\n * exact same URL string, which is what lets the browser cache the image\n * instead of re-fetching it on every render.\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: 3600, // valid ~1h, and stable for ~1h\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 const stableWindow = options.stableWindow ?? expiresIn\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 // Round the expiry UP to the next stableWindow boundary so every call within\n // a window signs the identical policy — and therefore returns the identical\n // URL, which the browser can actually cache. Rounding up (never down) means\n // the URL is always valid for at least `expiresIn`.\n const earliest = Math.floor(Date.now() / 1000) + expiresIn\n const expiry =\n stableWindow > 0 ? Math.ceil(earliest / stableWindow) * stableWindow : earliest\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\n/** The subset of an upload result `transformUrl` reads when given `file`. */\nexport type TransformableFile = { handle: string; transformBase?: string | null; url?: string }\n\nexport type TransformUrlOptions = {\n /**\n * The upload result (`FileResult`, or a file-API record). Preferred: the\n * URL is built on `file.transformBase`, the host the API says this file's\n * transforms render on — which depends on the plan and the file's delivery\n * protection (public files on the CDN hostname, gated files and BYO Pro on\n * the edge). Building on the API origin instead 403s those files.\n */\n file?: TransformableFile\n /** File handle from an upload (`FileResult.handle`). Not needed with `file`. */\n handle?: string\n /** Ordered transform ops to apply. Must contain at least one op. */\n ops: TransformOp[]\n /**\n * Base URL, used only when `file.transformBase` is unavailable. Defaults to\n * '' so the URL is relative — `/<chain>/<handle>` — and resolves same-origin\n * (e.g. proxied to the API). Pass an absolute URL to point elsewhere.\n */\n apiUrl?: string\n}\n\n/**\n * Build a delivery URL that applies `ops` to a file.\n *\n * Pass the upload result as `file` and the URL lands on the host the API chose\n * for that file (`file.transformBase`). The API decides per plan and per file\n * where transforms render, exactly as it does for `FileResult.url`, so a URL\n * built on a guessed host can 403.\n *\n * Returns `null` when `file.transformBase` is `null` — the plan has no\n * server-side transforms (BYO Lite); use `editImage()` client-side instead.\n * With only `handle` (legacy form) the result is never null.\n *\n * @example\n * transformUrl({\n * file: result, // from onUploadDone / client.upload()\n * ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],\n * })\n * // => \"https://cdn.postila.app/resize=w:200,h:200,fit:crop/output=format:webp/<handle>\"\n */\nexport function transformUrl(options: TransformUrlOptions & { file: TransformableFile }): string | null\nexport function transformUrl(options: TransformUrlOptions & { handle: string }): string\nexport function transformUrl({ file, handle, ops, apiUrl = '' }: TransformUrlOptions): string | null {\n const id = file?.handle ?? handle\n if (!id) throw new Error('transformUrl: pass `file` or `handle`')\n if (file && file.transformBase === null) return null\n const base = (file?.transformBase ?? 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}/${id}` : `${base}/${id}`\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":"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","names":[]}
|
package/dist/server.d.cts
CHANGED
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -85,29 +85,57 @@ declare const flop: () => TransformOp;
|
|
|
85
85
|
declare const quality: (params: QualityParams) => TransformOp;
|
|
86
86
|
/** Convert the output format, e.g. `{ format: 'webp' }`. */
|
|
87
87
|
declare const output: (params: OutputParams) => TransformOp;
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
/** The subset of an upload result `transformUrl` reads when given `file`. */
|
|
89
|
+
type TransformableFile = {
|
|
90
90
|
handle: string;
|
|
91
|
+
transformBase?: string | null;
|
|
92
|
+
url?: string;
|
|
93
|
+
};
|
|
94
|
+
type TransformUrlOptions = {
|
|
95
|
+
/**
|
|
96
|
+
* The upload result (`FileResult`, or a file-API record). Preferred: the
|
|
97
|
+
* URL is built on `file.transformBase`, the host the API says this file's
|
|
98
|
+
* transforms render on — which depends on the plan and the file's delivery
|
|
99
|
+
* protection (public files on the CDN hostname, gated files and BYO Pro on
|
|
100
|
+
* the edge). Building on the API origin instead 403s those files.
|
|
101
|
+
*/
|
|
102
|
+
file?: TransformableFile;
|
|
103
|
+
/** File handle from an upload (`FileResult.handle`). Not needed with `file`. */
|
|
104
|
+
handle?: string;
|
|
91
105
|
/** Ordered transform ops to apply. Must contain at least one op. */
|
|
92
106
|
ops: TransformOp[];
|
|
93
107
|
/**
|
|
94
|
-
* Base
|
|
95
|
-
*
|
|
96
|
-
*
|
|
108
|
+
* Base URL, used only when `file.transformBase` is unavailable. Defaults to
|
|
109
|
+
* '' so the URL is relative — `/<chain>/<handle>` — and resolves same-origin
|
|
110
|
+
* (e.g. proxied to the API). Pass an absolute URL to point elsewhere.
|
|
97
111
|
*/
|
|
98
112
|
apiUrl?: string;
|
|
99
113
|
};
|
|
100
114
|
/**
|
|
101
|
-
* Build a delivery URL that applies `ops` to
|
|
115
|
+
* Build a delivery URL that applies `ops` to a file.
|
|
116
|
+
*
|
|
117
|
+
* Pass the upload result as `file` and the URL lands on the host the API chose
|
|
118
|
+
* for that file (`file.transformBase`). The API decides per plan and per file
|
|
119
|
+
* where transforms render, exactly as it does for `FileResult.url`, so a URL
|
|
120
|
+
* built on a guessed host can 403.
|
|
121
|
+
*
|
|
122
|
+
* Returns `null` when `file.transformBase` is `null` — the plan has no
|
|
123
|
+
* server-side transforms (BYO Lite); use `editImage()` client-side instead.
|
|
124
|
+
* With only `handle` (legacy form) the result is never null.
|
|
102
125
|
*
|
|
103
126
|
* @example
|
|
104
127
|
* transformUrl({
|
|
105
|
-
*
|
|
128
|
+
* file: result, // from onUploadDone / client.upload()
|
|
106
129
|
* ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],
|
|
107
130
|
* })
|
|
108
|
-
* // => "/resize=w:200,h:200,fit:crop/output=format:webp/<handle>"
|
|
131
|
+
* // => "https://cdn.postila.app/resize=w:200,h:200,fit:crop/output=format:webp/<handle>"
|
|
109
132
|
*/
|
|
110
|
-
declare function transformUrl(
|
|
133
|
+
declare function transformUrl(options: TransformUrlOptions & {
|
|
134
|
+
file: TransformableFile;
|
|
135
|
+
}): string | null;
|
|
136
|
+
declare function transformUrl(options: TransformUrlOptions & {
|
|
137
|
+
handle: string;
|
|
138
|
+
}): string;
|
|
111
139
|
/**
|
|
112
140
|
* Append a signed read policy to a delivery/transform URL for accounts on the
|
|
113
141
|
* `signed` delivery-protection tier (Private plan, docs/12).
|
|
@@ -85,29 +85,57 @@ declare const flop: () => TransformOp;
|
|
|
85
85
|
declare const quality: (params: QualityParams) => TransformOp;
|
|
86
86
|
/** Convert the output format, e.g. `{ format: 'webp' }`. */
|
|
87
87
|
declare const output: (params: OutputParams) => TransformOp;
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
/** The subset of an upload result `transformUrl` reads when given `file`. */
|
|
89
|
+
type TransformableFile = {
|
|
90
90
|
handle: string;
|
|
91
|
+
transformBase?: string | null;
|
|
92
|
+
url?: string;
|
|
93
|
+
};
|
|
94
|
+
type TransformUrlOptions = {
|
|
95
|
+
/**
|
|
96
|
+
* The upload result (`FileResult`, or a file-API record). Preferred: the
|
|
97
|
+
* URL is built on `file.transformBase`, the host the API says this file's
|
|
98
|
+
* transforms render on — which depends on the plan and the file's delivery
|
|
99
|
+
* protection (public files on the CDN hostname, gated files and BYO Pro on
|
|
100
|
+
* the edge). Building on the API origin instead 403s those files.
|
|
101
|
+
*/
|
|
102
|
+
file?: TransformableFile;
|
|
103
|
+
/** File handle from an upload (`FileResult.handle`). Not needed with `file`. */
|
|
104
|
+
handle?: string;
|
|
91
105
|
/** Ordered transform ops to apply. Must contain at least one op. */
|
|
92
106
|
ops: TransformOp[];
|
|
93
107
|
/**
|
|
94
|
-
* Base
|
|
95
|
-
*
|
|
96
|
-
*
|
|
108
|
+
* Base URL, used only when `file.transformBase` is unavailable. Defaults to
|
|
109
|
+
* '' so the URL is relative — `/<chain>/<handle>` — and resolves same-origin
|
|
110
|
+
* (e.g. proxied to the API). Pass an absolute URL to point elsewhere.
|
|
97
111
|
*/
|
|
98
112
|
apiUrl?: string;
|
|
99
113
|
};
|
|
100
114
|
/**
|
|
101
|
-
* Build a delivery URL that applies `ops` to
|
|
115
|
+
* Build a delivery URL that applies `ops` to a file.
|
|
116
|
+
*
|
|
117
|
+
* Pass the upload result as `file` and the URL lands on the host the API chose
|
|
118
|
+
* for that file (`file.transformBase`). The API decides per plan and per file
|
|
119
|
+
* where transforms render, exactly as it does for `FileResult.url`, so a URL
|
|
120
|
+
* built on a guessed host can 403.
|
|
121
|
+
*
|
|
122
|
+
* Returns `null` when `file.transformBase` is `null` — the plan has no
|
|
123
|
+
* server-side transforms (BYO Lite); use `editImage()` client-side instead.
|
|
124
|
+
* With only `handle` (legacy form) the result is never null.
|
|
102
125
|
*
|
|
103
126
|
* @example
|
|
104
127
|
* transformUrl({
|
|
105
|
-
*
|
|
128
|
+
* file: result, // from onUploadDone / client.upload()
|
|
106
129
|
* ops: [resize({ w: 200, h: 200, fit: 'crop' }), output({ format: 'webp' })],
|
|
107
130
|
* })
|
|
108
|
-
* // => "/resize=w:200,h:200,fit:crop/output=format:webp/<handle>"
|
|
131
|
+
* // => "https://cdn.postila.app/resize=w:200,h:200,fit:crop/output=format:webp/<handle>"
|
|
109
132
|
*/
|
|
110
|
-
declare function transformUrl(
|
|
133
|
+
declare function transformUrl(options: TransformUrlOptions & {
|
|
134
|
+
file: TransformableFile;
|
|
135
|
+
}): string | null;
|
|
136
|
+
declare function transformUrl(options: TransformUrlOptions & {
|
|
137
|
+
handle: string;
|
|
138
|
+
}): string;
|
|
111
139
|
/**
|
|
112
140
|
* Append a signed read policy to a delivery/transform URL for accounts on the
|
|
113
141
|
* `signed` delivery-protection tier (Private plan, docs/12).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ibanzajoe/uploader",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/transform.ts"],"sourcesContent":["/**\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":";AAmEO,IAAM,SAAS,CAAC,YAAuC,EAAE,MAAM,UAAU,OAAO;AAGhF,IAAM,OAAO,CAAC,UAAuE;AAAA,EAC1F,MAAM;AAAA,EACN,QAAQ,EAAE,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,KAAK;AACpE;AAGO,IAAM,SAAS,CAAC,YAAuC,EAAE,MAAM,UAAU,OAAO;AAGhF,IAAM,OAAO,OAAoB,EAAE,MAAM,QAAQ,QAAQ,CAAC,EAAE;AAG5D,IAAM,OAAO,OAAoB,EAAE,MAAM,QAAQ,QAAQ,CAAC,EAAE;AAG5D,IAAM,UAAU,CAAC,YAAwC,EAAE,MAAM,WAAW,OAAO;AAGnF,IAAM,SAAS,CAAC,YAAuC,EAAE,MAAM,UAAU,OAAO;AAIvF,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;AAgBO,SAAS,iBACd,KACA,UACQ;AACR,QAAM,MAAM,IAAI,SAAS,GAAG,IAAI,MAAM;AACtC,SAAO,GAAG,GAAG,GAAG,GAAG,UAAU,mBAAmB,SAAS,MAAM,CAAC,cAAc,mBAAmB,SAAS,SAAS,CAAC;AACtH;","names":[]}
|