@giveitsmaller/sdk 0.9.0 → 0.10.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.
Files changed (42) hide show
  1. package/README.md +8 -0
  2. package/dist/_audit.js +0 -1
  3. package/dist/builder.js +65 -13
  4. package/dist/client.d.ts +38 -2
  5. package/dist/client.js +131 -7
  6. package/dist/credentials.js +4 -2
  7. package/dist/ergonomic/preset_resolver.js +4 -5
  8. package/dist/ergonomic/presets/image_compress.d.ts +1 -9
  9. package/dist/ergonomic/presets/image_compress.js +6 -25
  10. package/dist/ergonomic/presets/index.d.ts +1 -1
  11. package/dist/ergonomic/presets/index.js +1 -1
  12. package/dist/errors.d.ts +59 -1
  13. package/dist/errors.js +51 -0
  14. package/dist/file-first.d.ts +233 -0
  15. package/dist/file-first.js +585 -21
  16. package/dist/generated/sdk_spec/enums.d.ts +0 -11
  17. package/dist/generated/sdk_spec/enums.js +0 -7
  18. package/dist/generated/sdk_spec/errors.d.ts +1 -1
  19. package/dist/generated/sdk_spec/errors.js +26 -0
  20. package/dist/generated/sdk_spec/presets.js +0 -3
  21. package/dist/generated/sdk_spec/version.d.ts +2 -2
  22. package/dist/generated/sdk_spec/version.js +2 -2
  23. package/dist/handle.js +34 -14
  24. package/dist/index.browser.d.ts +1 -0
  25. package/dist/index.browser.js +14 -0
  26. package/dist/index.core.d.ts +35 -0
  27. package/dist/index.core.js +102 -0
  28. package/dist/index.d.ts +1 -32
  29. package/dist/index.js +9 -89
  30. package/dist/lazy-downloader.d.ts +19 -0
  31. package/dist/lazy-downloader.js +19 -0
  32. package/dist/merge.d.ts +11 -0
  33. package/dist/merge.js +144 -45
  34. package/dist/node-fs.browser.d.ts +17 -0
  35. package/dist/node-fs.browser.js +7 -0
  36. package/dist/node-fs.d.ts +14 -0
  37. package/dist/node-fs.js +14 -0
  38. package/dist/sha256.d.ts +20 -0
  39. package/dist/sha256.js +108 -0
  40. package/dist/types.d.ts +54 -2
  41. package/dist/types.js +2 -0
  42. package/package.json +15 -2
package/dist/merge.js CHANGED
@@ -26,7 +26,7 @@
26
26
  * assets both fail fast so the caller saves bandwidth on typo'd composes.
27
27
  */
28
28
  import { uploadSource, jobOutputSource } from './types.js';
29
- import { GislConfigError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, } from './errors.js';
29
+ import { GislConfigError, GislNetworkError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, SseEndedWithoutTerminal, } from './errors.js';
30
30
  import { _checkAborted, _consumeSseToTerminal, _parseMaxWait, _pollToTerminal, _projectResult, } from './builder.js';
31
31
  import { Handle } from './handle.js';
32
32
  /**
@@ -118,6 +118,12 @@ export class MergeBuilder {
118
118
  throw new GislTimeoutError(`Merge workflow ${created.workflowId} reached terminal status but maxWait elapsed before downloads could be fetched`);
119
119
  }
120
120
  const downloads = await this.client.getWorkflowDownloads(created.workflowId);
121
+ // TDqmkWpX: the maxWait deadline also covers the downloads fetch itself —
122
+ // re-check AFTER the call so a slow getWorkflowDownloads cannot return a
123
+ // success past the advertised whole-run deadline.
124
+ if (Date.now() >= deadline) {
125
+ throw new GislTimeoutError(`Merge workflow ${created.workflowId} downloads fetch completed after maxWait elapsed`);
126
+ }
121
127
  // p0SuJEeK — project ONLY the merge job's output. getWorkflowDownloads
122
128
  // returns a download group per terminal job, which now INCLUDES the
123
129
  // `passthrough` source jobs (their output is the unchanged upload). Those
@@ -125,7 +131,7 @@ export class MergeBuilder {
125
131
  // would pollute the Result with the raw inputs. The merge job's ref is
126
132
  // 'merge' (see buildPayload); the source jobs are 'src_N'.
127
133
  const mergeDownloads = downloads.downloads.filter((d) => d.ref === 'merge');
128
- return _projectResult(finalStatus, mergeDownloads, this.opOptionsForResolved());
134
+ return _projectResult(finalStatus, mergeDownloads, this.opOptionsForResolved(plan.mediaKind));
129
135
  }
130
136
  async submit(options) {
131
137
  const plan = this.planSequence();
@@ -202,6 +208,32 @@ export class MergeBuilder {
202
208
  if (positions.length > 10) {
203
209
  throw new GislConfigError(`merge accepts at most 10 inputs (got ${positions.length}). Reduce the sequence or split the merge.`);
204
210
  }
211
+ // Validate merge-level options BEFORE upload (parity with PHP MergeBuilder).
212
+ // A `targetSize: 'garbage'` typo must fail locally rather than burning N
213
+ // uploads before parseSizeString fires from wireMergeOptions().
214
+ //
215
+ // Gated to video (codex #176 r3 DCJUvvfA) — `target_size_bytes` only
216
+ // crosses the wire for video merges (see wireMergeOptions); for image/audio
217
+ // the field is silently dropped, so validating its string form would reject
218
+ // a merge over a value that never leaves the SDK.
219
+ if (mediaKind === 'video' && typeof this.opOptions.targetSize === 'string') {
220
+ try {
221
+ parseSizeString(this.opOptions.targetSize);
222
+ }
223
+ catch {
224
+ throw new GislConfigError(`Invalid targetSize string '${this.opOptions.targetSize}' — expected '<num>[B|KB|MB|GB]'.`);
225
+ }
226
+ }
227
+ // Image merges require an `output_type` (the generated merge schema marks
228
+ // `output_type` required for image kind). Without this check the SDK would
229
+ // upload all assets then receive a server-side validation failure instead
230
+ // of a free local one. Parity with PHP MergeBuilder.
231
+ if (mediaKind === 'image' &&
232
+ this.opOptions.output == null &&
233
+ this.opOptions.outputType == null) {
234
+ throw new GislConfigError('image merges require an explicit output_type — set MergeOptions(output: "video"|"gif") or ' +
235
+ 'MergeOptions(outputType: ...). The server rejects image merge requests with no output_type.');
236
+ }
205
237
  return { mediaKind, positions, uniqueAssets: uploadSet };
206
238
  }
207
239
  inferMediaKind() {
@@ -308,12 +340,55 @@ export class MergeBuilder {
308
340
  };
309
341
  return { jobs: [...sourceJobs, mergeJob] };
310
342
  }
311
- opOptionsForResolved() {
312
- // Strip the SDK-only fields before exposing on resolvedOptions.applied.
313
- const { mediaKind: _m, allowUnusedAssets: _a, ...rest } = this.opOptions;
314
- void _m;
315
- void _a;
316
- return { ...rest };
343
+ opOptionsForResolved(mediaKind) {
344
+ // Mirror wireMergeOptions's per-media allowlist (and PHP
345
+ // opOptionsForResolved) so resolvedOptions.applied reports ONLY the
346
+ // options that actually crossed the wire for this media kind — not the
347
+ // raw option bag (which would falsely claim dropped fields were applied).
348
+ const o = this.opOptions;
349
+ const out = {};
350
+ // All media kinds.
351
+ if (o.output !== undefined)
352
+ out.output = o.output;
353
+ if (o.outputType !== undefined)
354
+ out.outputType = o.outputType;
355
+ if (o.transition !== undefined)
356
+ out.transition = o.transition;
357
+ // Video + audio.
358
+ if (mediaKind === 'video' || mediaKind === 'audio') {
359
+ if (o.crossfadeDuration !== undefined)
360
+ out.crossfadeDuration = o.crossfadeDuration;
361
+ if (o.normalizeAudio !== undefined)
362
+ out.normalizeAudio = o.normalizeAudio;
363
+ }
364
+ // Audio only.
365
+ if (mediaKind === 'audio' && o.gapDuration !== undefined)
366
+ out.gapDuration = o.gapDuration;
367
+ // Video only.
368
+ if (mediaKind === 'video') {
369
+ if (o.codec !== undefined)
370
+ out.codec = o.codec;
371
+ if (o.crf !== undefined)
372
+ out.crf = o.crf;
373
+ if (o.preset !== undefined)
374
+ out.preset = o.preset;
375
+ if (o.targetSize !== undefined)
376
+ out.targetSize = o.targetSize;
377
+ }
378
+ // Image only.
379
+ if (mediaKind === 'image') {
380
+ if (o.transitionDuration !== undefined)
381
+ out.transitionDuration = o.transitionDuration;
382
+ if (o.fps !== undefined)
383
+ out.fps = o.fps;
384
+ if (o.durationPerImage !== undefined)
385
+ out.durationPerImage = o.durationPerImage;
386
+ if (o.loopCount !== undefined)
387
+ out.loopCount = o.loopCount;
388
+ if (o.videoFormat !== undefined)
389
+ out.videoFormat = o.videoFormat;
390
+ }
391
+ return out;
317
392
  }
318
393
  async awaitTerminal(args) {
319
394
  if (args.useSSE) {
@@ -321,10 +396,12 @@ export class MergeBuilder {
321
396
  return await _consumeSseToTerminal(this.client, args);
322
397
  }
323
398
  catch (err) {
324
- if (err instanceof GislTimeoutError)
325
- throw err;
326
- if (err instanceof DOMException && err.name === 'AbortError')
399
+ // TDqmkWpX: poll-fallback ONLY on a clean SSE stream-end or a typed
400
+ // transport error; rethrow everything else (timeout, abort, API, an
401
+ // onProgress callback throw, anything unexpected) so it isn't masked.
402
+ if (!(err instanceof SseEndedWithoutTerminal || err instanceof GislNetworkError)) {
327
403
  throw err;
404
+ }
328
405
  }
329
406
  }
330
407
  return await _pollToTerminal(this.client, args);
@@ -367,45 +444,67 @@ function assetIdentity(a) {
367
444
  // anything else is a separate upload" — predictable.
368
445
  return `path:${a.path}`;
369
446
  }
370
- function wireMergeOptions(opts, mediaKind) {
447
+ /**
448
+ * Project the merge-level {@link MergeOptions} into the per-media wire
449
+ * allowlist. Exported so the file-first `MergedRecipe`
450
+ * (`files([...]).merge(...)`) lowers identically to this operation-first
451
+ * `client.merge(...)` builder — one allowlist, no drift. Mirrors the PHP
452
+ * `MergeBuilder::wireMergeOptions()` public-static seam.
453
+ *
454
+ * @internal Not re-exported from `index.ts`; shared between the two merge
455
+ * surfaces only.
456
+ */
457
+ export function wireMergeOptions(opts, mediaKind) {
371
458
  const out = {};
372
- if (opts.transition !== undefined)
373
- out.transition = opts.transition;
374
- if (opts.crossfadeDuration !== undefined)
375
- out.crossfade_duration = opts.crossfadeDuration;
376
- // Codex r2 medium ab2422e56ea0 — merge-level `gap_duration` is on
377
- // MergeAudioOptions only (not MergeVideoOptions or MergeImageOptions).
378
- // Drop it for non-audio merges instead of shipping an invalid payload.
379
- if (opts.gapDuration !== undefined && mediaKind === 'audio')
380
- out.gap_duration = opts.gapDuration;
381
- if (opts.normalizeAudio !== undefined)
382
- out.normalize_audio = opts.normalizeAudio;
383
- if (opts.codec !== undefined)
384
- out.codec = opts.codec;
385
- if (opts.crf !== undefined)
386
- out.crf = opts.crf;
387
- if (opts.preset !== undefined)
388
- out.preset = opts.preset;
389
- if (opts.targetSize !== undefined) {
390
- out.target_size_bytes = typeof opts.targetSize === 'number'
391
- ? opts.targetSize
392
- : parseSizeString(opts.targetSize);
393
- out.encoding_mode = 'target_size';
394
- }
395
- if (opts.transitionDuration !== undefined)
396
- out.transition_duration = opts.transitionDuration;
397
- if (opts.fps !== undefined)
398
- out.fps = opts.fps;
399
- if (opts.durationPerImage !== undefined)
400
- out.duration_per_image = opts.durationPerImage;
401
- if (opts.loopCount !== undefined)
402
- out.loop_count = opts.loopCount;
459
+ // Per-media wire allowlist — mirror of PHP MergeBuilder::wireMergeOptions
460
+ // (codex 30d…/parity): a field set on the wrong media kind is DROPPED
461
+ // locally rather than shipped as an invalid payload the server 422s.
462
+ // All media kinds.
403
463
  if (opts.output !== undefined)
404
464
  out.output_type = opts.output;
405
465
  if (opts.outputType !== undefined)
406
466
  out.output_type = opts.outputType;
407
- if (opts.videoFormat !== undefined)
408
- out.video_format = opts.videoFormat;
467
+ if (opts.transition !== undefined)
468
+ out.transition = opts.transition;
469
+ // Video + audio.
470
+ if (mediaKind === 'video' || mediaKind === 'audio') {
471
+ if (opts.crossfadeDuration !== undefined)
472
+ out.crossfade_duration = opts.crossfadeDuration;
473
+ if (opts.normalizeAudio !== undefined)
474
+ out.normalize_audio = opts.normalizeAudio;
475
+ }
476
+ // Audio only — merge-level `gap_duration` is on MergeAudioOptions only
477
+ // (codex r2 medium ab2422e56ea0).
478
+ if (mediaKind === 'audio' && opts.gapDuration !== undefined)
479
+ out.gap_duration = opts.gapDuration;
480
+ // Video only.
481
+ if (mediaKind === 'video') {
482
+ if (opts.codec !== undefined)
483
+ out.codec = opts.codec;
484
+ if (opts.crf !== undefined)
485
+ out.crf = opts.crf;
486
+ if (opts.preset !== undefined)
487
+ out.preset = opts.preset;
488
+ if (opts.targetSize !== undefined) {
489
+ out.target_size_bytes = typeof opts.targetSize === 'number'
490
+ ? opts.targetSize
491
+ : parseSizeString(opts.targetSize);
492
+ out.encoding_mode = 'target_size';
493
+ }
494
+ }
495
+ // Image only.
496
+ if (mediaKind === 'image') {
497
+ if (opts.transitionDuration !== undefined)
498
+ out.transition_duration = opts.transitionDuration;
499
+ if (opts.fps !== undefined)
500
+ out.fps = opts.fps;
501
+ if (opts.durationPerImage !== undefined)
502
+ out.duration_per_image = opts.durationPerImage;
503
+ if (opts.loopCount !== undefined)
504
+ out.loop_count = opts.loopCount;
505
+ if (opts.videoFormat !== undefined)
506
+ out.video_format = opts.videoFormat;
507
+ }
409
508
  return out;
410
509
  }
411
510
  /**
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Browser stub for `node-fs.ts` — selected via the package.json `browser` field
3
+ * file-remap so browser bundlers (vite/webpack/esbuild) never pull `node:fs` /
4
+ * `node:path`. These are only reached by path-string uploads, which are
5
+ * Node-only; a browser caller passes a `Blob`/`File` (handled by
6
+ * `blobByteSource`, no filesystem) and never invokes these. If somehow called
7
+ * in a browser, they throw a clear error rather than a missing-module crash.
8
+ *
9
+ * The signatures intentionally mirror the `node-fs.ts` re-exports
10
+ * (`node:fs/promises` `open`/`stat`, `node:path` `basename`) so `client.ts`
11
+ * type-checks identically against either module.
12
+ */
13
+ import type { open as NodeOpen, stat as NodeStat } from 'node:fs/promises';
14
+ import type { basename as NodeBasename } from 'node:path';
15
+ export declare const open: typeof NodeOpen;
16
+ export declare const stat: typeof NodeStat;
17
+ export declare const basename: typeof NodeBasename;
@@ -0,0 +1,7 @@
1
+ const pathUploadsAreNodeOnly = () => {
2
+ throw new Error('Path-string file uploads require Node.js (node:fs). In a browser, pass a ' +
3
+ 'Blob/File to uploadFile()/file() instead.');
4
+ };
5
+ export const open = pathUploadsAreNodeOnly;
6
+ export const stat = pathUploadsAreNodeOnly;
7
+ export const basename = pathUploadsAreNodeOnly;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Node-only filesystem/path access used by path-string uploads in `client.ts`.
3
+ *
4
+ * @internal — isolated in its own module so the browser build can swap it for
5
+ * `node-fs.browser.ts` via the package.json `browser` field, keeping `node:fs`
6
+ * out of browser bundles. `client.ts` imports these STATICALLY (not via dynamic
7
+ * `import()`), which is deliberate: a static re-export chain stays interceptable
8
+ * by `vi.mock('node:fs/promises')` in the test suite, whereas a dynamic import
9
+ * of a `node:` builtin is not mocked by vitest. Browser callers upload a
10
+ * `Blob`/`File` (which never reaches this module); a browser bundle resolves the
11
+ * `.browser` stub instead, so these symbols are never loaded there.
12
+ */
13
+ export { open, stat } from 'node:fs/promises';
14
+ export { basename } from 'node:path';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Node-only filesystem/path access used by path-string uploads in `client.ts`.
3
+ *
4
+ * @internal — isolated in its own module so the browser build can swap it for
5
+ * `node-fs.browser.ts` via the package.json `browser` field, keeping `node:fs`
6
+ * out of browser bundles. `client.ts` imports these STATICALLY (not via dynamic
7
+ * `import()`), which is deliberate: a static re-export chain stays interceptable
8
+ * by `vi.mock('node:fs/promises')` in the test suite, whereas a dynamic import
9
+ * of a `node:` builtin is not mocked by vitest. Browser callers upload a
10
+ * `Blob`/`File` (which never reaches this module); a browser bundle resolves the
11
+ * `.browser` stub instead, so these symbols are never loaded there.
12
+ */
13
+ export { open, stat } from 'node:fs/promises';
14
+ export { basename } from 'node:path';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Dependency-free, **synchronous** SHA-256 — browser- and Node-safe.
3
+ *
4
+ * @internal — NOT re-exported from `index.ts`. Exists so the ergonomic
5
+ * preset-resolver fingerprint (`preset_resolver.ts`) can hash without importing
6
+ * `node:crypto`, which would drag a Node-only built-in into the browser entry
7
+ * graph. Output is byte-identical to Node's
8
+ * `createHash('sha256').update(input, 'utf8').digest('hex')` and to PHP's
9
+ * `hash('sha256', $input)` — the resolver fingerprint is parity-pinned
10
+ * (`tests/unit/preset-resolver.test.ts` asserts exact digests; "do NOT relax to
11
+ * a regex"). Web Crypto's `subtle.digest` is async and would force the resolver
12
+ * sync→async, so a small sync implementation is used instead.
13
+ *
14
+ * Standard FIPS 180-4 SHA-256. Input is hashed as UTF-8 bytes.
15
+ */
16
+ /**
17
+ * Compute the SHA-256 of `input` (hashed as its UTF-8 byte encoding) and return
18
+ * the lowercase 64-character hex digest.
19
+ */
20
+ export declare function sha256Hex(input: string): string;
package/dist/sha256.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Dependency-free, **synchronous** SHA-256 — browser- and Node-safe.
3
+ *
4
+ * @internal — NOT re-exported from `index.ts`. Exists so the ergonomic
5
+ * preset-resolver fingerprint (`preset_resolver.ts`) can hash without importing
6
+ * `node:crypto`, which would drag a Node-only built-in into the browser entry
7
+ * graph. Output is byte-identical to Node's
8
+ * `createHash('sha256').update(input, 'utf8').digest('hex')` and to PHP's
9
+ * `hash('sha256', $input)` — the resolver fingerprint is parity-pinned
10
+ * (`tests/unit/preset-resolver.test.ts` asserts exact digests; "do NOT relax to
11
+ * a regex"). Web Crypto's `subtle.digest` is async and would force the resolver
12
+ * sync→async, so a small sync implementation is used instead.
13
+ *
14
+ * Standard FIPS 180-4 SHA-256. Input is hashed as UTF-8 bytes.
15
+ */
16
+ // First 32 bits of the fractional parts of the cube roots of the first 64 primes.
17
+ const K = new Uint32Array([
18
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
19
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
20
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
21
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
22
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
23
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
24
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
25
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
26
+ ]);
27
+ function rotr(value, bits) {
28
+ return (value >>> bits) | (value << (32 - bits));
29
+ }
30
+ /**
31
+ * Compute the SHA-256 of `input` (hashed as its UTF-8 byte encoding) and return
32
+ * the lowercase 64-character hex digest.
33
+ */
34
+ export function sha256Hex(input) {
35
+ const bytes = new TextEncoder().encode(input); // UTF-8, matching Node `.update(str)`
36
+ // --- Padding (FIPS 180-4 §5.1.1) ---
37
+ const bitLength = bytes.length * 8;
38
+ // message + 0x80 + zero-pad to 56 mod 64 + 8-byte big-endian length
39
+ const paddedLength = ((bytes.length + 8) >> 6 << 6) + 64;
40
+ const buffer = new Uint8Array(paddedLength);
41
+ buffer.set(bytes);
42
+ buffer[bytes.length] = 0x80;
43
+ // 64-bit big-endian bit length in the final 8 bytes. Both words are written:
44
+ // the high word via Math.floor (exact for the Number range, well beyond any
45
+ // realistic input) and the low word via `>>> 0`. Fingerprint inputs are tiny,
46
+ // so the high word is effectively always zero, but it is set for correctness.
47
+ const view = new DataView(buffer.buffer);
48
+ view.setUint32(paddedLength - 4, bitLength >>> 0, false);
49
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
50
+ // --- Initial hash values: fractional parts of the square roots of the first 8 primes. ---
51
+ let h0 = 0x6a09e667;
52
+ let h1 = 0xbb67ae85;
53
+ let h2 = 0x3c6ef372;
54
+ let h3 = 0xa54ff53a;
55
+ let h4 = 0x510e527f;
56
+ let h5 = 0x9b05688c;
57
+ let h6 = 0x1f83d9ab;
58
+ let h7 = 0x5be0cd19;
59
+ const w = new Uint32Array(64);
60
+ for (let offset = 0; offset < paddedLength; offset += 64) {
61
+ for (let i = 0; i < 16; i++) {
62
+ w[i] = view.getUint32(offset + i * 4, false);
63
+ }
64
+ for (let i = 16; i < 64; i++) {
65
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3);
66
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10);
67
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
68
+ }
69
+ let a = h0;
70
+ let b = h1;
71
+ let c = h2;
72
+ let d = h3;
73
+ let e = h4;
74
+ let f = h5;
75
+ let g = h6;
76
+ let h = h7;
77
+ for (let i = 0; i < 64; i++) {
78
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
79
+ const ch = (e & f) ^ (~e & g);
80
+ const temp1 = (h + S1 + ch + K[i] + w[i]) | 0;
81
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
82
+ const maj = (a & b) ^ (a & c) ^ (b & c);
83
+ const temp2 = (S0 + maj) | 0;
84
+ h = g;
85
+ g = f;
86
+ f = e;
87
+ e = (d + temp1) | 0;
88
+ d = c;
89
+ c = b;
90
+ b = a;
91
+ a = (temp1 + temp2) | 0;
92
+ }
93
+ h0 = (h0 + a) | 0;
94
+ h1 = (h1 + b) | 0;
95
+ h2 = (h2 + c) | 0;
96
+ h3 = (h3 + d) | 0;
97
+ h4 = (h4 + e) | 0;
98
+ h5 = (h5 + f) | 0;
99
+ h6 = (h6 + g) | 0;
100
+ h7 = (h7 + h) | 0;
101
+ }
102
+ const out = [h0, h1, h2, h3, h4, h5, h6, h7];
103
+ let hex = '';
104
+ for (const value of out) {
105
+ hex += (value >>> 0).toString(16).padStart(8, '0');
106
+ }
107
+ return hex;
108
+ }
package/dist/types.d.ts CHANGED
@@ -18,6 +18,20 @@ export interface GislClientConfig {
18
18
  * environment's responsibility.
19
19
  */
20
20
  useSessionCookie?: boolean;
21
+ /**
22
+ * Preferred response language. When set, the SDK sends
23
+ * `Accept-Language: <locale>` on every GISL-API request (e.g. `'fr-FR'`,
24
+ * `'de'`). The server echoes the language it actually resolved via the
25
+ * `Content-Language` response header, surfaced on
26
+ * `GislApiError.contentLanguage`. When no supported language matches, the
27
+ * server falls back to its default (typically `en-GB`).
28
+ *
29
+ * A dedicated `locale` wins over any `Accept-Language` passed through
30
+ * `headers` (mirrors how `apiKey` wins over a caller-supplied
31
+ * `Authorization` header); the conflicting `headers` entry is dropped
32
+ * case-insensitively so the request never carries two variants.
33
+ */
34
+ locale?: string;
21
35
  /** Threshold in bytes above which multipart upload is used (default: 10MB) */
22
36
  multipartThreshold?: number;
23
37
  /**
@@ -70,12 +84,27 @@ export interface ConnectionSourcePayload {
70
84
  path: string;
71
85
  }
72
86
  export type WorkflowSourcePayload = UploadSourcePayload | JobOutputSourcePayload | ExternalImportSourcePayload | ConnectionSourcePayload;
87
+ /**
88
+ * The source leaves a multi-input `JobInputV2Payload.source` may take —
89
+ * `WorkflowSourcePayload` minus the `upload` leaf. Per the contract
90
+ * (`MultiInputSource` in `openapi/api.yaml`), multi-input operations
91
+ * (merge / archive / image_watermark / custom_luma / audio_overlay) do NOT
92
+ * accept upload-direct: an uploaded file enters a multi-input job via a
93
+ * `passthrough` source job referenced downstream by `{ type: 'job_output' }`.
94
+ * Single-input `JobDefinitionPayload.source` keeps the wider
95
+ * `WorkflowSourcePayload` (upload-direct is valid there).
96
+ *
97
+ * Named with the SDK's `*Payload` suffix to match every sibling source type
98
+ * and to avoid colliding with the generated `MultiInputSource` model (a
99
+ * different, deep-import-only type family).
100
+ */
101
+ export type MultiInputSourcePayload = JobOutputSourcePayload | ExternalImportSourcePayload | ConnectionSourcePayload;
73
102
  export declare function uploadSource(fileId: string): UploadSourcePayload;
74
103
  export declare function jobOutputSource(from: string, operation?: string): JobOutputSourcePayload;
75
104
  export declare function externalImportSource(externalSourceId: string): ExternalImportSourcePayload;
76
105
  export declare function connectionSource(connectionId: string, path: string): ConnectionSourcePayload;
77
106
  export interface JobInputV2Payload {
78
- source: WorkflowSourcePayload;
107
+ source: MultiInputSourcePayload;
79
108
  role?: JobInputV2RoleEnum;
80
109
  per_input_options?: Record<string, unknown>;
81
110
  }
@@ -139,6 +168,17 @@ export interface WorkflowProcessingPayload {
139
168
  }
140
169
  export interface WorkflowCreatePayload {
141
170
  jobs: JobDefinitionPayload[];
171
+ /**
172
+ * Flat single-job form (with `operations`): top-level input source, exactly
173
+ * equivalent to `jobs: [{ source, operations }]` (contracts D0Gsri8V, v2.64.0).
174
+ * The spec's `oneOf` makes `jobs` and `source`+`operations` mutually exclusive;
175
+ * the SDK builders always emit the explicit `jobs[]` form, so these are typed
176
+ * optional for spec-completeness (a consumer hand-building the flat form omits
177
+ * `jobs`). Builder adoption of the flat form is a follow-up.
178
+ */
179
+ source?: WorkflowSourcePayload;
180
+ /** Flat-form operation set (with `source`); equivalent to one job's `operations`. */
181
+ operations?: OperationDef[];
142
182
  workflow_edges?: Array<{
143
183
  from: string;
144
184
  to: string;
@@ -156,7 +196,7 @@ export interface WorkflowCreatePayload {
156
196
  * only via deep imports and should not be treated as public API.
157
197
  * @internal
158
198
  */
159
- export declare const WORKFLOW_CREATE_PAYLOAD_KEYS: readonly ["jobs", "workflow_edges", "callback_url", "callback_events", "export", "delivery", "processing"];
199
+ export declare const WORKFLOW_CREATE_PAYLOAD_KEYS: readonly ["jobs", "source", "operations", "workflow_edges", "callback_url", "callback_events", "export", "delivery", "processing"];
160
200
  export interface GetSchemaOptions {
161
201
  /** Filter the schema to operations that accept this MIME type (e.g. `image/jpeg`). */
162
202
  mimeType?: string;
@@ -196,6 +236,18 @@ export interface CreditsUsageOptions {
196
236
  /** Page offset (zero-based). Server default is 0. */
197
237
  offset?: number;
198
238
  }
239
+ export interface ListWorkflowsOptions {
240
+ /**
241
+ * Opaque pagination cursor from a previous page's `nextCursor`. Omit for
242
+ * the first page; treat the value as opaque (do not parse).
243
+ */
244
+ cursor?: string;
245
+ /**
246
+ * Rows per page. Server defaults to 20 and rejects values outside `[1, 100]`
247
+ * with a 400 validation envelope.
248
+ */
249
+ limit?: number;
250
+ }
199
251
  /**
200
252
  * Aggregated result of a `preflightClips()` batch probe — N parallel calls
201
253
  * to `POST /api/uploads/{id}/probe`, partitioned by outcome so the caller
package/dist/types.js CHANGED
@@ -42,6 +42,8 @@ export const JOB_DEFINITION_PAYLOAD_KEYS = Object.freeze([
42
42
  */
43
43
  export const WORKFLOW_CREATE_PAYLOAD_KEYS = Object.freeze([
44
44
  'jobs',
45
+ 'source',
46
+ 'operations',
45
47
  'workflow_edges',
46
48
  'callback_url',
47
49
  'callback_events',
package/package.json CHANGED
@@ -1,16 +1,28 @@
1
1
  {
2
2
  "name": "@giveitsmaller/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
7
7
  "main": "./dist/index.js",
8
8
  "exports": {
9
9
  ".": {
10
+ "browser": {
11
+ "types": "./dist/index.browser.d.ts",
12
+ "default": "./dist/index.browser.js"
13
+ },
10
14
  "types": "./dist/index.d.ts",
11
15
  "default": "./dist/index.js"
16
+ },
17
+ "./browser": {
18
+ "types": "./dist/index.browser.d.ts",
19
+ "default": "./dist/index.browser.js"
12
20
  }
13
21
  },
22
+ "browser": {
23
+ "./dist/index.js": "./dist/index.browser.js",
24
+ "./dist/node-fs.js": "./dist/node-fs.browser.js"
25
+ },
14
26
  "types": "./dist/index.d.ts",
15
27
  "files": [
16
28
  "dist/"
@@ -19,10 +31,11 @@
19
31
  "node": ">=18"
20
32
  },
21
33
  "dependencies": {
22
- "@giveitsmaller/contracts": "^0.9.0"
34
+ "@giveitsmaller/contracts": "^0.16.0"
23
35
  },
24
36
  "devDependencies": {
25
37
  "@types/node": "^22",
38
+ "esbuild": "^0.27",
26
39
  "typescript": "^5.7",
27
40
  "vitest": "^3.1 <3.2.5",
28
41
  "yaml": "^2.6"