@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.
- package/README.md +8 -0
- package/dist/_audit.js +5 -1
- package/dist/builder.d.ts +1 -8
- package/dist/builder.js +72 -18
- package/dist/client.d.ts +38 -2
- package/dist/client.js +131 -7
- package/dist/credentials.js +4 -2
- package/dist/ergonomic/preset_resolver.js +4 -5
- package/dist/ergonomic/presets/image_compress.d.ts +1 -9
- package/dist/ergonomic/presets/image_compress.js +6 -25
- package/dist/ergonomic/presets/index.d.ts +1 -1
- package/dist/ergonomic/presets/index.js +1 -1
- package/dist/errors.d.ts +75 -1
- package/dist/errors.js +73 -0
- package/dist/file-first.d.ts +456 -4
- package/dist/file-first.js +1042 -83
- package/dist/generated/sdk_spec/enums.d.ts +0 -11
- package/dist/generated/sdk_spec/enums.js +0 -7
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +26 -0
- package/dist/generated/sdk_spec/presets.js +0 -3
- package/dist/generated/sdk_spec/version.d.ts +2 -2
- package/dist/generated/sdk_spec/version.js +2 -2
- package/dist/gisl.d.ts +22 -1
- package/dist/gisl.js +31 -1
- package/dist/handle.d.ts +153 -0
- package/dist/handle.js +273 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +14 -0
- package/dist/index.core.d.ts +35 -0
- package/dist/index.core.js +102 -0
- package/dist/index.d.ts +1 -30
- package/dist/index.js +9 -73
- package/dist/lazy-downloader.d.ts +19 -0
- package/dist/lazy-downloader.js +19 -0
- package/dist/merge.d.ts +13 -1
- package/dist/merge.js +186 -55
- package/dist/node-fs.browser.d.ts +17 -0
- package/dist/node-fs.browser.js +7 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +14 -0
- package/dist/sha256.d.ts +20 -0
- package/dist/sha256.js +108 -0
- package/dist/types.d.ts +54 -2
- package/dist/types.js +2 -0
- 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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
336
|
+
const mergeJob = {
|
|
273
337
|
id: 'merge',
|
|
274
338
|
inputs,
|
|
275
339
|
operations: [{ type: 'merge', options: mergeOpts }],
|
|
276
340
|
};
|
|
277
|
-
return { jobs: [
|
|
341
|
+
return { jobs: [...sourceJobs, mergeJob] };
|
|
278
342
|
}
|
|
279
|
-
opOptionsForResolved() {
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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.
|
|
376
|
-
out.
|
|
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';
|
package/dist/node-fs.js
ADDED
|
@@ -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';
|
package/dist/sha256.d.ts
ADDED
|
@@ -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:
|
|
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
package/package.json
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giveitsmaller/sdk",
|
|
3
|
-
"version": "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.
|
|
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"
|