@giveitsmaller/sdk 0.2.3 → 0.6.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/types.d.ts CHANGED
@@ -1,54 +1,144 @@
1
- import type { OperationType, CallbackEventType, SseEventType, SseOperationProgressData, SseOperationCompletedData, SseOperationFailedData, SseJobCompletedData, SseJobFailedData, SseWorkflowTerminalData } from '@giveitsmaller/contracts/openapi';
1
+ import type { OperationType, OperationsSchemaResponse, CallbackEventType, SseEventType, SseOperationProgressData, SseOperationCompletedData, SseOperationFailedData, SseJobCompletedData, SseJobFailedData, SseWorkflowTerminalData, MultipartInitiateRequestMetadataHint, UploadProbeResponse } from '@giveitsmaller/contracts/openapi';
2
+ import type { JobInputV2RoleEnum } from '@giveitsmaller/contracts/openapi';
2
3
  export interface GislClientConfig {
3
4
  baseUrl: string;
4
5
  apiKey?: string;
5
6
  headers?: Record<string, string>;
6
7
  timeout?: number;
8
+ /**
9
+ * Send credentials (cookies) on every fetch — required in browsers when
10
+ * authenticating via the session cookie issued by `POST /api/auth/login`
11
+ * (Symfony firewall). Default `false` — the SDK ships in API-key mode by
12
+ * default. Set to `true` for browser SPAs that drive the auth flow via
13
+ * `client.login()` / `client.logout()` so the session cookie persists
14
+ * across requests.
15
+ *
16
+ * Node session persistence (cookie-jar across processes) is out of scope —
17
+ * this flag only flips fetch's `credentials` option; cookie storage is the
18
+ * environment's responsibility.
19
+ */
20
+ useSessionCookie?: boolean;
7
21
  /** Threshold in bytes above which multipart upload is used (default: 10MB) */
8
22
  multipartThreshold?: number;
9
- /** Max concurrent chunk uploads for multipart (default: 4) */
23
+ /**
24
+ * Max concurrent chunk uploads for multipart (default: 4). Non-finite or
25
+ * fractional values are coerced via `Math.floor`; `NaN`/`Infinity` and
26
+ * non-positive values fall back to the default. Zero workers would produce
27
+ * a multipart-complete with an incomplete parts array (silent corruption),
28
+ * so the sanitiser snaps below-1 to default rather than to 1.
29
+ */
10
30
  multipartConcurrency?: number;
31
+ /**
32
+ * Max total attempts per multipart S3 PUT, including the first try
33
+ * (default: 3 — one initial + two retries). 0 or 1 disables retry.
34
+ * Non-finite or fractional values are coerced via `Math.floor` and
35
+ * floored at 1; `NaN`/`Infinity` fall back to the default.
36
+ * Retries fire on 5xx/429 responses and on network TypeError; 4xx (other
37
+ * than 429) and abort signals fail fast.
38
+ */
39
+ multipartMaxAttempts?: number;
40
+ /**
41
+ * Base milliseconds for full-jitter exponential backoff between multipart
42
+ * retry attempts (default: 500). Each retry's delay is `random(0, base * 2^n)`
43
+ * where n is the zero-indexed retry number. `0` opts out of backoff (retries
44
+ * fire immediately) — useful for tests; not recommended for production where
45
+ * jitter is the only defence against thundering-herd retry storms against
46
+ * shared-throttling sources like S3. `NaN`/`Infinity` fall back to the default.
47
+ */
48
+ multipartRetryBaseMs?: number;
11
49
  }
12
50
  export interface OperationDef {
13
51
  type: OperationType;
14
52
  options?: Record<string, unknown>;
15
53
  }
16
- /** Job sourced from an uploaded file */
17
- export interface FileJobPayload {
18
- ref: string;
54
+ export interface UploadSourcePayload {
55
+ type: 'upload';
19
56
  file_id: string;
20
- operations: OperationDef[];
21
57
  }
22
- /** Job sourced from a single upstream job's output */
23
- export interface SourceJobPayload {
24
- ref: string;
25
- source: {
26
- ref: string;
27
- operation?: string;
28
- };
29
- operations: OperationDef[];
58
+ export interface JobOutputSourcePayload {
59
+ type: 'job_output';
60
+ from: string;
61
+ operation?: string;
30
62
  }
31
- /** Job sourced from multiple upstream jobs (merge/archive) */
32
- export interface InputsJobPayload {
33
- ref: string;
34
- inputs: Array<{
35
- ref: string;
36
- operation?: string;
37
- per_input_options?: Record<string, unknown>;
38
- }>;
63
+ export interface ExternalImportSourcePayload {
64
+ type: 'external_import';
65
+ external_source_id: string;
66
+ }
67
+ export interface ConnectionSourcePayload {
68
+ type: 'connection';
69
+ connection_id: string;
70
+ path: string;
71
+ }
72
+ export type WorkflowSourcePayload = UploadSourcePayload | JobOutputSourcePayload | ExternalImportSourcePayload | ConnectionSourcePayload;
73
+ export declare function uploadSource(fileId: string): UploadSourcePayload;
74
+ export declare function jobOutputSource(from: string, operation?: string): JobOutputSourcePayload;
75
+ export declare function externalImportSource(externalSourceId: string): ExternalImportSourcePayload;
76
+ export declare function connectionSource(connectionId: string, path: string): ConnectionSourcePayload;
77
+ export interface JobInputV2Payload {
78
+ source: WorkflowSourcePayload;
79
+ role?: JobInputV2RoleEnum;
80
+ per_input_options?: Record<string, unknown>;
81
+ }
82
+ export interface JobDefinitionPayload {
83
+ /**
84
+ * Optional local identifier within the workflow. Server auto-generates
85
+ * `^job_\d+$` when omitted; the SDK MUST NOT auto-generate. Required
86
+ * when this job is referenced by another job's `JobOutputSource.from`,
87
+ * `workflow_edges`, or `delivery.selection.explicit.refs[]`.
88
+ */
89
+ id?: string;
90
+ /** Single-input source. Mutually exclusive with `inputs[]` (server enforces). */
91
+ source?: WorkflowSourcePayload;
92
+ /** Multi-input list for merge / archive / image_watermark / custom_luma / audio_overlay. */
93
+ inputs?: JobInputV2Payload[];
39
94
  operations: OperationDef[];
95
+ /** Per-job hide-intermediates promotion flag per ADR-0003. */
96
+ deliver?: boolean;
97
+ /**
98
+ * Per-job opt-out of the "compress required in every chain" gate.
99
+ * When `true`, the server accepts a chain that doesn't terminate in a
100
+ * `compress` operation — required for chains that observe multi-output
101
+ * fan-out (e.g. convert PDF -> N images per ADR-0009 §D2) without
102
+ * collapsing the N outputs through a trailing chained compress.
103
+ *
104
+ * Accepted by the API at `compression/src/Jobs/.../JobDefinition.php`
105
+ * (`skipCompression`) and validated against the chain-ordering rule at
106
+ * `Job::validateChainOrdering`. Currently undocumented in
107
+ * `contracts/openapi/api.yaml` JobDefinition schema — spec follow-up
108
+ * pending; the SDK exposes the field to unblock e2e A8-FLIP.
109
+ */
110
+ skip_compression?: boolean;
40
111
  }
41
- export type JobDefinitionPayload = FileJobPayload | SourceJobPayload | InputsJobPayload;
42
- export declare function fileJob(ref: string, fileId: string, operations: OperationDef[]): FileJobPayload;
43
- export declare function sourceJob(ref: string, source: {
44
- ref: string;
45
- operation?: string;
46
- }, operations: OperationDef[]): SourceJobPayload;
47
- export declare function inputsJob(ref: string, inputs: Array<{
112
+ export type ExternalDestinationPayload = {
113
+ type: 'connection';
114
+ connection_id: string;
115
+ path: string;
116
+ } | {
117
+ type: 'external_import';
118
+ external_source_id: string;
119
+ };
120
+ export type DeliveryModePayload = 'individual' | 'bundle' | 'both';
121
+ export type DeliveryBundleFormatPayload = 'zip' | 'tar_gz';
122
+ export type DeliverySelectionTypePayload = 'terminal' | 'all_outputs' | 'explicit';
123
+ export interface DeliveryOutputRefPayload {
48
124
  ref: string;
49
125
  operation?: string;
50
- per_input_options?: Record<string, unknown>;
51
- }>, operations: OperationDef[]): InputsJobPayload;
126
+ }
127
+ export interface DeliverySelectionPayload {
128
+ type: DeliverySelectionTypePayload;
129
+ refs?: DeliveryOutputRefPayload[];
130
+ }
131
+ export interface DeliveryPayload {
132
+ mode?: DeliveryModePayload;
133
+ bundle_format?: DeliveryBundleFormatPayload;
134
+ bundle_filename?: string;
135
+ include_metadata?: boolean;
136
+ selection?: DeliverySelectionPayload;
137
+ }
138
+ export type ProcessingClassHintPayload = 'auto' | 'short_form_only' | 'long_form_allowed' | 'long_form_preferred';
139
+ export interface WorkflowProcessingPayload {
140
+ class_hint?: ProcessingClassHintPayload;
141
+ }
52
142
  export interface WorkflowCreatePayload {
53
143
  jobs: JobDefinitionPayload[];
54
144
  workflow_edges?: Array<{
@@ -57,12 +147,87 @@ export interface WorkflowCreatePayload {
57
147
  }>;
58
148
  callback_url?: string;
59
149
  callback_events?: CallbackEventType[];
60
- export?: {
61
- service: 's3';
62
- bucket: string;
63
- key_prefix?: string;
64
- role_arn: string;
65
- };
150
+ export?: ExternalDestinationPayload;
151
+ delivery?: DeliveryPayload;
152
+ processing?: WorkflowProcessingPayload;
153
+ }
154
+ /**
155
+ * Single source of truth for WorkflowCreatePayload's top-level wire keys.
156
+ * Read by `contract-drift-fields.test.ts` to cross-check against the spec at
157
+ * POST /api/workflows. Not re-exported from `index.ts`; this is reachable
158
+ * only via deep imports and should not be treated as public API.
159
+ * @internal
160
+ */
161
+ export declare const WORKFLOW_CREATE_PAYLOAD_KEYS: readonly ["jobs", "workflow_edges", "callback_url", "callback_events", "export", "delivery", "processing"];
162
+ export interface GetSchemaOptions {
163
+ /** Filter the schema to operations that accept this MIME type (e.g. `image/jpeg`). */
164
+ mimeType?: string;
165
+ /** Filter the schema to a single operation type. */
166
+ operation?: OperationType;
167
+ /**
168
+ * Conditional revalidation: send the previously-received `ETag` value to
169
+ * receive a 304-not-modified sentinel when the cached response is still
170
+ * fresh. Strong-ETag comparison.
171
+ */
172
+ ifNoneMatch?: string;
173
+ /**
174
+ * Conditional revalidation: send the previously-received `Last-Modified`
175
+ * value (HTTP-date) to receive a 304-not-modified sentinel when the
176
+ * cached response is still fresh.
177
+ */
178
+ ifModifiedSince?: string;
179
+ /** Cancel an in-flight schema fetch. Surfaces as `GislAbortError`. */
180
+ signal?: AbortSignal;
181
+ }
182
+ export type GetSchemaResult = {
183
+ notModified: false;
184
+ data: OperationsSchemaResponse;
185
+ etag?: string;
186
+ lastModified?: string;
187
+ } | {
188
+ notModified: true;
189
+ etag?: string;
190
+ lastModified?: string;
191
+ };
192
+ export interface CreditsUsageOptions {
193
+ /**
194
+ * Page size. Server defaults to 20 and rejects values outside `[1, 100]`
195
+ * with a 400 validation envelope.
196
+ */
197
+ limit?: number;
198
+ /** Page offset (zero-based). Server default is 0. */
199
+ offset?: number;
200
+ }
201
+ /**
202
+ * Aggregated result of a `preflightClips()` batch probe — N parallel calls
203
+ * to `POST /api/uploads/{id}/probe`, partitioned by outcome so the caller
204
+ * can drop bad clips before submitting a long-form merge workflow (per
205
+ * plan v5 round 10 / F11). Aggregation is structural — `ok` is everything
206
+ * the server marked workflow-ready, `rejected` is everything else with a
207
+ * typed probe response, and `errors` carries probe-call failures (e.g.
208
+ * the 422 `feature_not_available` envelope returned while the endpoint is
209
+ * still `availability: planned`).
210
+ */
211
+ export interface PreflightClipsResult {
212
+ /** Probes that returned `probe_status: 'ok'`. Safe to include in a workflow. */
213
+ ok: UploadProbeResponse[];
214
+ /**
215
+ * Probes that returned a non-`ok` `probe_status` (`corrupt`,
216
+ * `unsupported_codec`, `missing_metadata`). The caller should exclude
217
+ * these or convert them first.
218
+ */
219
+ rejected: UploadProbeResponse[];
220
+ /**
221
+ * Probe calls that themselves failed. Includes the
222
+ * `feature_not_available` (422) responses returned while the endpoint
223
+ * is `availability: planned` — narrow on `instanceof
224
+ * GislFeatureNotAvailableError` to detect that case.
225
+ */
226
+ errors: PreflightClipError[];
227
+ }
228
+ export interface PreflightClipError {
229
+ fileId: string;
230
+ error: unknown;
66
231
  }
67
232
  export interface WaitOptions {
68
233
  /** Poll interval in milliseconds (default: 2000) */
@@ -103,4 +268,152 @@ export type GislSseEvent = {
103
268
  export interface UploadOptions {
104
269
  /** Called with bytes uploaded so far (only for multipart) */
105
270
  onProgress?: (uploadedBytes: number, totalBytes: number) => void;
271
+ /**
272
+ * Cancel an in-flight upload. Aborting rejects the `uploadFile` promise with
273
+ * `GislAbortError`. Applies to the API requests (initiate, complete) and
274
+ * every S3 part PUT. Composes with the client-level per-request timeout —
275
+ * whichever callback fires first determines the error class: user abort
276
+ * first → `GislAbortError`; timer first → `GislTimeoutError`.
277
+ */
278
+ signal?: AbortSignal;
279
+ /**
280
+ * Optional metadata hint forwarded to multipart initiate so the server
281
+ * can size-check and preflight-route based on caller-asserted dimensions.
282
+ * Single-shot uploads ignore this field (the multipart initiate is the
283
+ * only endpoint that accepts it). Wire-encoded as a JSON-stringified
284
+ * single FormData field on the multipart/initiate request.
285
+ */
286
+ metadataHint?: MultipartInitiateRequestMetadataHint;
287
+ /**
288
+ * Resume an in-progress multipart upload (SDK-3 / Wb6ebOMM).
289
+ *
290
+ * When set, `uploadFile()` skips `/multipart/initiate` entirely. Instead it
291
+ * walks `GET /api/uploads/multipart/{uploadId}/status` (paginated via
292
+ * `next_part_number_marker` / `is_truncated`), computes which parts are
293
+ * still missing, re-presigns them in batches of <=100 via
294
+ * `POST /api/uploads/multipart/{uploadId}/presign`, PUTs only the missing
295
+ * parts, then `POST /api/uploads/multipart/complete`.
296
+ *
297
+ * The caller's `file` argument MUST be byte-identical to the file used in
298
+ * the original initiate call (same byte content at the same offsets).
299
+ * Mismatched bytes will produce S3 etags that don't match the server's
300
+ * recorded state, and `/multipart/complete` will reject.
301
+ *
302
+ * Anonymous-initiated sessions cannot be resumed by an authed caller
303
+ * (server returns 403 → `GislMultipartSessionAuthRequiredError`); a
304
+ * non-existent or expired session returns 404 →
305
+ * `GislMultipartSessionNotFoundError`.
306
+ */
307
+ resumeUploadId?: string;
308
+ /**
309
+ * Called after every successful part PUT during fresh-upload AND resume
310
+ * paths. Receives a JSON-serialisable snapshot of the upload state — round-
311
+ * trippable via `JSON.stringify` for persistence across process restarts,
312
+ * so a future `uploadFile({ resumeUploadId: state.uploadId, ... })` can
313
+ * pick up where the prior process stopped.
314
+ *
315
+ * The callback is invoked OUTSIDE the per-part retry-scoped path: a throw
316
+ * here will fail the upload but NEVER trigger a duplicate PUT (mirrors the
317
+ * `onProgress` discipline at `client.ts:1195-1199`).
318
+ */
319
+ onCheckpoint?: (state: MultipartCheckpointState) => void;
320
+ }
321
+ /**
322
+ * JSON-serialisable snapshot of an in-progress multipart upload, emitted
323
+ * after every successful part PUT via `UploadOptions.onCheckpoint`. Designed
324
+ * to round-trip through `JSON.stringify` / `JSON.parse` so consumers can
325
+ * persist it across process restarts and resume via
326
+ * `uploadFile({ resumeUploadId: state.uploadId, ... })`.
327
+ *
328
+ * All fields are primitive: no `Date` (use the ISO-8601 string on
329
+ * `manifestExpiresAt`), no `Buffer`, no functions.
330
+ */
331
+ export interface MultipartCheckpointState {
332
+ /** The server-assigned `upload_id` (UUID) used as `resumeUploadId` later. */
333
+ readonly uploadId: string;
334
+ /** Total parts the server computed for this upload. <=10 000. */
335
+ readonly totalParts: number;
336
+ /**
337
+ * Part numbers (1-indexed) that have been successfully PUT to S3 so far.
338
+ * Includes part 1 (the initiate first chunk) once a part >= 2 lands. Sorted
339
+ * ascending. Excluded numbers in `[1, totalParts]` are the still-missing
340
+ * set a resume must re-PUT.
341
+ */
342
+ readonly uploadedPartNumbers: readonly number[];
343
+ /**
344
+ * ISO-8601 wall-clock instant at which the server's durable manifest for
345
+ * this `uploadId` expires (currently a 48h TTL per the API-2 contract). On
346
+ * resume, clients SHOULD call `keepaliveUpload(uploadId)` every 12-24h to
347
+ * extend this, leaving >=24h of slack against the 48h ceiling even with
348
+ * worst-case clock skew between client and server.
349
+ */
350
+ readonly manifestExpiresAt: string;
351
+ }
352
+ /**
353
+ * One entry in `_Sdk3HandCodedMultipartStatusResult.uploadedParts`. Aggregated
354
+ * across all pages of the underlying `GET /status` endpoint by the SDK's
355
+ * walk-pagination loop.
356
+ *
357
+ * TODO(HxUmVr3Y): replace hand-coded shape on regen.
358
+ */
359
+ export interface _Sdk3HandCodedUploadedPart {
360
+ readonly partNumber: number;
361
+ readonly etag: string;
362
+ readonly sizeBytes: number;
363
+ /** ISO-8601 last-modified instant of the S3 part. */
364
+ readonly lastModified: string;
365
+ }
366
+ /**
367
+ * Aggregated result of `getUploadStatus()` after the SDK has walked every
368
+ * page of the underlying `GET /api/uploads/multipart/{uploadId}/status`
369
+ * endpoint (paginated via `next_part_number_marker` / `is_truncated`).
370
+ * `uploadedParts` is the merged-and-sorted list of every part the server
371
+ * has recorded across all pages — callers see the complete state without
372
+ * needing to drive the pagination cursor themselves.
373
+ *
374
+ * TODO(HxUmVr3Y): replace hand-coded shape on regen.
375
+ */
376
+ export interface _Sdk3HandCodedMultipartStatusResult {
377
+ readonly uploadId: string;
378
+ readonly multipartUploadId: string;
379
+ readonly cloudKey: string;
380
+ readonly totalParts: number;
381
+ /** Sorted ascending by `partNumber`; complete across all server-side pages. */
382
+ readonly uploadedParts: readonly _Sdk3HandCodedUploadedPart[];
383
+ /** ISO-8601 wall-clock when the durable session manifest expires. */
384
+ readonly manifestExpiresAt: string;
385
+ /** Server-recommended chunk size in bytes (matches the initiate envelope). */
386
+ readonly recommendedChunkSize: number;
387
+ }
388
+ /**
389
+ * One entry in `_Sdk3HandCodedPresignPartsResult.presignedUrls`. Shape mirrors
390
+ * the contract-pinned `PresignedUrlPart` from the initiate envelope; kept
391
+ * hand-coded here so the resume path does not depend on the generator's name
392
+ * for that shape (decoupling for the HxUmVr3Y regen window).
393
+ *
394
+ * TODO(HxUmVr3Y): replace hand-coded shape on regen.
395
+ */
396
+ export interface _Sdk3HandCodedPresignedPart {
397
+ readonly partNumber: number;
398
+ readonly url: string;
399
+ readonly expiresAt: string;
400
+ }
401
+ /**
402
+ * Result of `presignParts()` — the server-issued presigned PUT URLs for the
403
+ * requested part numbers.
404
+ *
405
+ * TODO(HxUmVr3Y): replace hand-coded shape on regen.
406
+ */
407
+ export interface _Sdk3HandCodedPresignPartsResult {
408
+ readonly uploadId: string;
409
+ readonly presignedUrls: readonly _Sdk3HandCodedPresignedPart[];
410
+ }
411
+ /**
412
+ * Result of `keepaliveUpload()` — the refreshed manifest TTL expiry instant.
413
+ *
414
+ * TODO(HxUmVr3Y): replace hand-coded shape on regen.
415
+ */
416
+ export interface _Sdk3HandCodedKeepaliveResult {
417
+ readonly uploadId: string;
418
+ readonly manifestExpiresAt: string;
106
419
  }
package/dist/types.js CHANGED
@@ -1,12 +1,33 @@
1
1
  // ---------------------------------------------------------------------------
2
- // Job factory functions
2
+ // Source factories — return wire-format objects with the `type` discriminator
3
3
  // ---------------------------------------------------------------------------
4
- export function fileJob(ref, fileId, operations) {
5
- return { ref, file_id: fileId, operations };
4
+ export function uploadSource(fileId) {
5
+ return { type: 'upload', file_id: fileId };
6
6
  }
7
- export function sourceJob(ref, source, operations) {
8
- return { ref, source, operations };
7
+ export function jobOutputSource(from, operation) {
8
+ return operation === undefined
9
+ ? { type: 'job_output', from }
10
+ : { type: 'job_output', from, operation };
9
11
  }
10
- export function inputsJob(ref, inputs, operations) {
11
- return { ref, inputs, operations };
12
+ export function externalImportSource(externalSourceId) {
13
+ return { type: 'external_import', external_source_id: externalSourceId };
12
14
  }
15
+ export function connectionSource(connectionId, path) {
16
+ return { type: 'connection', connection_id: connectionId, path };
17
+ }
18
+ /**
19
+ * Single source of truth for WorkflowCreatePayload's top-level wire keys.
20
+ * Read by `contract-drift-fields.test.ts` to cross-check against the spec at
21
+ * POST /api/workflows. Not re-exported from `index.ts`; this is reachable
22
+ * only via deep imports and should not be treated as public API.
23
+ * @internal
24
+ */
25
+ export const WORKFLOW_CREATE_PAYLOAD_KEYS = Object.freeze([
26
+ 'jobs',
27
+ 'workflow_edges',
28
+ 'callback_url',
29
+ 'callback_events',
30
+ 'export',
31
+ 'delivery',
32
+ 'processing',
33
+ ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giveitsmaller/sdk",
3
- "version": "0.2.3",
3
+ "version": "0.6.0",
4
4
  "description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "node": ">=18"
20
20
  },
21
21
  "dependencies": {
22
- "@giveitsmaller/contracts": "^0.2.3"
22
+ "@giveitsmaller/contracts": "^0.4.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^22",
@@ -31,6 +31,8 @@
31
31
  "build": "tsc",
32
32
  "prepack": "npm run build",
33
33
  "check": "tsc --noEmit",
34
- "test": "vitest run"
34
+ "test": "vitest run",
35
+ "test:parity": "vitest run tests/parity",
36
+ "parity:update": "UPDATE_PARITY_FIXTURES=1 vitest run tests/parity"
35
37
  }
36
38
  }