@giveitsmaller/sdk 0.8.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 (46) hide show
  1. package/README.md +8 -0
  2. package/dist/_audit.js +5 -1
  3. package/dist/builder.d.ts +1 -8
  4. package/dist/builder.js +72 -18
  5. package/dist/client.d.ts +38 -2
  6. package/dist/client.js +131 -7
  7. package/dist/credentials.js +4 -2
  8. package/dist/ergonomic/preset_resolver.js +4 -5
  9. package/dist/ergonomic/presets/image_compress.d.ts +1 -9
  10. package/dist/ergonomic/presets/image_compress.js +6 -25
  11. package/dist/ergonomic/presets/index.d.ts +1 -1
  12. package/dist/ergonomic/presets/index.js +1 -1
  13. package/dist/errors.d.ts +75 -1
  14. package/dist/errors.js +73 -0
  15. package/dist/file-first.d.ts +456 -4
  16. package/dist/file-first.js +1042 -83
  17. package/dist/generated/sdk_spec/enums.d.ts +0 -11
  18. package/dist/generated/sdk_spec/enums.js +0 -7
  19. package/dist/generated/sdk_spec/errors.d.ts +1 -1
  20. package/dist/generated/sdk_spec/errors.js +26 -0
  21. package/dist/generated/sdk_spec/presets.js +0 -3
  22. package/dist/generated/sdk_spec/version.d.ts +2 -2
  23. package/dist/generated/sdk_spec/version.js +2 -2
  24. package/dist/gisl.d.ts +22 -1
  25. package/dist/gisl.js +31 -1
  26. package/dist/handle.d.ts +153 -0
  27. package/dist/handle.js +273 -0
  28. package/dist/index.browser.d.ts +1 -0
  29. package/dist/index.browser.js +14 -0
  30. package/dist/index.core.d.ts +35 -0
  31. package/dist/index.core.js +102 -0
  32. package/dist/index.d.ts +1 -30
  33. package/dist/index.js +9 -73
  34. package/dist/lazy-downloader.d.ts +19 -0
  35. package/dist/lazy-downloader.js +19 -0
  36. package/dist/merge.d.ts +13 -1
  37. package/dist/merge.js +186 -55
  38. package/dist/node-fs.browser.d.ts +17 -0
  39. package/dist/node-fs.browser.js +7 -0
  40. package/dist/node-fs.d.ts +14 -0
  41. package/dist/node-fs.js +14 -0
  42. package/dist/sha256.d.ts +20 -0
  43. package/dist/sha256.js +108 -0
  44. package/dist/types.d.ts +54 -2
  45. package/dist/types.js +2 -0
  46. package/package.json +15 -2
package/dist/merge.js CHANGED
@@ -25,9 +25,10 @@
25
25
  * Local validation runs BEFORE any upload — undeclared refs and unused
26
26
  * assets both fail fast so the caller saves bandwidth on typo'd composes.
27
27
  */
28
- import { uploadSource } from './types.js';
29
- import { GislConfigError, GislPerInputOptionsNotSupportedError, GislTimeoutError, GislUndeclaredAssetError, GislUnusedAssetError, } from './errors.js';
28
+ import { uploadSource, jobOutputSource } from './types.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
+ import { Handle } from './handle.js';
31
32
  /**
32
33
  * Construct a path-asset. Bare-string arguments to `merge(...)` are
33
34
  * implicitly wrapped via this helper.
@@ -117,7 +118,20 @@ export class MergeBuilder {
117
118
  throw new GislTimeoutError(`Merge workflow ${created.workflowId} reached terminal status but maxWait elapsed before downloads could be fetched`);
118
119
  }
119
120
  const downloads = await this.client.getWorkflowDownloads(created.workflowId);
120
- return _projectResult(finalStatus, downloads.downloads, this.opOptionsForResolved());
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
+ }
127
+ // p0SuJEeK — project ONLY the merge job's output. getWorkflowDownloads
128
+ // returns a download group per terminal job, which now INCLUDES the
129
+ // `passthrough` source jobs (their output is the unchanged upload). Those
130
+ // are plumbing, not the merge deliverable — surfacing them as artifacts
131
+ // would pollute the Result with the raw inputs. The merge job's ref is
132
+ // 'merge' (see buildPayload); the source jobs are 'src_N'.
133
+ const mergeDownloads = downloads.downloads.filter((d) => d.ref === 'merge');
134
+ return _projectResult(finalStatus, mergeDownloads, this.opOptionsForResolved(plan.mediaKind));
121
135
  }
122
136
  async submit(options) {
123
137
  const plan = this.planSequence();
@@ -125,11 +139,9 @@ export class MergeBuilder {
125
139
  const payload = this.buildPayload(plan, uploadedByAssetId);
126
140
  payload.callback_url = options.webhook;
127
141
  const created = await this.client.createWorkflow(payload);
128
- const handle = {
129
- workflowId: created.workflowId,
130
- ...(created.webhookSecret != null ? { webhookSecret: created.webhookSecret } : {}),
131
- };
132
- return handle;
142
+ // No client passed → the returned Handle's status()/wait()/result()
143
+ // throw `no_client`; the merge submit reconciles via webhook.
144
+ return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined);
133
145
  }
134
146
  // ---------------------------------------------------------------------------
135
147
  /**
@@ -196,6 +208,32 @@ export class MergeBuilder {
196
208
  if (positions.length > 10) {
197
209
  throw new GislConfigError(`merge accepts at most 10 inputs (got ${positions.length}). Reduce the sequence or split the merge.`);
198
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
+ }
199
237
  return { mediaKind, positions, uniqueAssets: uploadSet };
200
238
  }
201
239
  inferMediaKind() {
@@ -247,12 +285,38 @@ export class MergeBuilder {
247
285
  return uploaded;
248
286
  }
249
287
  buildPayload(plan, uploadedByAssetId) {
250
- const inputs = plan.positions.map((pos) => {
288
+ // p0SuJEeK the API rejects upload-direct multi-input
289
+ // (`MultiInputSource` excludes the `upload` leaf: "use type=job_output").
290
+ // So each uploaded asset is wrapped in its OWN single-input `passthrough`
291
+ // source job, and the merge job references those via `job_output` — the
292
+ // shape the v2.35.0 `v2_merge_two_uploads` example prescribes. One source
293
+ // job per UNIQUE asset (in first-seen position order); a repeated asset
294
+ // re-uses its src job. `passthrough` is a lossless inert op (it does NOT
295
+ // get the implicit compress an empty `operations: []` job would).
296
+ const srcIdByAsset = new Map();
297
+ const sourceJobs = [];
298
+ for (const pos of plan.positions) {
299
+ if (srcIdByAsset.has(pos.assetId))
300
+ continue;
251
301
  const fileId = uploadedByAssetId.get(pos.assetId);
252
302
  if (fileId === undefined) {
253
303
  // Defensive — planSequence should have rejected this.
254
304
  throw new Error(`Asset '${pos.assetId}' was never uploaded — internal builder bug`);
255
305
  }
306
+ const srcId = `src_${sourceJobs.length}`;
307
+ srcIdByAsset.set(pos.assetId, srcId);
308
+ sourceJobs.push({
309
+ id: srcId,
310
+ source: uploadSource(fileId),
311
+ operations: [{ type: 'passthrough' }],
312
+ });
313
+ }
314
+ const inputs = plan.positions.map((pos) => {
315
+ // Defensive — srcIdByAsset was populated for every position's asset above.
316
+ const srcId = srcIdByAsset.get(pos.assetId);
317
+ if (srcId === undefined) {
318
+ throw new Error(`Asset '${pos.assetId}' has no source job — internal builder bug`);
319
+ }
256
320
  // Codex r1 HIGH 502c6bf232c2 — per_input_options goes on EACH
257
321
  // JobInputV2Payload (per-input entry), NOT on operations[0].options.
258
322
  // Skip emission for image merges (planSequence already rejects opts
@@ -261,7 +325,7 @@ export class MergeBuilder {
261
325
  const wireOpts = plan.mediaKind === 'image'
262
326
  ? {}
263
327
  : wirePerInputOptions(pos.options, plan.mediaKind);
264
- const input = { source: uploadSource(fileId) };
328
+ const input = { source: jobOutputSource(srcId) };
265
329
  if (Object.keys(wireOpts).length > 0) {
266
330
  input.per_input_options = wireOpts;
267
331
  }
@@ -269,19 +333,62 @@ export class MergeBuilder {
269
333
  });
270
334
  // Merge-level options (excluding the SDK-side mediaKind/allowUnusedAssets).
271
335
  const mergeOpts = wireMergeOptions(this.opOptions, plan.mediaKind);
272
- const job = {
336
+ const mergeJob = {
273
337
  id: 'merge',
274
338
  inputs,
275
339
  operations: [{ type: 'merge', options: mergeOpts }],
276
340
  };
277
- return { jobs: [job] };
341
+ return { jobs: [...sourceJobs, mergeJob] };
278
342
  }
279
- opOptionsForResolved() {
280
- // Strip the SDK-only fields before exposing on resolvedOptions.applied.
281
- const { mediaKind: _m, allowUnusedAssets: _a, ...rest } = this.opOptions;
282
- void _m;
283
- void _a;
284
- 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;
285
392
  }
286
393
  async awaitTerminal(args) {
287
394
  if (args.useSSE) {
@@ -289,10 +396,12 @@ export class MergeBuilder {
289
396
  return await _consumeSseToTerminal(this.client, args);
290
397
  }
291
398
  catch (err) {
292
- if (err instanceof GislTimeoutError)
293
- throw err;
294
- 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)) {
295
403
  throw err;
404
+ }
296
405
  }
297
406
  }
298
407
  return await _pollToTerminal(this.client, args);
@@ -335,45 +444,67 @@ function assetIdentity(a) {
335
444
  // anything else is a separate upload" — predictable.
336
445
  return `path:${a.path}`;
337
446
  }
338
- 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) {
339
458
  const out = {};
340
- if (opts.transition !== undefined)
341
- out.transition = opts.transition;
342
- if (opts.crossfadeDuration !== undefined)
343
- out.crossfade_duration = opts.crossfadeDuration;
344
- // Codex r2 medium ab2422e56ea0 — merge-level `gap_duration` is on
345
- // MergeAudioOptions only (not MergeVideoOptions or MergeImageOptions).
346
- // Drop it for non-audio merges instead of shipping an invalid payload.
347
- if (opts.gapDuration !== undefined && mediaKind === 'audio')
348
- out.gap_duration = opts.gapDuration;
349
- if (opts.normalizeAudio !== undefined)
350
- out.normalize_audio = opts.normalizeAudio;
351
- if (opts.codec !== undefined)
352
- out.codec = opts.codec;
353
- if (opts.crf !== undefined)
354
- out.crf = opts.crf;
355
- if (opts.preset !== undefined)
356
- out.preset = opts.preset;
357
- if (opts.targetSize !== undefined) {
358
- out.target_size_bytes = typeof opts.targetSize === 'number'
359
- ? opts.targetSize
360
- : parseSizeString(opts.targetSize);
361
- out.encoding_mode = 'target_size';
362
- }
363
- if (opts.transitionDuration !== undefined)
364
- out.transition_duration = opts.transitionDuration;
365
- if (opts.fps !== undefined)
366
- out.fps = opts.fps;
367
- if (opts.durationPerImage !== undefined)
368
- out.duration_per_image = opts.durationPerImage;
369
- if (opts.loopCount !== undefined)
370
- 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.
371
463
  if (opts.output !== undefined)
372
464
  out.output_type = opts.output;
373
465
  if (opts.outputType !== undefined)
374
466
  out.output_type = opts.outputType;
375
- if (opts.videoFormat !== undefined)
376
- 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
+ }
377
508
  return out;
378
509
  }
379
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.8.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"