@nitida/sdk 0.27.1 → 0.29.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/AGENTS.md +17 -1
- package/LICENSE +21 -0
- package/README.md +46 -10
- package/SECURITY.md +56 -0
- package/dist/expo.d.ts +4 -4
- package/dist/expo.js +2 -2
- package/dist/expo.js.map +1 -1
- package/dist/index.d.ts +52 -10
- package/dist/index.js +28 -7
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +28 -7
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +2 -2
- package/dist/web.js +28 -7
- package/dist/web.js.map +1 -1
- package/package.json +7 -5
- package/skills/nitida-sdk/SKILL.md +19 -2
- package/src/expo/index.ts +5 -5
- package/src/index.ts +89 -14
- package/src/server/index.ts +3 -0
- package/src/web/index.ts +6 -3
package/AGENTS.md
CHANGED
|
@@ -101,6 +101,22 @@ await getPrivateTransformUrl(asset, { width: 1280 }, signingKey, { expiresInSeco
|
|
|
101
101
|
- **`exp` is mandatory**; revocation is *"within a minute"* (60 s TTL at the edge).
|
|
102
102
|
- **The signing key is a backend secret** — it mints URLs for every private
|
|
103
103
|
asset the tenant owns.
|
|
104
|
+
- **⭐ Where the signing key comes from: the response that CREATED your
|
|
105
|
+
project, once.** `POST /admin/projects` returns `signingKey` next to the
|
|
106
|
+
three API keys, and the console shows it in the same panel. Nothing else
|
|
107
|
+
hands it out — `GET /admin/projects/:code` does **not** include it. If it is
|
|
108
|
+
lost, the only endpoint that returns a key is
|
|
109
|
+
`POST /admin/projects/:code/rotate-signing-key`, which **invalidates every
|
|
110
|
+
URL already signed** and needs the system-scope key the platform operator
|
|
111
|
+
holds. A tenant with nothing signed yet can rotate for free; a live one
|
|
112
|
+
cannot, which is why you save it at creation.
|
|
113
|
+
- **⭐ Already have a project and never saw a signing key?** Then you never got
|
|
114
|
+
one — projects created before 2026-08-23 were not handed it, and no endpoint
|
|
115
|
+
shows you the current one. **If you have not signed any URLs yet, ask us to
|
|
116
|
+
rotate: with nothing in flight, rotation invalidates nothing and is free.**
|
|
117
|
+
If you already have signed URLs circulating, ask us for the current key
|
|
118
|
+
instead. This is the wall the T5 agent hit, and the sentence that was
|
|
119
|
+
missing.
|
|
104
120
|
|
|
105
121
|
## Two ways an image gets smaller, and only one of them is yours to call
|
|
106
122
|
|
|
@@ -151,4 +167,4 @@ degrades to a wrong choice.
|
|
|
151
167
|
|---|---|
|
|
152
168
|
| `@nitida/asset-client` | URL builders + preset tables. No network, no key. |
|
|
153
169
|
| `@nitida/asset-compressor-web` | Browser image compression (worker → OffscreenCanvas → WebP). |
|
|
154
|
-
| `@
|
|
170
|
+
| `@nitida/asset-uploader-web` | Per-file transport: multipart, resume via IndexedDB, progress. |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Espacio Futuro LTD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ That `/server` and `/web` really do mirror the root is **asserted in CI**, not
|
|
|
55
55
|
maintained by hand. It was not always true: before 2026-08-21 `/server` was
|
|
56
56
|
short 28 of the root's 72 exports and `/web` short 37.
|
|
57
57
|
|
|
58
|
-
Uploaders + compressors are **optional** peer deps (`@
|
|
58
|
+
Uploaders + compressors are **optional** peer deps (`@nitida/asset-uploader-{web,expo}`,
|
|
59
59
|
`@nitida/asset-compressor-{web,native}`). Skip them if your app only resolves
|
|
60
60
|
slots and reads assets — your bundle stays a few KB.
|
|
61
61
|
|
|
@@ -229,10 +229,13 @@ Peer deps: `@nitida/asset-client` (URL builders + types) and `react` (only if
|
|
|
229
229
|
you use the `/react` subpath). Both compressors are optional and lazy — the
|
|
230
230
|
SDK never imports them unless you call a compress path.
|
|
231
231
|
|
|
232
|
-
>
|
|
233
|
-
>
|
|
234
|
-
> single PUT and is what every consumer uses
|
|
235
|
-
> for where
|
|
232
|
+
> The multipart uploader packages (`@nitida/asset-uploader-web` / `-expo`) are
|
|
233
|
+
> on npm since 2026-08-23. You do not need them for ordinary uploads:
|
|
234
|
+
> `aq.upload()` covers a file in a single PUT and is what every consumer uses
|
|
235
|
+
> today — see *Large uploads* below for where that ceiling actually is. Reach
|
|
236
|
+
> for an uploader when you need **resume**: a 2 GB upload that must survive a
|
|
237
|
+
> reload (web, IndexedDB) or the app being backgrounded (Expo, iOS URLSession
|
|
238
|
+
> / Android WorkManager).
|
|
236
239
|
|
|
237
240
|
## Quick start
|
|
238
241
|
|
|
@@ -259,8 +262,11 @@ const heroes = await aq.slots.resolveMany([
|
|
|
259
262
|
"storefront.home.tile-2",
|
|
260
263
|
]);
|
|
261
264
|
|
|
262
|
-
// Lower-level operations.
|
|
265
|
+
// Lower-level operations. `byHash` takes the full 64-hex sha256 that
|
|
266
|
+
// `upload()` returns as `.sha256`, OR its 16-char prefix — the short form
|
|
267
|
+
// that appears inside every CDN URL. Both resolve to the same asset.
|
|
263
268
|
const asset = await aq.assets.byHash("3c…<64 hex>…");
|
|
269
|
+
const same = await aq.assets.byHash("3c8f1a20b7d94e05"); // 16-char prefix ✓
|
|
264
270
|
const { assets, nextCursor } = await aq.assets.list({ limit: 50 });
|
|
265
271
|
|
|
266
272
|
// Uploads — hash-deduped; returns the canonical v2 URL immediately.
|
|
@@ -431,7 +437,7 @@ export function UploadHeroScreen() {
|
|
|
431
437
|
}
|
|
432
438
|
```
|
|
433
439
|
|
|
434
|
-
⚠️ The Expo background-upload path depends on `@
|
|
440
|
+
⚠️ The Expo background-upload path depends on `@nitida/asset-uploader-expo`,
|
|
435
441
|
which is **not on npm**. `aq.upload()` works on Expo today without it; the
|
|
436
442
|
background/resumable variant is not something an external consumer can install
|
|
437
443
|
yet.
|
|
@@ -894,6 +900,29 @@ as they specify the same params.
|
|
|
894
900
|
|
|
895
901
|
Every tenant has an HMAC-SHA256 signing key — 32 random bytes, generated
|
|
896
902
|
on tenant creation.
|
|
903
|
+
|
|
904
|
+
> **Where you get it: the response that created your project, once.**
|
|
905
|
+
> `POST /admin/projects` returns `signingKey` next to the three API keys, and
|
|
906
|
+
> the console shows it in the same panel. Save it with the keys — nothing else
|
|
907
|
+
> hands it out. In particular `GET /admin/projects/:code` does **not** return
|
|
908
|
+
> it, and neither does any tenant-scoped endpoint.
|
|
909
|
+
>
|
|
910
|
+
> **Your project already exists and you never saw a signing key?** Then you
|
|
911
|
+
> never got one: projects created before 2026-08-23 were not handed it, and
|
|
912
|
+
> there is no endpoint that shows you the current one. This is the exact wall
|
|
913
|
+
> to hit, so here is the way through it:
|
|
914
|
+
>
|
|
915
|
+
> - **If you have not signed any URLs yet** — which is true of every project
|
|
916
|
+
> that has not shipped private assets — ask us to rotate. Rotation returns a
|
|
917
|
+
> key, and with nothing in flight it invalidates nothing. It is free.
|
|
918
|
+
> - **If you already have signed URLs in circulation**, rotation kills them.
|
|
919
|
+
> Ask us for the current key instead; we can read it.
|
|
920
|
+
>
|
|
921
|
+
> Either way it is one request to us, because
|
|
922
|
+
> `POST /admin/projects/:code/rotate-signing-key` needs the system-scope key
|
|
923
|
+
> the platform operator holds — your own admin key answers
|
|
924
|
+
> `403 SYSTEM_KEY_REQUIRED`.
|
|
925
|
+
|
|
897
926
|
Optionally enable `strict_transforms = true` to reject unsigned URLs
|
|
898
927
|
with a 401 — useful when transform URLs leak from a private surface
|
|
899
928
|
(internal admin, b2b portal) and you don't want third parties
|
|
@@ -905,7 +934,8 @@ const aq = new NitidaClient({
|
|
|
905
934
|
apiKey: process.env.AQUIENPZ_API_KEY!,
|
|
906
935
|
tenantCode: "your-tenant",
|
|
907
936
|
tenantId: 42,
|
|
908
|
-
//
|
|
937
|
+
// Handed to you once, by the response that created the project.
|
|
938
|
+
// do NOT ship to the browser.
|
|
909
939
|
signingKey: process.env.AQUIENPZ_SIGNING_KEY!,
|
|
910
940
|
});
|
|
911
941
|
|
|
@@ -1304,7 +1334,7 @@ API key tiers:
|
|
|
1304
1334
|
| `aq.slots` | `bind(key, {assetId, preset})` | Admin rebind |
|
|
1305
1335
|
| `aq.slots` | `unbind(key)` | Remove binding |
|
|
1306
1336
|
| `aq.slots` | `invalidateCache(key?)` | After admin rebind |
|
|
1307
|
-
| `aq.assets` | `byHash(sha)` / `byHashes([])` | Lookup |
|
|
1337
|
+
| `aq.assets` | `byHash(sha)` / `byHashes([])` | Lookup — full 64-hex sha **or** its 16-char prefix |
|
|
1308
1338
|
| `aq.assets` | `list({limit, cursor})` | Paginated |
|
|
1309
1339
|
| `aq.assets` | `get(id)` | Full DTO |
|
|
1310
1340
|
| `aq.assets` | `patchMetadata(id, {…})` | Merge JSON |
|
|
@@ -1356,4 +1386,10 @@ or a cache hit runs no encoder and books nothing, so `0` means zero.
|
|
|
1356
1386
|
|
|
1357
1387
|
## License
|
|
1358
1388
|
|
|
1359
|
-
|
|
1389
|
+
MIT — see [LICENSE](./LICENSE).
|
|
1390
|
+
|
|
1391
|
+
⚠️ **The licence covers this client code, not the service.** MIT lets you use,
|
|
1392
|
+
modify and redistribute the SDK; it does not grant access to nitida. The
|
|
1393
|
+
service is governed by its own terms and plan tiers, and every call still needs
|
|
1394
|
+
credentials issued to your project. Same shape as every other API client you
|
|
1395
|
+
already depend on.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Email **security@espaciofuturo.io**. Please do not open a public issue — a
|
|
6
|
+
public report is a disclosure, and it reaches attackers at the same moment it
|
|
7
|
+
reaches us.
|
|
8
|
+
|
|
9
|
+
Include what you have: the affected package and version, what you did, and what
|
|
10
|
+
happened. A URL that reproduces it is worth more than a paragraph describing
|
|
11
|
+
one. We will confirm receipt and tell you what we found.
|
|
12
|
+
|
|
13
|
+
## What is in scope
|
|
14
|
+
|
|
15
|
+
The published client packages and the service they talk to:
|
|
16
|
+
|
|
17
|
+
- `@nitida/sdk`, `@nitida/asset-client`
|
|
18
|
+
- `@nitida/asset-compressor-web`, `@nitida/asset-compressor-native`
|
|
19
|
+
- `@nitida/asset-uploader-web`, `@nitida/asset-uploader-expo`
|
|
20
|
+
- the delivery edge (`8ok.uk`) and the API (`api.nitida.gofuture.space`)
|
|
21
|
+
|
|
22
|
+
## Two behaviours that look like bugs and are not
|
|
23
|
+
|
|
24
|
+
Reported often enough to state up front, so a real report is not lost among
|
|
25
|
+
them:
|
|
26
|
+
|
|
27
|
+
**A 404 on a private asset is the feature.** An asset with
|
|
28
|
+
`visibility: "private"` answers 404 on every public URL — the raw path, every
|
|
29
|
+
stored variant, every `/t/` transform and every HLS segment. It is not a
|
|
30
|
+
missing file. Signed access lives at `/a/{tenant}/…?exp&sig`, minted on your
|
|
31
|
+
backend. See the SDK's `getPrivateAssetUrl`.
|
|
32
|
+
|
|
33
|
+
**A presigned upload URL is a bearer credential, on purpose.** The browser
|
|
34
|
+
uploader persists presigned R2 PUT URLs in IndexedDB, because that is what
|
|
35
|
+
resuming after a reload means. They are write-only, scoped to the parts of one
|
|
36
|
+
upload the holder started, expire in 60 minutes, and are deleted on complete
|
|
37
|
+
and on abort.
|
|
38
|
+
|
|
39
|
+
## Credentials, and the one mistake that matters
|
|
40
|
+
|
|
41
|
+
An API key (`amk_rt_*`, `amk_ad_*`, `amk_ci_*`) is a **server-side** credential.
|
|
42
|
+
It must never reach a browser bundle, an `NEXT_PUBLIC_*` variable, or a mobile
|
|
43
|
+
app binary.
|
|
44
|
+
|
|
45
|
+
The same goes for a tenant's **URL signing key**: it mints signed URLs for
|
|
46
|
+
every private asset that tenant owns. Sign on your backend, or pre-sign at
|
|
47
|
+
build time.
|
|
48
|
+
|
|
49
|
+
The client packages are built so this is hard to get wrong — the web entrypoint
|
|
50
|
+
`@nitida/sdk/web` does not accept `apiKey` or `signingKey` at all; the types
|
|
51
|
+
reject them. If you find a path that leaks either one, that is exactly the
|
|
52
|
+
report we want.
|
|
53
|
+
|
|
54
|
+
If you believe a key has been exposed, email the address above and we will
|
|
55
|
+
rotate it. Rotating a signing key invalidates every URL already signed with it,
|
|
56
|
+
so tell us whether you have signed URLs in circulation.
|
package/dist/expo.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { UploadTaskOptions, UploadTask } from '@
|
|
2
|
-
export { UploadEvents, UploadFileInput, UploadSessionState, UploadTask, UploadTaskOptions, cancelResumableSession, listResumableSessions } from '@
|
|
1
|
+
import { UploadTaskOptions, UploadTask } from '@nitida/asset-uploader-expo';
|
|
2
|
+
export { UploadEvents, UploadFileInput, UploadSessionState, UploadTask, UploadTaskOptions, cancelResumableSession, listResumableSessions } from '@nitida/asset-uploader-expo';
|
|
3
3
|
import { NitidaClient } from './index.js';
|
|
4
4
|
import '@nitida/asset-client';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @nitida/sdk/expo — native multipart uploads for React Native / Expo.
|
|
8
8
|
*
|
|
9
|
-
* Wraps `@
|
|
9
|
+
* Wraps `@nitida/asset-uploader-expo`'s `UploadTask`, which delegates
|
|
10
10
|
* the actual byte transfer to a native background session (URLSession
|
|
11
11
|
* on iOS, WorkManager on Android). The upload survives:
|
|
12
12
|
* - JS thread freezing
|
|
@@ -58,7 +58,7 @@ import '@nitida/asset-client';
|
|
|
58
58
|
* is not built. It is the same gap `/web` documents. Ask; there is no public
|
|
59
59
|
* tracker.
|
|
60
60
|
*
|
|
61
|
-
* Peer dep: `@
|
|
61
|
+
* Peer dep: `@nitida/asset-uploader-expo` (lazy — apps that don't
|
|
62
62
|
* use the mobile SDK skip the install).
|
|
63
63
|
* @module @nitida/sdk/expo
|
|
64
64
|
*/
|
package/dist/expo.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// src/expo/index.ts
|
|
2
2
|
import {
|
|
3
3
|
UploadTask
|
|
4
|
-
} from "@
|
|
4
|
+
} from "@nitida/asset-uploader-expo";
|
|
5
5
|
import {
|
|
6
6
|
cancelResumableSession,
|
|
7
7
|
listResumableSessions,
|
|
8
8
|
UploadTask as UploadTask2
|
|
9
|
-
} from "@
|
|
9
|
+
} from "@nitida/asset-uploader-expo";
|
|
10
10
|
function createExpoUploader(client, options) {
|
|
11
11
|
const access = client;
|
|
12
12
|
return new UploadTask({
|
package/dist/expo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/expo/index.ts"],"sourcesContent":["/**\n * @nitida/sdk/expo — native multipart uploads for React Native / Expo.\n *\n * Wraps `@
|
|
1
|
+
{"version":3,"sources":["../src/expo/index.ts"],"sourcesContent":["/**\n * @nitida/sdk/expo — native multipart uploads for React Native / Expo.\n *\n * Wraps `@nitida/asset-uploader-expo`'s `UploadTask`, which delegates\n * the actual byte transfer to a native background session (URLSession\n * on iOS, WorkManager on Android). The upload survives:\n * - JS thread freezing\n * - App backgrounding\n * - OS-initiated kill (low-memory, user swiping away)\n * - Network blips (retries with backoff)\n *\n * Usage:\n *\n * import { createExpoUploader, listResumableSessions } from \"@nitida/sdk/expo\";\n *\n * const aq = new NitidaClient({ ... });\n *\n * // Resume any uploads from a prior app launch on boot.\n * const resumable = await listResumableSessions();\n * // ...show a banner offering to resume them\n *\n * const upload = createExpoUploader(aq, {\n * file: { uri: assetUri, mime: \"video/mp4\", name: \"tour.mp4\" },\n * });\n * upload.on(\"progress\", ({ ratio }) => setProgress(ratio));\n * const { assetId } = await upload.start();\n * await aq.slots.bind(\"storefront.tour.video\", { assetId, preset: \"video\" });\n *\n * ⚠️ THIS PATH PUTS A RUNTIME KEY ON THE DEVICE. Read before shipping.\n *\n * `createExpoUploader` reads `client.opts.apiKey` and hands it to the native\n * session as its `authToken` (see below — it is four lines, and they are the\n * whole story). The native uploader talks to the API directly, so it needs a\n * credential the OS can replay hours later from a background task. There is no\n * BFF in that loop to inject one.\n *\n * That is the opposite of what every other surface here does, and the opposite\n * of what `@nitida/sdk/web` chose when it hit the SAME constraint: `/web`\n * deliberately does NOT expose the multipart uploader, because it needs a raw\n * `authToken` a BFF cannot supply. `/expo` exposes it anyway, because\n * background-surviving uploads are the entire reason a native session exists.\n *\n * So, concretely, an `amk_rt_*` key in your app bundle is readable by anyone\n * who unzips the IPA/APK, and it is a WRITE key to a paid platform.\n *\n * - Uploading big video in the background is worth it to you ⇒ use this, and\n * scope the key to one tenant so a leak is contained and revocable.\n * - It is not ⇒ build the client from `@nitida/sdk/web` pointed at your own\n * route and call `aq.upload(file)`. No key on the device. You lose survival\n * across backgrounding and OS kill; the upload dies with the JS thread.\n *\n * The real fix — a BFF-minted short-lived token the native session can carry —\n * is not built. It is the same gap `/web` documents. Ask; there is no public\n * tracker.\n *\n * Peer dep: `@nitida/asset-uploader-expo` (lazy — apps that don't\n * use the mobile SDK skip the install).\n * @module @nitida/sdk/expo\n */\n\nimport {\n UploadTask,\n type UploadTaskOptions,\n} from \"@nitida/asset-uploader-expo\";\nimport type { NitidaClient } from \"..\";\n\nexport type ExpoUploadOptions = Omit<\n UploadTaskOptions,\n \"tenantCode\" | \"endpoint\" | \"authToken\"\n>;\n\n/**\n * Spawn a native-backed `UploadTask` bound to a configured client.\n *\n * ⚠️ It reaches into the client for `apiKey` and uses it as the session's\n * `authToken`, so the client you pass MUST have been built with a real runtime\n * key — which means that key is on the device. See the module header for what\n * that costs and what the alternative is.\n */\nexport function createExpoUploader(\n client: NitidaClient,\n options: ExpoUploadOptions,\n): UploadTask {\n const access = client as unknown as {\n opts: { endpoint: string; apiKey: string; tenantCode: string };\n };\n return new UploadTask({\n ...options,\n endpoint: access.opts.endpoint,\n authToken: access.opts.apiKey,\n tenantCode: access.opts.tenantCode,\n });\n}\n\nexport type {\n UploadEvents,\n UploadFileInput,\n UploadSessionState,\n UploadTaskOptions,\n} from \"@nitida/asset-uploader-expo\";\nexport {\n cancelResumableSession,\n listResumableSessions,\n UploadTask,\n} from \"@nitida/asset-uploader-expo\";\n"],"mappings":";AA4DA;AAAA,EACE;AAAA,OAEK;AAqCP;AAAA,EACE;AAAA,EACA;AAAA,EACA,cAAAA;AAAA,OACK;AAzBA,SAAS,mBACd,QACA,SACY;AACZ,QAAM,SAAS;AAGf,SAAO,IAAI,WAAW;AAAA,IACpB,GAAG;AAAA,IACH,UAAU,OAAO,KAAK;AAAA,IACtB,WAAW,OAAO,KAAK;AAAA,IACvB,YAAY,OAAO,KAAK;AAAA,EAC1B,CAAC;AACH;","names":["UploadTask"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ResolveSlotOptions, SlotResolution, SlotDTO, VariantPreset, AssetDTO, AssetVariant, RequestablePreset, TransformOptions, SignedTransformOptions } from '@nitida/asset-client';
|
|
2
|
-
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl } from '@nitida/asset-client';
|
|
2
|
+
export { AssetDTO, AssetPalette, AssetVariant, HlsRung, PRESET_EXT, PRESET_LONG, PRESET_MAX_DIM, PRESET_SHORT, PaletteSwatch, REQUESTABLE_PRESETS, RequestablePreset, ResolveSlotOptions, SignAccessOptions, SignedTransformOptions, SlotDTO, SlotResolution, TRANSFORM_WIDTHS, TransformEffect, TransformFit, TransformFormat, TransformGravity, TransformOptions, TransformWidth, VariantEntryPreset, VariantPreset, VisibilityHint, accessMessage, assertPublic, assertSha, bestTextContrast, computeVariantDimensions, configureSlotResolver, contrastRatio, deriveAccessKey, extractAssetSha, getAmbientGradient, getAssetDimensions, getAssetSrcSet, getAssetUrl, getCdnBase, getHlsLadder, getHlsStreamingUrl, getPaletteBlurBackground, getPaletteCssVars, getPrivateAssetUrl, getPrivateTransformUrl, getSignedTransformUrl, getTenantId, getTextColorForBackground, getTransformSrcSet, getTransformUrl, getVideoTransformUrl, hasPreset, hlsLadderAlignment, invalidateSlotCache, isRequestablePreset, iteratePaletteSwatches, pickAmbientBackground, relativeLuminance, resolveSlot, resolveSlots, serializeTransform, setCdnBase, setTenantId, signAccessUrl, signTransformUrl, toRequestablePresets } from '@nitida/asset-client';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Which audio uploads already play everywhere — the single source of truth for
|
|
@@ -40,7 +40,7 @@ declare function isUniversallyPlayableAudio(mime: string): boolean;
|
|
|
40
40
|
* platform.
|
|
41
41
|
*
|
|
42
42
|
* One ergonomic facade over the underlying packages
|
|
43
|
-
* (`@nitida/asset-client` URL builders + `@
|
|
43
|
+
* (`@nitida/asset-client` URL builders + `@nitida/asset-uploader-web`
|
|
44
44
|
* + the slot resolver). Auth is a bearer API key (`amk_rt_*`), issued
|
|
45
45
|
* per tenant when the tenant is created; tenant scope comes from
|
|
46
46
|
* the key's metadata (`X-Tenant-Code` is log-only).
|
|
@@ -119,12 +119,25 @@ type NitidaClientOptions = {
|
|
|
119
119
|
* Tenant's HMAC signing key for transform URLs (Phase 3). Required
|
|
120
120
|
* only when calling `aq.transform(asset, opts, { sign: true })`.
|
|
121
121
|
*
|
|
122
|
-
* 32 random bytes, generated server-side on tenant creation
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
122
|
+
* 32 random bytes, generated server-side on tenant creation.
|
|
123
|
+
*
|
|
124
|
+
* **Where you get it: the response to your project's creation, once.**
|
|
125
|
+
* `POST /admin/projects` returns `signingKey` next to the three API keys,
|
|
126
|
+
* and the console shows it in the same panel. It is shown there and nowhere
|
|
127
|
+
* else — save it with the keys.
|
|
128
|
+
*
|
|
129
|
+
* **Never saw one?** Projects created before 2026-08-23 were not handed it,
|
|
130
|
+
* and no endpoint shows you the current one. If you have not signed any URLs
|
|
131
|
+
* yet — true of every project that has not shipped private assets — ask for
|
|
132
|
+
* a rotation: with nothing in flight it invalidates nothing and is free. If
|
|
133
|
+
* you already have signed URLs circulating, ask for the current key instead.
|
|
134
|
+
*
|
|
135
|
+
* Either way it is one request to the platform operator:
|
|
136
|
+
* `POST /admin/projects/:code/rotate-signing-key` needs a SYSTEM-scope
|
|
137
|
+
* credential, and your own admin key answers `403 SYSTEM_KEY_REQUIRED`.
|
|
138
|
+
*
|
|
139
|
+
* **Keep it server-side only** — do not ship in `NEXT_PUBLIC_*` env vars.
|
|
140
|
+
* Sign URLs from a BFF route handler, or pre-sign at build time.
|
|
128
141
|
*/
|
|
129
142
|
signingKey?: string;
|
|
130
143
|
};
|
|
@@ -319,7 +332,12 @@ type ComposeMarketingResult = {
|
|
|
319
332
|
declare class AssetsApi {
|
|
320
333
|
private readonly opts;
|
|
321
334
|
constructor(opts: NitidaClientOptions);
|
|
322
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* Look up an asset by sha256. Accepts the full 64-hex digest that
|
|
337
|
+
* `upload()` returns as `sha256`, or the 16-char short prefix that appears
|
|
338
|
+
* in every CDN URL. Returns null on 404; throws with a message naming the
|
|
339
|
+
* expected shape if the string is neither form.
|
|
340
|
+
*/
|
|
323
341
|
byHash(sha256: string): Promise<AssetDTO | null>;
|
|
324
342
|
/** Bulk lookup by sha256s. */
|
|
325
343
|
byHashes(hashes: string[]): Promise<{
|
|
@@ -461,7 +479,31 @@ declare class AssetsApi {
|
|
|
461
479
|
* Default timeout is 5 minutes; videos / HLS ladders may need a
|
|
462
480
|
* higher cap (pass `10 * 60_000` for compositions, transcodes).
|
|
463
481
|
*/
|
|
464
|
-
waitReady(assetId: string,
|
|
482
|
+
waitReady(assetId: string,
|
|
483
|
+
/**
|
|
484
|
+
* Milliseconds, OR `{ timeoutMs }`.
|
|
485
|
+
*
|
|
486
|
+
* ⭐ It accepts the object because that is what people write. Found
|
|
487
|
+
* 2026-08-23 while smoke-testing a brand-new tenant: I passed
|
|
488
|
+
* `{ timeoutMs: 90_000 }` — every other option-taking method in this SDK
|
|
489
|
+
* takes an object — and got
|
|
490
|
+
*
|
|
491
|
+
* Error: waitReady: no asset a379fbbb-…
|
|
492
|
+
*
|
|
493
|
+
* about an asset that existed, was `ready`, and whose variants were all
|
|
494
|
+
* serving 200. `Date.now() - start < {…}` compares against NaN, so the
|
|
495
|
+
* loop body never ran, `last` stayed null, and the message blamed the one
|
|
496
|
+
* thing that was fine.
|
|
497
|
+
*
|
|
498
|
+
* TypeScript catches the wrong shape. Running it through `bun` does not —
|
|
499
|
+
* the same gap that let `upload()`'s `sha256` reach a URL builder wanting
|
|
500
|
+
* `sha` and produce `.../undefined.webp`. The lesson there was the same as
|
|
501
|
+
* here: a guard with a good message is still a mistake the user has to
|
|
502
|
+
* make first. Make the obvious call correct instead.
|
|
503
|
+
*/
|
|
504
|
+
timeout?: number | {
|
|
505
|
+
timeoutMs?: number;
|
|
506
|
+
}): Promise<AssetDTO>;
|
|
465
507
|
/**
|
|
466
508
|
* Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip
|
|
467
509
|
* segments into a single MP4 composition. Returns the processing asset
|
package/dist/index.js
CHANGED
|
@@ -53,12 +53,14 @@ import {
|
|
|
53
53
|
hasPreset as hasPreset2,
|
|
54
54
|
hlsLadderAlignment,
|
|
55
55
|
invalidateSlotCache as invalidateSlotCache2,
|
|
56
|
+
isRequestablePreset as isRequestablePreset2,
|
|
56
57
|
iteratePaletteSwatches,
|
|
57
58
|
PRESET_EXT,
|
|
58
59
|
PRESET_LONG,
|
|
59
60
|
PRESET_MAX_DIM,
|
|
60
61
|
PRESET_SHORT,
|
|
61
62
|
pickAmbientBackground,
|
|
63
|
+
REQUESTABLE_PRESETS as REQUESTABLE_PRESETS2,
|
|
62
64
|
relativeLuminance,
|
|
63
65
|
resolveSlot as resolveSlot2,
|
|
64
66
|
resolveSlots as resolveSlots2,
|
|
@@ -67,7 +69,8 @@ import {
|
|
|
67
69
|
setTenantId as setTenantId2,
|
|
68
70
|
signAccessUrl,
|
|
69
71
|
signTransformUrl,
|
|
70
|
-
TRANSFORM_WIDTHS
|
|
72
|
+
TRANSFORM_WIDTHS,
|
|
73
|
+
toRequestablePresets as toRequestablePresets2
|
|
71
74
|
} from "@nitida/asset-client";
|
|
72
75
|
function endpointUrl(opts, path, searchParams) {
|
|
73
76
|
const endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
@@ -199,7 +202,12 @@ var AssetsApi = class {
|
|
|
199
202
|
this.opts = opts;
|
|
200
203
|
}
|
|
201
204
|
opts;
|
|
202
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Look up an asset by sha256. Accepts the full 64-hex digest that
|
|
207
|
+
* `upload()` returns as `sha256`, or the 16-char short prefix that appears
|
|
208
|
+
* in every CDN URL. Returns null on 404; throws with a message naming the
|
|
209
|
+
* expected shape if the string is neither form.
|
|
210
|
+
*/
|
|
203
211
|
async byHash(sha256) {
|
|
204
212
|
const r = await fetch(
|
|
205
213
|
endpointHref(this.opts, `/assets/by-hash/${sha256}`),
|
|
@@ -414,7 +422,13 @@ var AssetsApi = class {
|
|
|
414
422
|
* Default timeout is 5 minutes; videos / HLS ladders may need a
|
|
415
423
|
* higher cap (pass `10 * 60_000` for compositions, transcodes).
|
|
416
424
|
*/
|
|
417
|
-
async waitReady(assetId,
|
|
425
|
+
async waitReady(assetId, timeout = 5 * 6e4) {
|
|
426
|
+
const timeoutMs = typeof timeout === "number" ? timeout : timeout !== null && typeof timeout === "object" ? timeout.timeoutMs ?? 5 * 6e4 : Number.NaN;
|
|
427
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`waitReady: timeout must be a positive number of milliseconds or { timeoutMs }, got ${JSON.stringify(timeout)}. A non-numeric timeout makes the poll loop exit before its first iteration, which used to surface as "no asset <id>" about an asset that was fine.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
418
432
|
const start = Date.now();
|
|
419
433
|
let delay = 500;
|
|
420
434
|
let last = null;
|
|
@@ -424,7 +438,11 @@ var AssetsApi = class {
|
|
|
424
438
|
await new Promise((r) => setTimeout(r, delay));
|
|
425
439
|
delay = Math.min(delay * 1.5, 5e3);
|
|
426
440
|
}
|
|
427
|
-
if (!last)
|
|
441
|
+
if (!last) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`waitReady: polled ${assetId} zero times in ${timeoutMs}ms \u2014 this is a bug in the SDK, not a missing asset.`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
428
446
|
throw new Error(`waitReady timeout for ${assetId}`);
|
|
429
447
|
}
|
|
430
448
|
/**
|
|
@@ -572,7 +590,7 @@ var NitidaClient = class {
|
|
|
572
590
|
}
|
|
573
591
|
if (!this.opts.signingKey) {
|
|
574
592
|
throw new Error(
|
|
575
|
-
"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. No signingKey on this client.
|
|
593
|
+
"aq.transform({ sign: true }) requires `signingKey` in NitidaClientOptions. No signingKey on this client. It is returned ONCE, in the response that creates your project (POST /admin/projects \u2192 `signingKey`, next to the three API keys; the console shows it in the same panel). If you never saw one \u2014 projects created before 2026-08-23 were not handed it \u2014 ask the platform operator to rotate: with no signed URLs in flight that invalidates nothing and is free. If you DO have signed URLs circulating, ask for the current key instead, because rotating would kill them. Then pass it to the SDK constructor on a SERVER-side instance only."
|
|
576
594
|
);
|
|
577
595
|
}
|
|
578
596
|
return getSignedTransformUrl(asset, opts, this.opts.signingKey) ?? Promise.resolve(this.urlFor(asset, "lg"));
|
|
@@ -581,7 +599,7 @@ var NitidaClient = class {
|
|
|
581
599
|
if (!signOpts?.sign) return getTransformSrcSet(asset, widths, extraOpts);
|
|
582
600
|
if (!this.opts.signingKey) {
|
|
583
601
|
throw new Error(
|
|
584
|
-
"aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions."
|
|
602
|
+
"aq.transformSrcSet({ sign: true }) requires `signingKey` in NitidaClientOptions. It was returned ONCE, by the response that created your project (POST /admin/projects \u2192 `signingKey`). See the `signingKey` docs on NitidaClientOptions."
|
|
585
603
|
);
|
|
586
604
|
}
|
|
587
605
|
const key = this.opts.signingKey;
|
|
@@ -860,6 +878,7 @@ export {
|
|
|
860
878
|
PRESET_LONG,
|
|
861
879
|
PRESET_MAX_DIM,
|
|
862
880
|
PRESET_SHORT,
|
|
881
|
+
REQUESTABLE_PRESETS2 as REQUESTABLE_PRESETS,
|
|
863
882
|
TRANSFORM_WIDTHS,
|
|
864
883
|
accessMessage,
|
|
865
884
|
assertPublic,
|
|
@@ -890,6 +909,7 @@ export {
|
|
|
890
909
|
hasPreset2 as hasPreset,
|
|
891
910
|
hlsLadderAlignment,
|
|
892
911
|
invalidateSlotCache2 as invalidateSlotCache,
|
|
912
|
+
isRequestablePreset2 as isRequestablePreset,
|
|
893
913
|
isUniversallyPlayableAudio,
|
|
894
914
|
iteratePaletteSwatches,
|
|
895
915
|
mimeFromFileName,
|
|
@@ -901,6 +921,7 @@ export {
|
|
|
901
921
|
setCdnBase2 as setCdnBase,
|
|
902
922
|
setTenantId2 as setTenantId,
|
|
903
923
|
signAccessUrl,
|
|
904
|
-
signTransformUrl
|
|
924
|
+
signTransformUrl,
|
|
925
|
+
toRequestablePresets2 as toRequestablePresets
|
|
905
926
|
};
|
|
906
927
|
//# sourceMappingURL=index.js.map
|