@giveitsmaller/sdk 0.16.0 → 0.18.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/README.md CHANGED
@@ -69,6 +69,27 @@ Docs are published in the [giveitsmaller-sdks](https://github.com/AntonioCS/give
69
69
  - **Troubleshooting** — [`docs/typescript/troubleshooting.md`](https://github.com/AntonioCS/giveitsmaller-sdks/blob/main/docs/typescript/troubleshooting.md)
70
70
  - **Examples** — compress, thumbnail, convert, merge, archive — [`docs/typescript/examples/`](https://github.com/AntonioCS/giveitsmaller-sdks/tree/main/docs/typescript/examples)
71
71
 
72
+ ## Contributing: the committed `dist/`
73
+
74
+ Unlike most packages, `packages/typescript/dist/` is **committed**, not gitignored.
75
+ This is deliberate: consumers that install the SDK via the `file:` protocol (the
76
+ e2e canary, the frontend, the API repo, local dev) get whatever is on disk — npm
77
+ does **not** run `prepare`/`prepack` for `file:` deps, so a `dist/` that lagged
78
+ behind `src/` would silently ship stale exports.
79
+
80
+ When you change anything under `src/`, rebuild and commit `dist/`:
81
+
82
+ ```bash
83
+ npm run build # tsc → dist/
84
+ ```
85
+
86
+ CI enforces this with a freshness guard that rebuilds `dist/` and fails the PR on
87
+ any `git diff` against the committed tree (mirroring the `git diff --exit-code
88
+ generated/` drift rule). `typescript` is pinned to an exact version so the rebuild
89
+ is reproducible. New hand-written SDK packages (PHP/Python) that grow a build step
90
+ should follow the same commit-`dist`-and-guard convention to avoid reintroducing
91
+ the `file:` gap.
92
+
72
93
  ## License
73
94
 
74
95
  MIT — see the [LICENSE](https://github.com/AntonioCS/giveitsmaller-sdks/blob/main/LICENSE) file.
package/dist/_audit.js CHANGED
@@ -68,6 +68,17 @@ export function _runAudit() {
68
68
  accept();
69
69
  // T2 / xVDTIm8C — operation-builder surface.
70
70
  accept();
71
+ // 8yqUXLCS — pin the credits/limits accessor SIGNATURES on ErgonomicClient.
72
+ // accept<ErgonomicClient>() proves the type compiles; these prove the three
73
+ // methods EXIST and their signatures/return types match (indexed access errors
74
+ // if a method is missing; the typed LHS errors if the signature drifts). The
75
+ // RHS is a type-only cast (`null as unknown as …`) — no runtime property read.
76
+ const _creditsSig = null;
77
+ const _creditsUsageSig = null;
78
+ const _limitsSig = null;
79
+ void _creditsSig;
80
+ void _creditsUsageSig;
81
+ void _limitsSig;
71
82
  accept();
72
83
  accept();
73
84
  accept();
@@ -113,8 +124,6 @@ export function _runAudit() {
113
124
  accept();
114
125
  accept();
115
126
  accept();
116
- accept();
117
- accept();
118
127
  // T4b / 27rE1fZn — preset resolver public types.
119
128
  accept();
120
129
  accept();
@@ -136,6 +145,7 @@ export function _runAudit() {
136
145
  accept();
137
146
  accept();
138
147
  accept();
148
+ accept();
139
149
  // FF5a / Ao8RPVxD — file-first Handle reattach surface.
140
150
  accept();
141
151
  accept();
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AudioWatermarkDecodeRequest, AudioWatermarkDecodeResponse, ExternalImportCreatedResponse, ExternalImportRequest, LoginUserRequest, LoginUser200ResponseData, ContactRequest, CreditsBalanceResponse, CreditsUsageResponse, UploadResponse, UploadProbeResponse, WorkflowCancelResponse, WorkflowCreateResponse, WorkflowResumeResponse, WorkflowStatusResponse, WorkflowListResponse, WorkflowSummary, WorkflowDownloadResponse, MetadataResponse, RetryResponse } from '@giveitsmaller/contracts/openapi';
1
+ import type { AudioWatermarkDecodeRequest, AudioWatermarkDecodeResponse, ExternalImportCreatedResponse, ExternalImportRequest, LoginUserRequest, LoginUser200ResponseData, ContactRequest, AccountLimits, CreditsBalanceResponse, CreditsUsageResponse, UploadResponse, UploadProbeResponse, WorkflowCancelResponse, WorkflowCreateResponse, WorkflowResumeResponse, WorkflowStatusResponse, WorkflowListResponse, WorkflowSummary, WorkflowDownloadResponse, MetadataResponse, RetryResponse } from '@giveitsmaller/contracts/openapi';
2
2
  import type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, GislClientConfig, GislSseEvent, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, ReadCapabilityOptions, UploadOptions, WaitOptions, WorkflowCreatePayload, _Sdk3HandCodedKeepaliveResult, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignPartsResult } from './types.js';
3
3
  export declare const MULTIPART_CONCURRENCY_DEFAULT: 4;
4
4
  export declare const DEFAULT_MULTIPART_FIRST_CHUNK_SIZE: number;
@@ -245,6 +245,13 @@ export declare class GislClient {
245
245
  * off the error envelope.
246
246
  */
247
247
  getCreditsBalance(): Promise<CreditsBalanceResponse>;
248
+ /**
249
+ * Fetch the caller's effective account limits (the tier-resolved caps:
250
+ * upload/merge size + total caps, surfaced override-aware). `GET
251
+ * /api/v2/account/limits`. The success envelope's `data` is unwrapped to the
252
+ * {@link AccountLimits} model (mirrors {@link getCreditsBalance}).
253
+ */
254
+ getAccountLimits(): Promise<AccountLimits>;
248
255
  /**
249
256
  * Authenticate with email/password. On success the server issues a
250
257
  * session cookie via `Set-Cookie`; subsequent requests authenticate
package/dist/client.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // blobByteSource, which never touches these). Kept as a STATIC import (not a
5
5
  // dynamic one) so `vi.mock('node:fs/promises')` still intercepts it in tests.
6
6
  import { open, stat, basename } from './node-fs.js';
7
- import { AudioWatermarkDecodeRequestToJSON, AudioWatermarkDecodeResponseFromJSON, ExternalImportCreatedResponseFromJSON, ExternalImportRequestToJSON, LoginUser200ResponseDataFromJSON, CreditsBalanceResponseFromJSON, CreditsUsageResponseFromJSON, UploadResponseFromJSON, UploadProbeResponseFromJSON, MultipartInitiateResponseFromJSON, MultipartInitiateRequestMetadataHintToJSON, MultipartCompleteResponseFromJSON, MultipartCompleteRequestToJSON, WorkflowCancelResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowResumeResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowListResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, AuthErrorResponseFromJSON, AuthErrorType, AuthRejectionEnvelopeFromJSON, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponseFromJSON, BalanceExhaustedResponseRequiredActionEnum, FeatureNotAvailableResponseFromJSON, FeatureTierRestrictedResponseFromJSON, TierRestrictionKind, TierRestrictionResponseFromJSON, UserTier, WorkflowExpiredResponseFromJSON, ProbePendingResponseFromJSON, UploadSizeExceedsTierResponseFromJSON, UploadDurationExceedsTierResponseFromJSON, UploadConstraintsAppliedProcessingClassPreAssignmentEnum, UploadThresholdsSingleShotMaxBytesEnum, UploadThresholdsMultipartChunkSizeEnum, UploadThresholdsMultipartConcurrencyDefaultEnum, } from '@giveitsmaller/contracts/openapi';
7
+ import { AudioWatermarkDecodeRequestToJSON, AudioWatermarkDecodeResponseFromJSON, ExternalImportCreatedResponseFromJSON, ExternalImportRequestToJSON, LoginUser200ResponseDataFromJSON, AccountLimitsFromJSON, CreditsBalanceResponseFromJSON, CreditsUsageResponseFromJSON, UploadResponseFromJSON, UploadProbeResponseFromJSON, MultipartInitiateResponseFromJSON, MultipartInitiateRequestMetadataHintToJSON, MultipartCompleteResponseFromJSON, MultipartCompleteRequestToJSON, WorkflowCancelResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowResumeResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowListResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, AuthErrorResponseFromJSON, AuthErrorType, AuthRejectionEnvelopeFromJSON, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponseFromJSON, BalanceExhaustedResponseRequiredActionEnum, FeatureNotAvailableResponseFromJSON, FeatureTierRestrictedResponseFromJSON, TierRestrictionKind, TierRestrictionResponseFromJSON, UserTier, WorkflowExpiredResponseFromJSON, ProbePendingResponseFromJSON, UploadSizeExceedsTierResponseFromJSON, UploadDurationExceedsTierResponseFromJSON, UploadConstraintsAppliedProcessingClassPreAssignmentEnum, UploadThresholdsSingleShotMaxBytesEnum, UploadThresholdsMultipartChunkSizeEnum, UploadThresholdsMultipartConcurrencyDefaultEnum, } from '@giveitsmaller/contracts/openapi';
8
8
  import { GislAbortError, GislApiError, GislAuthError, GislAuthRejectionError, GislBalanceExhaustedError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
9
9
  import { parseSseStream } from './sse.js';
10
10
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -1934,6 +1934,17 @@ export class GislClient {
1934
1934
  deserialize: CreditsBalanceResponseFromJSON,
1935
1935
  });
1936
1936
  }
1937
+ /**
1938
+ * Fetch the caller's effective account limits (the tier-resolved caps:
1939
+ * upload/merge size + total caps, surfaced override-aware). `GET
1940
+ * /api/v2/account/limits`. The success envelope's `data` is unwrapped to the
1941
+ * {@link AccountLimits} model (mirrors {@link getCreditsBalance}).
1942
+ */
1943
+ async getAccountLimits() {
1944
+ return this.request('GET', '/api/v2/account/limits', {
1945
+ deserialize: AccountLimitsFromJSON,
1946
+ });
1947
+ }
1937
1948
  // -----------------------------------------------------------------------
1938
1949
  // Auth
1939
1950
  // -----------------------------------------------------------------------
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Route-aware image "Output" model (card YNLrGhNo, contracts tewB37Jg / v2.97.0).
3
+ *
4
+ * The file-first `output()`/`resize()` helpers resolve a single user-facing
5
+ * "Output" operation to the right underlying wire op + options, driven by the
6
+ * contract's `accepted-options/image-output-routes.json` projection. The route is
7
+ * `(input format token, output_format token)`:
8
+ * - `same_format` (output == input) → `source_op: compress` (libcaesium optimiser),
9
+ * wire `output_format: 'original'`;
10
+ * - `format_change` (output != input) → `source_op: convert` (transcoder),
11
+ * wire `output_format: <token>`.
12
+ *
13
+ * Each route cell lists the options the worker HONORS (live) and PLANS (advertised,
14
+ * not yet honored — gated unavailable). Resize (`width`/`height`/`fit`) is
15
+ * **INPUT-keyed**: it lives only on the `same_format[input]` cell but applies on
16
+ * EITHER route, gated by input resizability (raster only — `svg` is vector and
17
+ * carries no resize). So a `png → webp + resize` request reads its resize
18
+ * capability from `same_format.png` and its transcoder options from
19
+ * `format_change.webp`.
20
+ *
21
+ * This hand table MIRRORS the generated projection and is PINNED to it by
22
+ * `output-route-conformance.test.ts` (the watermark-capability-gate precedent) —
23
+ * a contract regen that changes a route's source_op / honored / planned options
24
+ * fails that test. Kept a hand table (not a runtime JSON read) so the gate is
25
+ * browser-safe, exactly like {@link WATERMARK_CAPABILITY}. Mirrored by the PHP
26
+ * `ImageOutputRoutes`.
27
+ */
28
+ /** The resize option keys — input-keyed, raster-only (see module doc). */
29
+ export declare const RESIZE_KEYS: readonly ["width", "height", "fit"];
30
+ /** Image area cap shared by every resizable route (projection `max_output_pixels`). */
31
+ export declare const MAX_OUTPUT_PIXELS = 16000000;
32
+ /**
33
+ * Output formats reachable via the legacy `compress(output_format=…)` facade
34
+ * (projection `facade_managed_outputs`). Used ONLY as the undetectable-input
35
+ * fallback — a detectable input always routes via `source_op`.
36
+ */
37
+ export declare const FACADE_MANAGED_OUTPUTS: readonly string[];
38
+ interface RouteCell {
39
+ readonly honored: readonly string[];
40
+ readonly planned: readonly string[];
41
+ }
42
+ /**
43
+ * Per-route, per-output-format honored + planned option keys, mirroring
44
+ * `image-output-routes.json` `media.image`. `source_op` is uniform
45
+ * (same_format→compress, format_change→convert) so it is a derivation rule, not
46
+ * a table column. `same_format` is keyed by the INPUT token; `format_change` by
47
+ * the OUTPUT token.
48
+ */
49
+ export declare const IMAGE_OUTPUT_ROUTES: {
50
+ readonly same_format: Readonly<Record<string, RouteCell>>;
51
+ readonly format_change: Readonly<Record<string, RouteCell>>;
52
+ };
53
+ export type OutputRoute = 'same_format' | 'format_change';
54
+ /** A resolved Output lowering target. */
55
+ export interface ResolvedOutputRoute {
56
+ readonly route: OutputRoute;
57
+ /** The wire op to emit. */
58
+ readonly sourceOp: 'compress' | 'convert';
59
+ /** The wire `output_format` value ('original' for same_format, the token for format_change). */
60
+ readonly outputFormatWire: string;
61
+ /** The input format token the route resolved against (for per-value gating). */
62
+ readonly inputToken: string;
63
+ /** Effective honored option keys (incl. input-keyed resize on format_change). */
64
+ readonly honored: ReadonlySet<string>;
65
+ /** Planned option keys → gate as `feature_not_available`. */
66
+ readonly planned: ReadonlySet<string>;
67
+ }
68
+ /** The bare format token for a MIME type, or undefined if not a known image MIME. */
69
+ export declare function tokenForMime(mime: string): string | undefined;
70
+ /** The bare format token for a filename / path extension, or undefined. */
71
+ export declare function tokenForPath(path: string): string | undefined;
72
+ /** Every image format token the projection knows (for validation / tests). */
73
+ export declare function knownImageTokens(): ReadonlySet<string>;
74
+ /**
75
+ * Resolve an Output request to its wire op + gating sets. Returns undefined when
76
+ * the route is unrepresentable (e.g. converting TO a format no `format_change`
77
+ * cell covers). `outputFormat` undefined → same-format (keep input format).
78
+ */
79
+ export declare function resolveOutputRoute(inputToken: string, outputFormat: string | undefined): ResolvedOutputRoute | undefined;
80
+ /**
81
+ * Whether a specific VALUE of an option is `availability: 'planned'` for the
82
+ * given input format — the per-value gate (e.g. `metadata: 'keep'` is planned
83
+ * even though the `metadata` key is honored). Reads the generated
84
+ * `compressMetadata` `per_value_availability`; same_format only (the only route
85
+ * where value-level options like `metadata` are honored). Returns false when the
86
+ * option / value / group is unknown (no gate).
87
+ */
88
+ export declare function isPlannedValue(inputToken: string, optionKey: string, value: unknown): boolean;
89
+ export {};
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Route-aware image "Output" model (card YNLrGhNo, contracts tewB37Jg / v2.97.0).
3
+ *
4
+ * The file-first `output()`/`resize()` helpers resolve a single user-facing
5
+ * "Output" operation to the right underlying wire op + options, driven by the
6
+ * contract's `accepted-options/image-output-routes.json` projection. The route is
7
+ * `(input format token, output_format token)`:
8
+ * - `same_format` (output == input) → `source_op: compress` (libcaesium optimiser),
9
+ * wire `output_format: 'original'`;
10
+ * - `format_change` (output != input) → `source_op: convert` (transcoder),
11
+ * wire `output_format: <token>`.
12
+ *
13
+ * Each route cell lists the options the worker HONORS (live) and PLANS (advertised,
14
+ * not yet honored — gated unavailable). Resize (`width`/`height`/`fit`) is
15
+ * **INPUT-keyed**: it lives only on the `same_format[input]` cell but applies on
16
+ * EITHER route, gated by input resizability (raster only — `svg` is vector and
17
+ * carries no resize). So a `png → webp + resize` request reads its resize
18
+ * capability from `same_format.png` and its transcoder options from
19
+ * `format_change.webp`.
20
+ *
21
+ * This hand table MIRRORS the generated projection and is PINNED to it by
22
+ * `output-route-conformance.test.ts` (the watermark-capability-gate precedent) —
23
+ * a contract regen that changes a route's source_op / honored / planned options
24
+ * fails that test. Kept a hand table (not a runtime JSON read) so the gate is
25
+ * browser-safe, exactly like {@link WATERMARK_CAPABILITY}. Mirrored by the PHP
26
+ * `ImageOutputRoutes`.
27
+ */
28
+ import { compressMetadata } from '@giveitsmaller/contracts/operations';
29
+ /** The resize option keys — input-keyed, raster-only (see module doc). */
30
+ export const RESIZE_KEYS = ['width', 'height', 'fit'];
31
+ /** Image area cap shared by every resizable route (projection `max_output_pixels`). */
32
+ export const MAX_OUTPUT_PIXELS = 16_000_000;
33
+ /**
34
+ * Output formats reachable via the legacy `compress(output_format=…)` facade
35
+ * (projection `facade_managed_outputs`). Used ONLY as the undetectable-input
36
+ * fallback — a detectable input always routes via `source_op`.
37
+ */
38
+ export const FACADE_MANAGED_OUTPUTS = ['webp'];
39
+ /** Canonical MIME → bare format token (projection `mime_tokens`). */
40
+ const MIME_TOKEN = {
41
+ 'image/avif': 'avif',
42
+ 'image/gif': 'gif',
43
+ 'image/jpeg': 'jpeg',
44
+ 'image/png': 'png',
45
+ 'image/svg+xml': 'svg',
46
+ 'image/tiff': 'tiff',
47
+ 'image/webp': 'webp',
48
+ };
49
+ /** File extension → bare format token (for path / named-blob inputs). */
50
+ const EXT_TOKEN = {
51
+ jpg: 'jpeg', jpeg: 'jpeg', jpe: 'jpeg', jfif: 'jpeg',
52
+ png: 'png', webp: 'webp', gif: 'gif', avif: 'avif',
53
+ tif: 'tiff', tiff: 'tiff', svg: 'svg',
54
+ };
55
+ /**
56
+ * Per-route, per-output-format honored + planned option keys, mirroring
57
+ * `image-output-routes.json` `media.image`. `source_op` is uniform
58
+ * (same_format→compress, format_change→convert) so it is a derivation rule, not
59
+ * a table column. `same_format` is keyed by the INPUT token; `format_change` by
60
+ * the OUTPUT token.
61
+ */
62
+ export const IMAGE_OUTPUT_ROUTES = {
63
+ same_format: {
64
+ avif: { honored: ['avif_speed', 'fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
65
+ gif: { honored: ['fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
66
+ jpeg: { honored: ['fit', 'height', 'lossless', 'metadata', 'output_format', 'progressive', 'quality', 'width'], planned: [] },
67
+ png: { honored: ['fit', 'height', 'metadata', 'optimization_level', 'output_format', 'quality', 'width'], planned: ['lossy'] },
68
+ svg: { honored: ['metadata', 'output_format', 'quality'], planned: [] },
69
+ tiff: { honored: ['fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
70
+ webp: { honored: ['fit', 'height', 'lossless', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
71
+ },
72
+ format_change: {
73
+ avif: { honored: ['output_format', 'quality'], planned: [] },
74
+ gif: { honored: ['output_format'], planned: [] },
75
+ jpeg: { honored: ['background', 'output_format', 'quality'], planned: [] },
76
+ png: { honored: ['output_format'], planned: [] },
77
+ tiff: { honored: ['output_format'], planned: [] },
78
+ webp: { honored: ['output_format', 'quality'], planned: [] },
79
+ },
80
+ };
81
+ /** The bare format token for a MIME type, or undefined if not a known image MIME. */
82
+ export function tokenForMime(mime) {
83
+ return MIME_TOKEN[mime.split(';')[0].trim().toLowerCase()];
84
+ }
85
+ /** The bare format token for a filename / path extension, or undefined. */
86
+ export function tokenForPath(path) {
87
+ const ext = path.toLowerCase().split('.').pop();
88
+ return ext !== undefined ? EXT_TOKEN[ext] : undefined;
89
+ }
90
+ /** Every image format token the projection knows (for validation / tests). */
91
+ export function knownImageTokens() {
92
+ return new Set(Object.keys(IMAGE_OUTPUT_ROUTES.same_format));
93
+ }
94
+ /**
95
+ * Resolve an Output request to its wire op + gating sets. Returns undefined when
96
+ * the route is unrepresentable (e.g. converting TO a format no `format_change`
97
+ * cell covers). `outputFormat` undefined → same-format (keep input format).
98
+ */
99
+ export function resolveOutputRoute(inputToken, outputFormat) {
100
+ const outToken = outputFormat ?? inputToken;
101
+ if (outToken === inputToken) {
102
+ const cell = IMAGE_OUTPUT_ROUTES.same_format[inputToken];
103
+ if (cell === undefined)
104
+ return undefined;
105
+ return {
106
+ route: 'same_format',
107
+ sourceOp: 'compress',
108
+ outputFormatWire: 'original',
109
+ inputToken,
110
+ honored: new Set(cell.honored),
111
+ planned: new Set(cell.planned),
112
+ };
113
+ }
114
+ const cell = IMAGE_OUTPUT_ROUTES.format_change[outToken];
115
+ if (cell === undefined)
116
+ return undefined;
117
+ // Input-keyed resize: the format_change cell carries only transcoder options;
118
+ // resize capability comes from the INPUT's same_format cell (raster only).
119
+ const inCell = IMAGE_OUTPUT_ROUTES.same_format[inputToken];
120
+ const resize = inCell ? RESIZE_KEYS.filter((k) => inCell.honored.includes(k)) : [];
121
+ return {
122
+ route: 'format_change',
123
+ sourceOp: 'convert',
124
+ outputFormatWire: outToken,
125
+ inputToken,
126
+ honored: new Set([...cell.honored, ...resize]),
127
+ planned: new Set(cell.planned),
128
+ };
129
+ }
130
+ /** Input token → its `compress.image*` mime-group name (for per-value availability lookup). */
131
+ function compressGroupForToken(token) {
132
+ if (token === 'jpeg')
133
+ return 'image_jpeg';
134
+ if (token === 'png')
135
+ return 'image_png';
136
+ if (token === 'avif')
137
+ return 'image_avif';
138
+ return 'image'; // webp / gif / svg / tiff
139
+ }
140
+ /**
141
+ * Whether a specific VALUE of an option is `availability: 'planned'` for the
142
+ * given input format — the per-value gate (e.g. `metadata: 'keep'` is planned
143
+ * even though the `metadata` key is honored). Reads the generated
144
+ * `compressMetadata` `per_value_availability`; same_format only (the only route
145
+ * where value-level options like `metadata` are honored). Returns false when the
146
+ * option / value / group is unknown (no gate).
147
+ */
148
+ export function isPlannedValue(inputToken, optionKey, value) {
149
+ const group = compressMetadata.mime_groups[compressGroupForToken(inputToken)];
150
+ const opt = group?.options[optionKey];
151
+ if (opt === undefined)
152
+ return false;
153
+ const entry = opt.per_value_availability[String(value)];
154
+ return entry?.availability === 'planned';
155
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Typed per-op option interfaces for the ergonomic verbs (card Dhje3Faq).
3
+ * These replace the untyped `Record<string, unknown>` bags so the IDE can offer
4
+ * key completion and `tsc` rejects typos. The KEY SET of each interface is pinned
5
+ * to the contract two ways: (1) the source-level `Equal<...>` assertions below tie
6
+ * each interface to its `*_OPTION_KEYS` tuple at `tsc` time; (2) the wire-key
7
+ * conformance guard ties each tuple (∪ positional-owned) to the generated
8
+ * `OperationMetadata` at test time. Value types are best-effort (per-value/enum
9
+ * sync is out of scope — keys are the contract anchor).
10
+ *
11
+ * Keys a verb owns via a positional argument are EXCLUDED from its interface
12
+ * (`output_format` on convert, `text` on textWatermark) — they are set by the
13
+ * first argument and rejected if supplied in the bag (see `option_validation.ts`).
14
+ *
15
+ * Mirrored by the PHP array-shape docblocks — keep in lockstep.
16
+ */
17
+ /** 9-grid anchor shared by text/image/video watermark. */
18
+ export type WatermarkAnchor = 'top_left' | 'top_center' | 'top_right' | 'center_left' | 'center' | 'center_right' | 'bottom_left' | 'bottom_center' | 'bottom_right';
19
+ export interface ConvertOptions {
20
+ /** Output quality for lossy image formats (1-100). */
21
+ quality?: number;
22
+ /** Background colour (hex) for transparent images → JPEG. */
23
+ background?: string;
24
+ /** Video Constant Rate Factor (0 best — 51 worst). */
25
+ crf?: number;
26
+ /** Trim from the start, in seconds (video). */
27
+ trim_start?: number;
28
+ /** Trim from the end, in seconds (video). */
29
+ trim_end?: number;
30
+ /** Output frame rate for video → GIF. */
31
+ fps?: number;
32
+ /** Max output width in pixels (video → GIF downscale cap). */
33
+ width?: number;
34
+ /** GIF palette size (2-256). */
35
+ max_colors?: number;
36
+ /** GIF loop count (0 infinite, N>0 N times, -1 once). */
37
+ loop?: number;
38
+ /** GIF dithering method. */
39
+ dither?: 'none' | 'bayer' | 'floyd_steinberg' | 'sierra2' | 'sierra2_4a';
40
+ /** Output bitrate in kbps (lossy audio). */
41
+ bitrate?: 64 | 96 | 128 | 192 | 256 | 320;
42
+ /** Page selection for PDF → image (e.g. '1-5,8'). */
43
+ pages?: string;
44
+ /** Render resolution in DPI for PDF → image. */
45
+ dpi?: number;
46
+ }
47
+ export interface ThumbnailOptions {
48
+ /** Target width in pixels (1-16384). REQUIRED. */
49
+ width: number;
50
+ /** Target height in pixels (1-16384). REQUIRED. */
51
+ height: number;
52
+ /** Resize mode. */
53
+ fit?: 'max' | 'crop' | 'scale';
54
+ /** Output format for the thumbnail. */
55
+ format?: 'jpg' | 'png' | 'webp';
56
+ /** Output quality for lossy thumbnail formats (image input). */
57
+ quality?: number;
58
+ /** Frame timestamp for video input (e.g. '00:00:01'). */
59
+ timestamp?: string;
60
+ /** Document source: a printed page or the cover. */
61
+ source?: 'page' | 'cover';
62
+ /** 1-based page index for document input. */
63
+ page?: number;
64
+ }
65
+ export interface TextWatermarkOptions {
66
+ /** Font size in pixels (8-512). */
67
+ font_size?: number;
68
+ /** Text colour as hex RGB/RGBA (e.g. '#FFFFFF80'). */
69
+ color?: string;
70
+ /** Font family (bundled). */
71
+ font_family?: 'liberation_sans';
72
+ /** Rotation angle in degrees (-360..360). */
73
+ rotation?: number;
74
+ /** Rendering mode. */
75
+ watermark_mode?: 'single' | 'tiled';
76
+ /** Spacing between tiled labels in pixels (tiled mode). */
77
+ tile_spacing?: number;
78
+ /** 9-grid anchor position. */
79
+ anchor?: WatermarkAnchor;
80
+ /** Horizontal offset from the anchor (e.g. '40px' or '5%'). */
81
+ margin_x?: string;
82
+ /** Vertical offset from the anchor. */
83
+ margin_y?: string;
84
+ /** Overlay opacity (0-1). */
85
+ opacity?: number;
86
+ }
87
+ export interface WatermarkOptions {
88
+ /** 9-grid anchor position. */
89
+ anchor?: WatermarkAnchor;
90
+ /** Horizontal offset from the anchor (e.g. '40px' or '5%'). */
91
+ margin_x?: string;
92
+ /** Vertical offset from the anchor. */
93
+ margin_y?: string;
94
+ /** Overlay opacity (0-1). */
95
+ opacity?: number;
96
+ /** Overlay width (e.g. '120px' or '20%'). */
97
+ overlay_width?: string;
98
+ }
99
+ /** Resize mode (contract `fit` enum, v2.97.0). */
100
+ export type OutputFit = 'max' | 'crop' | 'scale';
101
+ /** Metadata policy (contract `metadata` enum). Both values stable since v2.102.0 (keep/strip un-parked). */
102
+ export type OutputMetadata = 'all' | 'keep';
103
+ /**
104
+ * Options for the file-first `output()` image transform. The KEY SET is the
105
+ * UNION of every image route's honored + planned option keys (image-output-routes
106
+ * projection); the PER-ROUTE honored/planned narrowing happens in the lowering
107
+ * (`resolveOutputRoute`), so supplying an option not honored on the resolved
108
+ * route (or a planned one) throws pre-upload. `output_format` is set via the
109
+ * positional `format` argument, so it is excluded here. Resize (`width`/`height`/
110
+ * `fit`) is honored on raster routes; `height` is optional (width-only resize).
111
+ */
112
+ export interface OutputOptions {
113
+ /** Output quality for lossy formats (1-100). Honored: avif/jpeg/webp routes. */
114
+ quality?: number;
115
+ /** Resize target width in px (1-16384; width*height <= 16MP). */
116
+ width?: number;
117
+ /** Resize target height in px (optional — width-only resize preserves aspect). */
118
+ height?: number;
119
+ /** Resize mode (applies when width or height is set). */
120
+ fit?: OutputFit;
121
+ /** Background colour (hex) for transparent images → JPEG. Honored: format_change→jpeg only. */
122
+ background?: string;
123
+ /** Progressive JPEG. Honored: same_format jpeg only. */
124
+ progressive?: boolean;
125
+ /** PNG lossless optimisation effort. Honored: same_format png only. */
126
+ optimization_level?: number;
127
+ /** AVIF encode speed. Honored: same_format avif only. */
128
+ avif_speed?: number;
129
+ /** Metadata policy. Honored: same_format routes (both `all` and `keep` since v2.102.0). */
130
+ metadata?: OutputMetadata;
131
+ /** JPEG/WebP lossless. Honored: same_format jpeg/webp (stable since v2.101.0). */
132
+ lossless?: boolean;
133
+ /** Lossy PNG quantization. PLANNED (gated unavailable; licence-gated). */
134
+ lossy?: boolean;
135
+ }
136
+ /**
137
+ * The user-supplyable option keys per verb (excludes positional-owned keys).
138
+ * Exported for the wire-key conformance guard, which asserts each tuple ∪ its
139
+ * positional-owned keys equals the contract `operationOptionKeys(metadata)`.
140
+ */
141
+ export declare const VERB_OPTION_KEYS: {
142
+ readonly convert: readonly ["quality", "background", "crf", "trim_start", "trim_end", "fps", "width", "max_colors", "loop", "dither", "bitrate", "pages", "dpi"];
143
+ readonly thumbnail: readonly ["width", "height", "fit", "format", "quality", "timestamp", "source", "page"];
144
+ readonly textWatermark: readonly ["font_size", "color", "font_family", "rotation", "watermark_mode", "tile_spacing", "anchor", "margin_x", "margin_y", "opacity"];
145
+ readonly watermark: readonly ["anchor", "margin_x", "margin_y", "opacity", "overlay_width"];
146
+ readonly output: readonly ["quality", "width", "height", "fit", "background", "progressive", "optimization_level", "avif_speed", "metadata", "lossless", "lossy"];
147
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Typed per-op option interfaces for the ergonomic verbs (card Dhje3Faq).
3
+ * These replace the untyped `Record<string, unknown>` bags so the IDE can offer
4
+ * key completion and `tsc` rejects typos. The KEY SET of each interface is pinned
5
+ * to the contract two ways: (1) the source-level `Equal<...>` assertions below tie
6
+ * each interface to its `*_OPTION_KEYS` tuple at `tsc` time; (2) the wire-key
7
+ * conformance guard ties each tuple (∪ positional-owned) to the generated
8
+ * `OperationMetadata` at test time. Value types are best-effort (per-value/enum
9
+ * sync is out of scope — keys are the contract anchor).
10
+ *
11
+ * Keys a verb owns via a positional argument are EXCLUDED from its interface
12
+ * (`output_format` on convert, `text` on textWatermark) — they are set by the
13
+ * first argument and rejected if supplied in the bag (see `option_validation.ts`).
14
+ *
15
+ * Mirrored by the PHP array-shape docblocks — keep in lockstep.
16
+ */
17
+ const CONVERT_OPTION_KEYS = [
18
+ 'quality', 'background', 'crf', 'trim_start', 'trim_end', 'fps', 'width',
19
+ 'max_colors', 'loop', 'dither', 'bitrate', 'pages', 'dpi',
20
+ ];
21
+ const THUMBNAIL_OPTION_KEYS = [
22
+ 'width', 'height', 'fit', 'format', 'quality', 'timestamp', 'source', 'page',
23
+ ];
24
+ const TEXT_WATERMARK_OPTION_KEYS = [
25
+ 'font_size', 'color', 'font_family', 'rotation', 'watermark_mode',
26
+ 'tile_spacing', 'anchor', 'margin_x', 'margin_y', 'opacity',
27
+ ];
28
+ const WATERMARK_OPTION_KEYS = [
29
+ 'anchor', 'margin_x', 'margin_y', 'opacity', 'overlay_width',
30
+ ];
31
+ const OUTPUT_OPTION_KEYS = [
32
+ 'quality', 'width', 'height', 'fit', 'background', 'progressive',
33
+ 'optimization_level', 'avif_speed', 'metadata', 'lossless', 'lossy',
34
+ ];
35
+ const _convertKeysMatch = true;
36
+ const _thumbnailKeysMatch = true;
37
+ const _textWatermarkKeysMatch = true;
38
+ const _watermarkKeysMatch = true;
39
+ const _outputKeysMatch = true;
40
+ // Reference the assertions so `noUnusedLocals` doesn't strip them.
41
+ void _convertKeysMatch;
42
+ void _thumbnailKeysMatch;
43
+ void _textWatermarkKeysMatch;
44
+ void _watermarkKeysMatch;
45
+ void _outputKeysMatch;
46
+ /**
47
+ * The user-supplyable option keys per verb (excludes positional-owned keys).
48
+ * Exported for the wire-key conformance guard, which asserts each tuple ∪ its
49
+ * positional-owned keys equals the contract `operationOptionKeys(metadata)`.
50
+ */
51
+ export const VERB_OPTION_KEYS = {
52
+ convert: CONVERT_OPTION_KEYS,
53
+ thumbnail: THUMBNAIL_OPTION_KEYS,
54
+ textWatermark: TEXT_WATERMARK_OPTION_KEYS,
55
+ watermark: WATERMARK_OPTION_KEYS,
56
+ output: OUTPUT_OPTION_KEYS,
57
+ };
@@ -0,0 +1,58 @@
1
+ import { type OperationMetadata } from '@giveitsmaller/contracts/operations';
2
+ /**
3
+ * Eager, synchronous, PRE-UPLOAD option-key validation for the ergonomic verbs
4
+ * (card Dhje3Faq). The file-first builders accept verb options as untyped bags;
5
+ * a user typo (`{ quaity: 80 }`) would otherwise flow to the server and 422.
6
+ * This module rejects unknown keys at the verb call — before any upload —
7
+ * mirroring the existing `compress()` optimize-check and the watermark eager
8
+ * gate. The allowed key set is read from the generated `OperationMetadata`
9
+ * sidecars (the same contract-anchored source the wire-key conformance guard
10
+ * uses), so it can never silently drift from the contract.
11
+ *
12
+ * SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` only. `compress`
13
+ * is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
14
+ * (`optimize`, `presetOverrides`) and camelCase resolver aliases (`targetSize`,
15
+ * `outputFormat`) that are not `operationOptionKeys(compressMetadata)`; it has its
16
+ * own `unknown_field` validation through the preset resolver.
17
+ *
18
+ * Mirrored by the PHP `OptionValidation` helper — keep the two in lockstep.
19
+ */
20
+ /**
21
+ * OPERATION-LEVEL contract option keys (the keys valid in `OperationDef.options`):
22
+ * the union of every mime group's `options` plus `direct_options` for
23
+ * media-agnostic ops. Deliberately EXCLUDES `per_input_options` (valid only on a
24
+ * merge input). Promoted from the wire-key conformance guard for runtime reuse.
25
+ */
26
+ export declare function operationOptionKeys(metadata: OperationMetadata): ReadonlySet<string>;
27
+ /** The ergonomic verbs whose option bags this module key-validates. */
28
+ export type ValidatedVerb = 'convert' | 'thumbnail' | 'textWatermark' | 'watermark' | 'output';
29
+ /** Accessor for the conformance guard (pins these sets to the contract metadata). */
30
+ export declare function allowedKeysFor(verb: ValidatedVerb): ReadonlySet<string>;
31
+ /**
32
+ * Validate a USER-supplied options bag for an ergonomic verb. Throws
33
+ * {@link GislConfigError} (reason `unknown_field`) synchronously, BEFORE any
34
+ * upload or wire-key injection. Call this at the TOP of every verb body, before
35
+ * the `format`-drop / `output_format` / `text` injection.
36
+ *
37
+ * @throws {GislConfigError} reason `unknown_field` when the bag carries a key the
38
+ * verb owns via a positional argument, or a key absent from the op's contract
39
+ * option set.
40
+ */
41
+ export declare function validateVerbOptions(verb: ValidatedVerb, options: object | null | undefined): void;
42
+ /**
43
+ * Assert thumbnail `width` AND `height` are both present and non-nullish (the
44
+ * contract marks both `required` for image/video/document). The typed signature
45
+ * already enforces this at compile time; this RUNTIME guard catches JS callers and
46
+ * an explicit `undefined`/`null` BEFORE upload. Rejecting `null` (not just
47
+ * `undefined`) keeps TS in lockstep with the PHP `assertThumbnailDimensions`, which
48
+ * must reject `null` because PHP drops null values pre-lower — so a `null` dimension
49
+ * is a pre-upload error in BOTH languages, never a wire `null` that 422s. Mirrored
50
+ * in PHP.
51
+ *
52
+ * @throws {GislConfigError} reason `missing_required_field` naming the absent
53
+ * dimension(s) in `conflictingFields`.
54
+ */
55
+ export declare function assertThumbnailDimensions(options: {
56
+ width?: unknown;
57
+ height?: unknown;
58
+ } | null | undefined): void;