@nitida/sdk 0.28.0 → 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 +1 -1
- package/LICENSE +21 -0
- package/README.md +16 -7
- 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 +26 -2
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/server.js +12 -2
- package/dist/server.js.map +1 -1
- package/dist/web.d.ts +1 -1
- package/dist/web.js +12 -2
- package/dist/web.js.map +1 -1
- package/package.json +7 -5
- package/skills/nitida-sdk/SKILL.md +3 -2
- package/src/expo/index.ts +5 -5
- package/src/index.ts +55 -5
- package/src/web/index.ts +3 -3
package/AGENTS.md
CHANGED
|
@@ -167,4 +167,4 @@ degrades to a wrong choice.
|
|
|
167
167
|
|---|---|
|
|
168
168
|
| `@nitida/asset-client` | URL builders + preset tables. No network, no key. |
|
|
169
169
|
| `@nitida/asset-compressor-web` | Browser image compression (worker → OffscreenCanvas → WebP). |
|
|
170
|
-
| `@
|
|
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
|
|
|
@@ -434,7 +437,7 @@ export function UploadHeroScreen() {
|
|
|
434
437
|
}
|
|
435
438
|
```
|
|
436
439
|
|
|
437
|
-
⚠️ The Expo background-upload path depends on `@
|
|
440
|
+
⚠️ The Expo background-upload path depends on `@nitida/asset-uploader-expo`,
|
|
438
441
|
which is **not on npm**. `aq.upload()` works on Expo today without it; the
|
|
439
442
|
background/resumable variant is not something an external consumer can install
|
|
440
443
|
yet.
|
|
@@ -1383,4 +1386,10 @@ or a cache hit runs no encoder and books nothing, so `0` means zero.
|
|
|
1383
1386
|
|
|
1384
1387
|
## License
|
|
1385
1388
|
|
|
1386
|
-
|
|
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
|
@@ -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).
|
|
@@ -479,7 +479,31 @@ declare class AssetsApi {
|
|
|
479
479
|
* Default timeout is 5 minutes; videos / HLS ladders may need a
|
|
480
480
|
* higher cap (pass `10 * 60_000` for compositions, transcodes).
|
|
481
481
|
*/
|
|
482
|
-
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>;
|
|
483
507
|
/**
|
|
484
508
|
* Dispatch `POST /assets/compose-marketing` to stitch pre-uploaded clip
|
|
485
509
|
* segments into a single MP4 composition. Returns the processing asset
|
package/dist/index.js
CHANGED
|
@@ -422,7 +422,13 @@ var AssetsApi = class {
|
|
|
422
422
|
* Default timeout is 5 minutes; videos / HLS ladders may need a
|
|
423
423
|
* higher cap (pass `10 * 60_000` for compositions, transcodes).
|
|
424
424
|
*/
|
|
425
|
-
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
|
+
}
|
|
426
432
|
const start = Date.now();
|
|
427
433
|
let delay = 500;
|
|
428
434
|
let last = null;
|
|
@@ -432,7 +438,11 @@ var AssetsApi = class {
|
|
|
432
438
|
await new Promise((r) => setTimeout(r, delay));
|
|
433
439
|
delay = Math.min(delay * 1.5, 5e3);
|
|
434
440
|
}
|
|
435
|
-
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
|
+
}
|
|
436
446
|
throw new Error(`waitReady timeout for ${assetId}`);
|
|
437
447
|
}
|
|
438
448
|
/**
|