@liqhtworks/sophon-sdk 0.1.4 → 0.1.5

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.
@@ -0,0 +1,6 @@
1
+ # This repository is generated from Liqhtworks/sophon-api at every
2
+ # release. Source-side changes (helpers, OpenAPI spec, generator
3
+ # config) belong in that repo, not here. Filing issues / PRs against
4
+ # this repo is fine — Liqhtworks engineers triage them and route the
5
+ # fix upstream as needed.
6
+ * @Liqhtworks/engineering
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something the SDK does that contradicts the docs or its types.
4
+ title: "[bug] "
5
+ labels: bug
6
+ ---
7
+
8
+ ## What happened
9
+
10
+ <!-- What you tried to do, what the SDK did instead. -->
11
+
12
+ ## Reproducer
13
+
14
+ ```ts
15
+ // Minimum code that reproduces. Strip secrets.
16
+ import { Configuration, JobsApi } from "@liqhtworks/sophon-sdk";
17
+ // …
18
+ ```
19
+
20
+ ## Environment
21
+
22
+ - `@liqhtworks/sophon-sdk` version: `0.1.x`
23
+ - Node / Bun / Deno version: `…`
24
+ - OS: `…`
25
+ - Running in: server-side / browser / edge runtime
26
+
27
+ ## Expected vs. actual
28
+
29
+ - Expected: `…`
30
+ - Actual: `…` (paste error message + stack trace inside a fenced block)
31
+
32
+ ## Anything else
33
+
34
+ <!-- Logs, X-Request-Id headers from the response, network captures, etc. -->
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: SOPHON API documentation
4
+ url: https://registry.scalar.com/@liqhtworks/apis/sophon-encoding-api/latest
5
+ about: Reference for every endpoint the SDK wraps.
6
+ - name: API + spec issues (sophon-api)
7
+ url: https://github.com/Liqhtworks/sophon-api/issues
8
+ about: For server-side bugs or OpenAPI questions, file there. SDK bugs stay here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: A capability you want from the SDK that isn't shipped.
4
+ title: "[feature] "
5
+ labels: enhancement
6
+ ---
7
+
8
+ ## What would you like the SDK to do
9
+
10
+ <!-- Describe the customer-facing behavior, not the implementation. -->
11
+
12
+ ## What you tried instead
13
+
14
+ <!-- Working around it today, or which competitor / hand-rolled tool you'd
15
+ otherwise reach for. -->
16
+
17
+ ## Is this an SDK-level fit, or upstream?
18
+
19
+ - [ ] SDK-level (helper, type, ergonomic)
20
+ - [ ] Spec / API-level (would also need a sophon-api change)
package/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@liqhtworks/sophon-sdk` are recorded here. The
4
+ package follows [SemVer](https://semver.org/) — see `README.md` for the
5
+ versioning policy applied during the v0.x pre-1.0 phase.
6
+
7
+ ## [0.1.4] — 2026-05-08
8
+
9
+ - `JobSource.upload(uploadId)` constructor — typed alternative to the
10
+ fragile `{ type: "upload", upload_id: "..." }` literal.
11
+ - Generated exports tightened so all helpers and discriminated-union
12
+ constructors are reachable at the top level.
13
+ - Build-test coverage extended over the new surface.
14
+
15
+ ## [0.1.3] — 2026-05-08
16
+
17
+ - `UploadsApiLike` helper interface narrowed to only the fields the
18
+ helpers actually read. The previous declaration typed `expires_at`
19
+ as `string`, but the generated `UploadsApi` returns `Date`, so the
20
+ helper compiled in isolation but not against the real client. Fixed.
21
+
22
+ ## [0.1.2] — 2026-04-23
23
+
24
+ - Per-route idempotency keys in `uploadFile`. Earlier releases reused
25
+ one key for both `createUpload` and `completeUpload`; SOPHON scopes
26
+ idempotency keys per route and rejected the second call with HTTP 409.
27
+ Now derives `${idem}/create` and `${idem}/complete` from the caller's
28
+ seed so retries still reach the server's idempotent path.
29
+ - Build-test fixtures regenerate as real ffprobe-able media via ffmpeg.
30
+
31
+ ## [0.1.0] — 2026-04-23
32
+
33
+ Initial public release.
34
+
35
+ - Generated transport (`Configuration`, `JobsApi`, `UploadsApi`,
36
+ `WebhooksApi`, `DownloadsApi`, `HealthApi`) from the SOPHON OpenAPI
37
+ spec.
38
+ - Hand-written helpers spliced on top of the generated client:
39
+ - `uploadFile` — chunked, concurrent, resumable upload with progress
40
+ reporting and bounded retry.
41
+ - `waitForJob` — typed terminal-state polling with backoff and
42
+ timeout.
43
+ - `verifyWebhookSignature` — constant-time HMAC-SHA256 verification
44
+ with a default replay window. Uses Web Crypto so it runs on Node
45
+ 18+ and modern browsers.
46
+ - Provenance signed via npm sigstore on every publish.
47
+
48
+ [0.1.4]: https://github.com/Liqhtworks/sophon-sdk-typescript/releases/tag/v0.1.4
49
+ [0.1.3]: https://github.com/Liqhtworks/sophon-sdk-typescript/releases/tag/v0.1.3
50
+ [0.1.2]: https://github.com/Liqhtworks/sophon-sdk-typescript/releases/tag/v0.1.2
51
+ [0.1.0]: https://github.com/Liqhtworks/sophon-sdk-typescript/releases/tag/v0.1.0
package/README.md CHANGED
@@ -14,42 +14,110 @@ npm install @liqhtworks/sophon-sdk
14
14
  Requires Node 18+ or a runtime with `fetch`, `Blob`, `AbortController`, and Web
15
15
  Crypto.
16
16
 
17
+ ## Get an API key
18
+
19
+ 1. Sign in at <https://liqhtworks.xyz/account/general>.
20
+ 2. In **API keys**, create a key for your server-side integration.
21
+ 3. Copy the `xt_live_...` token when it is shown. It is only shown once.
22
+ 4. Store it as an environment variable:
23
+
24
+ ```bash
25
+ export SOPHON_API_KEY=xt_live_...
26
+ export SOPHON_BASE_URL=https://api.liqhtworks.xyz
27
+ ```
28
+
29
+ Keep API keys on the server. Do not ship them in browser bundles, mobile apps,
30
+ public repos, logs, or analytics events.
31
+
17
32
  ## Quick Start
18
33
 
34
+ This is the smallest complete server-side flow: upload a local video, create an
35
+ encode job, wait for completion, and download the MP4 output.
36
+
19
37
  ```ts
20
38
  import {
21
39
  Configuration,
40
+ JobProfile,
41
+ JobSource,
42
+ JobStatus,
43
+ JobsApi,
22
44
  UploadsApi,
23
45
  uploadFile,
46
+ waitForJob,
24
47
  } from "@liqhtworks/sophon-sdk";
25
48
  import { Blob } from "node:buffer";
26
- import { readFile } from "node:fs/promises";
49
+ import { randomUUID } from "node:crypto";
50
+ import { readFile, writeFile } from "node:fs/promises";
51
+ import { basename } from "node:path";
52
+
53
+ const inputPath = process.argv[2] ?? "./source.mov";
54
+ const apiKey = process.env.SOPHON_API_KEY;
55
+ if (!apiKey) throw new Error("SOPHON_API_KEY is required");
27
56
 
57
+ const basePath = process.env.SOPHON_BASE_URL ?? "https://api.liqhtworks.xyz";
28
58
  const config = new Configuration({
29
- basePath: process.env.SOPHON_BASE_URL ?? "https://api.liqhtworks.xyz",
30
- accessToken: process.env.SOPHON_API_KEY,
59
+ basePath,
60
+ accessToken: apiKey,
31
61
  });
32
62
 
33
63
  const uploads = new UploadsApi(config);
64
+ const jobs = new JobsApi(config);
34
65
 
35
- const bytes = await readFile("./source.mov");
36
- const source = new Blob([bytes], { type: "video/quicktime" });
66
+ const bytes = await readFile(inputPath);
67
+ const mimeType = inputPath.endsWith(".mov") ? "video/quicktime" : "video/mp4";
68
+ const source = new Blob([bytes], { type: mimeType });
37
69
 
38
70
  const upload = await uploadFile({
39
71
  api: uploads,
40
72
  source,
41
- fileName: "source.mov",
42
- mimeType: "video/quicktime",
73
+ fileName: basename(inputPath),
74
+ mimeType,
43
75
  concurrency: 4,
44
76
  onProgress: (p) => console.log(`${p.partsDone}/${p.partsTotal} parts`),
45
77
  });
46
78
 
47
- console.log(upload.uploadId);
79
+ const job = await jobs.createJob({
80
+ idempotencyKey: randomUUID(),
81
+ createJobRequest: {
82
+ source: JobSource.upload(upload.uploadId),
83
+ profile: JobProfile.SOPHON_ESPRESSO,
84
+ },
85
+ });
86
+
87
+ const final = await waitForJob({
88
+ api: jobs,
89
+ jobId: job.id,
90
+ timeoutMs: 30 * 60 * 1000,
91
+ });
92
+ if (final.status !== JobStatus.COMPLETED) {
93
+ throw new Error(`job ended in ${final.status}`);
94
+ }
95
+
96
+ const redirect = await fetch(`${basePath}/v1/jobs/${final.id}/output`, {
97
+ headers: { authorization: `Bearer ${apiKey}` },
98
+ redirect: "manual",
99
+ });
100
+ const location = redirect.headers.get("location");
101
+ if (!location) throw new Error("missing output redirect");
102
+
103
+ const download = await fetch(new URL(location, basePath));
104
+ await writeFile("sophon-output.mp4", Buffer.from(await download.arrayBuffer()));
105
+
106
+ console.log(`wrote sophon-output.mp4 from ${final.id}`);
48
107
  ```
49
108
 
50
- For a standalone file-path upload recipe, see
109
+ For a runnable copy of this flow, see
110
+ [`examples/encode-file.mjs`](./examples/encode-file.mjs).
111
+
112
+ For upload-only integration work, see
51
113
  [`examples/upload-node-path.mjs`](./examples/upload-node-path.mjs).
52
114
 
115
+ ### Profile choice
116
+
117
+ Use `sophon-auto` for production unless you need deterministic encoder
118
+ settings. The quickstart uses `sophon-espresso` because it is the fastest
119
+ smoke-test profile and always produces a new encoded output.
120
+
53
121
  ## Webhooks
54
122
 
55
123
  Use `verifyWebhookSignature` with the raw request body before JSON parsing.
@@ -77,6 +145,22 @@ npm install
77
145
  npm run build
78
146
  ```
79
147
 
148
+ ## Versioning
149
+
150
+ `@liqhtworks/sophon-sdk` follows [SemVer](https://semver.org/), with one
151
+ pre-1.0 caveat: while we are at `v0.x`, **minor bumps may include
152
+ breaking changes**. Pin a tilde range until 1.0:
153
+
154
+ ```bash
155
+ npm install @liqhtworks/sophon-sdk@~0.1
156
+ ```
157
+
158
+ Patch releases (`0.1.x`) are always backward-compatible — they ship bug
159
+ fixes, helper-layer improvements, and additive types. Once we cut
160
+ `v1.0.0`, regular SemVer applies and breaking changes only land on
161
+ major bumps. See [`CHANGELOG.md`](./CHANGELOG.md) for the per-release
162
+ log.
163
+
80
164
  ## License
81
165
 
82
166
  Proprietary. See [`LICENSE`](./LICENSE).
@@ -0,0 +1,70 @@
1
+ import {
2
+ Configuration,
3
+ JobProfile,
4
+ JobSource,
5
+ JobStatus,
6
+ JobsApi,
7
+ UploadsApi,
8
+ uploadFile,
9
+ waitForJob,
10
+ } from "@liqhtworks/sophon-sdk";
11
+ import { Blob } from "node:buffer";
12
+ import { randomUUID } from "node:crypto";
13
+ import { readFile, writeFile } from "node:fs/promises";
14
+ import { basename } from "node:path";
15
+
16
+ const inputPath = process.argv[2];
17
+ if (!inputPath) {
18
+ throw new Error("usage: node examples/encode-file.mjs /path/to/video.mov");
19
+ }
20
+
21
+ const apiKey = process.env.SOPHON_API_KEY;
22
+ if (!apiKey) throw new Error("SOPHON_API_KEY is required");
23
+
24
+ const basePath = process.env.SOPHON_BASE_URL ?? "https://api.liqhtworks.xyz";
25
+ const config = new Configuration({ basePath, accessToken: apiKey });
26
+ const uploads = new UploadsApi(config);
27
+ const jobs = new JobsApi(config);
28
+
29
+ const bytes = await readFile(inputPath);
30
+ const mimeType = inputPath.endsWith(".mov") ? "video/quicktime" : "video/mp4";
31
+ const upload = await uploadFile({
32
+ api: uploads,
33
+ source: new Blob([bytes], { type: mimeType }),
34
+ fileName: basename(inputPath),
35
+ mimeType,
36
+ concurrency: 4,
37
+ onProgress: (p) => console.log(`upload ${p.partsDone}/${p.partsTotal}`),
38
+ });
39
+
40
+ const job = await jobs.createJob({
41
+ idempotencyKey: randomUUID(),
42
+ createJobRequest: {
43
+ source: JobSource.upload(upload.uploadId),
44
+ profile: JobProfile.SOPHON_ESPRESSO,
45
+ },
46
+ });
47
+ console.log(`created ${job.id}`);
48
+
49
+ const final = await waitForJob({
50
+ api: jobs,
51
+ jobId: job.id,
52
+ timeoutMs: 30 * 60 * 1000,
53
+ onProgress: (j) => console.log(`job ${j.id}: ${j.status}`),
54
+ });
55
+ if (final.status !== JobStatus.COMPLETED) {
56
+ throw new Error(`job ended in ${final.status}`);
57
+ }
58
+
59
+ const redirect = await fetch(`${basePath}/v1/jobs/${final.id}/output`, {
60
+ headers: { authorization: `Bearer ${apiKey}` },
61
+ redirect: "manual",
62
+ });
63
+ const location = redirect.headers.get("location");
64
+ if (!location) throw new Error("missing output redirect");
65
+
66
+ const download = await fetch(new URL(location, basePath));
67
+ if (!download.ok) throw new Error(`download failed: ${download.status}`);
68
+
69
+ await writeFile("sophon-output.mp4", Buffer.from(await download.arrayBuffer()));
70
+ console.log("wrote sophon-output.mp4");
@@ -0,0 +1,32 @@
1
+ import {
2
+ Configuration,
3
+ UploadsApi,
4
+ uploadFile,
5
+ } from "@liqhtworks/sophon-sdk";
6
+ import { Blob } from "node:buffer";
7
+ import { basename } from "node:path";
8
+ import { readFile } from "node:fs/promises";
9
+
10
+ const path = process.argv[2];
11
+ if (!path) {
12
+ throw new Error("usage: node examples/upload-node-path.mjs /path/to/video.mov");
13
+ }
14
+
15
+ const config = new Configuration({
16
+ basePath: process.env.SOPHON_BASE_URL ?? "https://api.liqhtworks.xyz",
17
+ accessToken: process.env.SOPHON_API_KEY,
18
+ });
19
+
20
+ const bytes = await readFile(path);
21
+ const source = new Blob([bytes], { type: "video/quicktime" });
22
+ const uploads = new UploadsApi(config);
23
+
24
+ const upload = await uploadFile({
25
+ api: uploads,
26
+ source,
27
+ fileName: basename(path),
28
+ mimeType: "video/quicktime",
29
+ concurrency: 4,
30
+ });
31
+
32
+ console.log(upload.uploadId);
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "sophon-webhook-server-example",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "node src/server.mjs",
7
+ "check": "node --check src/server.mjs"
8
+ },
9
+ "dependencies": {
10
+ "@liqhtworks/sophon-sdk": "latest",
11
+ "express": "^4.19.2"
12
+ }
13
+ }
@@ -0,0 +1,51 @@
1
+ import express from "express";
2
+ import {
3
+ WebhookSignatureError,
4
+ verifyWebhookSignature,
5
+ } from "@liqhtworks/sophon-sdk";
6
+
7
+ const secret = process.env.SOPHON_WEBHOOK_SECRET;
8
+ if (!secret) {
9
+ throw new Error("SOPHON_WEBHOOK_SECRET is required");
10
+ }
11
+
12
+ const app = express();
13
+
14
+ app.post(
15
+ "/webhooks/sophon",
16
+ express.raw({ type: "application/json", limit: "2mb" }),
17
+ async (req, res) => {
18
+ if (!Buffer.isBuffer(req.body)) {
19
+ res.status(400).send("raw body required");
20
+ return;
21
+ }
22
+
23
+ try {
24
+ await verifyWebhookSignature({
25
+ rawBody: req.body,
26
+ signatureHeader: req.get("X-Turbo-Signature-256"),
27
+ timestampHeader: req.get("X-Turbo-Timestamp"),
28
+ secret,
29
+ });
30
+ } catch (err) {
31
+ if (err instanceof WebhookSignatureError) {
32
+ console.warn("rejected SOPHON webhook", { reason: err.reason });
33
+ }
34
+ res.status(401).send("invalid signature");
35
+ return;
36
+ }
37
+
38
+ const event = JSON.parse(req.body.toString("utf8"));
39
+ console.log("accepted SOPHON webhook", {
40
+ type: event.type,
41
+ id: event.id,
42
+ });
43
+
44
+ res.sendStatus(204);
45
+ },
46
+ );
47
+
48
+ const port = Number(process.env.PORT ?? 3000);
49
+ app.listen(port, () => {
50
+ console.log(`listening on :${port}`);
51
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liqhtworks/sophon-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "OpenAPI client for @liqhtworks/sophon-sdk",
5
5
  "author": "OpenAPI-Generator",
6
6
  "repository": {