@giveitsmaller/sdk 0.15.0 → 0.17.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();
@@ -130,6 +139,10 @@ export function _runAudit() {
130
139
  accept();
131
140
  // FF3a / u0hBt6fl — homogeneous fan-out builder surface.
132
141
  accept();
142
+ // FF4a / Z7zTr789 — multi-input watermark recipe surface.
143
+ accept();
144
+ accept();
145
+ accept();
133
146
  accept();
134
147
  accept();
135
148
  accept();
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
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';
2
- import type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, GislClientConfig, GislSseEvent, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, UploadOptions, WaitOptions, WorkflowCreatePayload, _Sdk3HandCodedKeepaliveResult, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignPartsResult } from './types.js';
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
+ 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;
5
5
  export interface ValidationDetail {
@@ -133,8 +133,13 @@ export declare class GislClient {
133
133
  createWorkflow(payload: WorkflowCreatePayload): Promise<WorkflowCreateResponse>;
134
134
  /**
135
135
  * Get current workflow status.
136
+ *
137
+ * For an anonymous (null-owner) workflow, pass `opts.capability` (the `cap`
138
+ * from the anonymous workflow-create response) so a session-less caller can
139
+ * read it — the SDK sends it as the `X-Workflow-Capability` header. Omit for
140
+ * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
136
141
  */
137
- getWorkflowStatus(workflowId: string): Promise<WorkflowStatusResponse>;
142
+ getWorkflowStatus(workflowId: string, opts?: ReadCapabilityOptions): Promise<WorkflowStatusResponse>;
138
143
  /**
139
144
  * Poll until the workflow reaches a terminal status.
140
145
  */
@@ -176,13 +181,24 @@ export declare class GislClient {
176
181
  resumeWorkflow(workflowId: string): Promise<WorkflowResumeResponse>;
177
182
  /**
178
183
  * Get download URLs for a completed workflow.
184
+ *
185
+ * For an anonymous (null-owner) workflow, pass `opts.capability` (the `cap`
186
+ * from the anonymous workflow-create response) so a session-less caller can
187
+ * read it — the SDK sends it as the `X-Workflow-Capability` header. Omit for
188
+ * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
179
189
  */
180
- getWorkflowDownloads(workflowId: string): Promise<WorkflowDownloadResponse>;
190
+ getWorkflowDownloads(workflowId: string, opts?: ReadCapabilityOptions): Promise<WorkflowDownloadResponse>;
181
191
  /**
182
192
  * Stream SSE events for a workflow. Returns an async iterable.
193
+ *
194
+ * For an anonymous (null-owner) workflow, pass `opts.capability` (the `cap`
195
+ * from the anonymous workflow-create response) so a session-less caller can
196
+ * read it — the SDK sends it as the `X-Workflow-Capability` header. Omit for
197
+ * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
183
198
  */
184
199
  streamEvents(workflowId: string, opts?: {
185
200
  signal?: AbortSignal;
201
+ capability?: string;
186
202
  }): Promise<AsyncGenerator<GislSseEvent>>;
187
203
  /**
188
204
  * Get metadata for an uploaded file.
@@ -229,6 +245,13 @@ export declare class GislClient {
229
245
  * off the error envelope.
230
246
  */
231
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>;
232
255
  /**
233
256
  * Authenticate with email/password. On success the server issues a
234
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;
@@ -56,6 +56,17 @@ const S3_MAX_MULTIPART_PARTS = 10_000;
56
56
  const RECOMMENDED_CHUNK_SIZE_MAX_BYTES = 104_857_600; // 100 MiB
57
57
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
58
58
  const DEFAULT_POLL_TIMEOUT_MS = 300_000; // 5 min
59
+ // Anonymous-read capability header. An anonymous (null-owner) workflow create
60
+ // returns a one-time `cap` token (WorkflowCreateResponse.cap); the session-less
61
+ // caller passes it back on status/downloads/events reads via this header so the
62
+ // server can authorize the read. A wrong/missing cap on a null-owner workflow
63
+ // returns 404 (no existence oracle), per contracts ticket YQt88cq2.
64
+ const WORKFLOW_CAPABILITY_HEADER = 'X-Workflow-Capability';
65
+ // Build the capability header set for a workflow read. Empty when no token is
66
+ // supplied (authenticated reads — the session authorizes those).
67
+ function workflowCapabilityHeaders(capability) {
68
+ return capability ? { [WORKFLOW_CAPABILITY_HEADER]: capability } : {};
69
+ }
59
70
  // Statuses that waitForWorkflow() returns immediately on. Per ticket I24,
60
71
  // `cancelled` and `expired` are terminal (a workflow cannot leave either
61
72
  // state). `paused_insufficient_credits` is a soft-pause: not terminal, but
@@ -1599,10 +1610,16 @@ export class GislClient {
1599
1610
  }
1600
1611
  /**
1601
1612
  * Get current workflow status.
1613
+ *
1614
+ * For an anonymous (null-owner) workflow, pass `opts.capability` (the `cap`
1615
+ * from the anonymous workflow-create response) so a session-less caller can
1616
+ * read it — the SDK sends it as the `X-Workflow-Capability` header. Omit for
1617
+ * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
1602
1618
  */
1603
- async getWorkflowStatus(workflowId) {
1619
+ async getWorkflowStatus(workflowId, opts = {}) {
1604
1620
  return this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/status`, {
1605
1621
  deserialize: WorkflowStatusResponseFromJSON,
1622
+ headers: workflowCapabilityHeaders(opts.capability),
1606
1623
  });
1607
1624
  }
1608
1625
  /**
@@ -1613,7 +1630,9 @@ export class GislClient {
1613
1630
  const timeoutMs = options?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
1614
1631
  const deadline = Date.now() + timeoutMs;
1615
1632
  while (true) {
1616
- const status = await this.getWorkflowStatus(workflowId);
1633
+ const status = await this.getWorkflowStatus(workflowId, {
1634
+ capability: options?.capability,
1635
+ });
1617
1636
  options?.onPoll?.(status.status);
1618
1637
  if (TERMINAL_STATUSES.has(status.status)) {
1619
1638
  return status;
@@ -1669,14 +1688,25 @@ export class GislClient {
1669
1688
  }
1670
1689
  /**
1671
1690
  * Get download URLs for a completed workflow.
1691
+ *
1692
+ * For an anonymous (null-owner) workflow, pass `opts.capability` (the `cap`
1693
+ * from the anonymous workflow-create response) so a session-less caller can
1694
+ * read it — the SDK sends it as the `X-Workflow-Capability` header. Omit for
1695
+ * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
1672
1696
  */
1673
- async getWorkflowDownloads(workflowId) {
1697
+ async getWorkflowDownloads(workflowId, opts = {}) {
1674
1698
  return this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/downloads`, {
1675
1699
  deserialize: WorkflowDownloadResponseFromJSON,
1700
+ headers: workflowCapabilityHeaders(opts.capability),
1676
1701
  });
1677
1702
  }
1678
1703
  /**
1679
1704
  * Stream SSE events for a workflow. Returns an async iterable.
1705
+ *
1706
+ * For an anonymous (null-owner) workflow, pass `opts.capability` (the `cap`
1707
+ * from the anonymous workflow-create response) so a session-less caller can
1708
+ * read it — the SDK sends it as the `X-Workflow-Capability` header. Omit for
1709
+ * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
1680
1710
  */
1681
1711
  async streamEvents(workflowId, opts = {}) {
1682
1712
  const eventsPath = `/api/workflows/${encodeURIComponent(workflowId)}/events`;
@@ -1702,6 +1732,7 @@ export class GislClient {
1702
1732
  response = await this.request('GET', eventsPath, {
1703
1733
  rawResponse: true,
1704
1734
  signal: controller.signal,
1735
+ headers: workflowCapabilityHeaders(opts.capability),
1705
1736
  });
1706
1737
  }
1707
1738
  catch (err) {
@@ -1903,6 +1934,17 @@ export class GislClient {
1903
1934
  deserialize: CreditsBalanceResponseFromJSON,
1904
1935
  });
1905
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
+ }
1906
1948
  // -----------------------------------------------------------------------
1907
1949
  // Auth
1908
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', 'metadata', 'output_format', 'progressive', 'quality', 'width'], planned: ['lossless'] },
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', 'metadata', 'output_format', 'quality', 'width'], planned: ['lossless'] },
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, v2.97.0). `keep` is `availability:planned`. */
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. (`keep` value is planned.) */
130
+ metadata?: OutputMetadata;
131
+ /** JPEG/WebP lossless. PLANNED (gated unavailable). */
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
+ };