@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.
- package/.github/CODEOWNERS +6 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +34 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/CHANGELOG.md +51 -0
- package/README.md +93 -9
- package/examples/encode-file.mjs +70 -0
- package/examples/upload-node-path.mjs +32 -0
- package/examples/webhook-server/package.json +13 -0
- package/examples/webhook-server/src/server.mjs +51 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
|
30
|
-
accessToken:
|
|
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(
|
|
36
|
-
const
|
|
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:
|
|
42
|
-
mimeType
|
|
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
|
-
|
|
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
|
|
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
|
+
});
|