@nodaro/sdk 2.55.0 → 2.56.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/dist/{chunk-HRV3VX22.cjs → chunk-2LOTDOUV.cjs} +5 -5
- package/dist/{chunk-HRV3VX22.cjs.map → chunk-2LOTDOUV.cjs.map} +1 -1
- package/dist/{chunk-CV2ZZAUO.js → chunk-37LL6BPM.js} +4 -4
- package/dist/{chunk-CV2ZZAUO.js.map → chunk-37LL6BPM.js.map} +1 -1
- package/dist/{chunk-XALPYUZI.cjs → chunk-3CVWTUK5.cjs} +4 -4
- package/dist/{chunk-XALPYUZI.cjs.map → chunk-3CVWTUK5.cjs.map} +1 -1
- package/dist/{chunk-7CUWAVWB.js → chunk-4FSWJTRS.js} +3 -3
- package/dist/{chunk-7CUWAVWB.js.map → chunk-4FSWJTRS.js.map} +1 -1
- package/dist/{chunk-DV6GTEX5.js → chunk-4SZUSZXK.js} +2 -2
- package/dist/{chunk-DV6GTEX5.js.map → chunk-4SZUSZXK.js.map} +1 -1
- package/dist/{chunk-L6T44A3G.js → chunk-4VP2GFIZ.js} +20 -5
- package/dist/chunk-4VP2GFIZ.js.map +1 -0
- package/dist/{chunk-NKH2UFSD.cjs → chunk-4WOT5OAY.cjs} +5 -5
- package/dist/{chunk-NKH2UFSD.cjs.map → chunk-4WOT5OAY.cjs.map} +1 -1
- package/dist/{chunk-AUKHMT53.js → chunk-BGOZRZHY.js} +3 -3
- package/dist/{chunk-AUKHMT53.js.map → chunk-BGOZRZHY.js.map} +1 -1
- package/dist/{chunk-OSMKIRKI.cjs → chunk-DFT4LD2H.cjs} +4 -4
- package/dist/{chunk-OSMKIRKI.cjs.map → chunk-DFT4LD2H.cjs.map} +1 -1
- package/dist/{chunk-RBX3QTKB.js → chunk-E6X6BGRO.js} +3 -3
- package/dist/{chunk-RBX3QTKB.js.map → chunk-E6X6BGRO.js.map} +1 -1
- package/dist/{chunk-47ULSVO2.js → chunk-FTCAT2LK.js} +3 -3
- package/dist/chunk-FTCAT2LK.js.map +1 -0
- package/dist/{chunk-65VIIZEF.cjs → chunk-GJH73EGF.cjs} +2 -2
- package/dist/{chunk-65VIIZEF.cjs.map → chunk-GJH73EGF.cjs.map} +1 -1
- package/dist/{chunk-D2ILCSOQ.js → chunk-HK6XP5EN.js} +3 -3
- package/dist/{chunk-D2ILCSOQ.js.map → chunk-HK6XP5EN.js.map} +1 -1
- package/dist/{chunk-SF2CMKWA.cjs → chunk-OMJ45CW4.cjs} +9 -9
- package/dist/{chunk-SF2CMKWA.cjs.map → chunk-OMJ45CW4.cjs.map} +1 -1
- package/dist/{chunk-LUHAB7UJ.js → chunk-PQ6G5EM7.js} +9 -9
- package/dist/{chunk-LUHAB7UJ.js.map → chunk-PQ6G5EM7.js.map} +1 -1
- package/dist/{chunk-FRQJNGJS.cjs → chunk-Q4MFQC2J.cjs} +5 -5
- package/dist/{chunk-FRQJNGJS.cjs.map → chunk-Q4MFQC2J.cjs.map} +1 -1
- package/dist/{chunk-VHTDOACT.cjs → chunk-Q7W4QNE5.cjs} +22 -22
- package/dist/chunk-Q7W4QNE5.cjs.map +1 -0
- package/dist/{chunk-I6Q4UDS2.cjs → chunk-RIMDKJSQ.cjs} +16 -16
- package/dist/{chunk-I6Q4UDS2.cjs.map → chunk-RIMDKJSQ.cjs.map} +1 -1
- package/dist/{chunk-24JE54FQ.js → chunk-VEQLP5TA.js} +3 -3
- package/dist/{chunk-24JE54FQ.js.map → chunk-VEQLP5TA.js.map} +1 -1
- package/dist/{chunk-FM2KM5TY.cjs → chunk-Z4B4SZ33.cjs} +21 -4
- package/dist/chunk-Z4B4SZ33.cjs.map +1 -0
- package/dist/entries/cast-swap.cjs +2 -2
- package/dist/entries/cast-swap.d.cts +12 -0
- package/dist/entries/cast-swap.d.ts +12 -0
- package/dist/entries/cast-swap.js +1 -1
- package/dist/entries/characters.d.cts +1 -1
- package/dist/entries/characters.d.ts +1 -1
- package/dist/entries/client.cjs +15 -15
- package/dist/entries/client.d.cts +1 -1
- package/dist/entries/client.d.ts +1 -1
- package/dist/entries/client.js +10 -10
- package/dist/entries/copilot.cjs +4 -4
- package/dist/entries/copilot.js +3 -3
- package/dist/entries/core.cjs +6 -6
- package/dist/entries/core.js +2 -2
- package/dist/entries/creatures.d.cts +1 -1
- package/dist/entries/creatures.d.ts +1 -1
- package/dist/entries/edit.cjs +7 -7
- package/dist/entries/edit.js +2 -2
- package/dist/entries/errors.cjs +32 -24
- package/dist/entries/errors.d.cts +26 -2
- package/dist/entries/errors.d.ts +26 -2
- package/dist/entries/errors.js +1 -1
- package/dist/entries/locations.d.cts +1 -1
- package/dist/entries/locations.d.ts +1 -1
- package/dist/entries/media.cjs +4 -4
- package/dist/entries/media.js +3 -3
- package/dist/entries/nodes.cjs +3 -3
- package/dist/entries/nodes.d.cts +1 -1
- package/dist/entries/nodes.d.ts +1 -1
- package/dist/entries/nodes.js +2 -2
- package/dist/entries/objects.d.cts +1 -1
- package/dist/entries/objects.d.ts +1 -1
- package/dist/entries/scene3d.d.cts +2 -2
- package/dist/entries/scene3d.d.ts +2 -2
- package/dist/entries/studio.cjs +5 -5
- package/dist/entries/studio.js +2 -2
- package/dist/index.cjs +59 -51
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -10
- package/dist/{nodes-B74WaY9r.d.ts → nodes-1u7MPrD5.d.ts} +10 -0
- package/dist/{nodes-Ceetyyh0.d.cts → nodes-BZavad1T.d.cts} +10 -0
- package/package.json +1 -1
- package/dist/chunk-47ULSVO2.js.map +0 -1
- package/dist/chunk-FM2KM5TY.cjs.map +0 -1
- package/dist/chunk-L6T44A3G.js.map +0 -1
- package/dist/chunk-VHTDOACT.cjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkZ4B4SZ33_cjs = require('./chunk-Z4B4SZ33.cjs');
|
|
4
4
|
var organizations = require('@nodaro/shared/organizations');
|
|
5
5
|
|
|
6
6
|
// src/binary-response.ts
|
|
@@ -36,7 +36,7 @@ async function readBinaryResponse(response, maxBytes) {
|
|
|
36
36
|
}
|
|
37
37
|
return bytes.buffer;
|
|
38
38
|
}
|
|
39
|
-
var SDK_VERSION = "2.
|
|
39
|
+
var SDK_VERSION = "2.56.0" ;
|
|
40
40
|
var CLIENT_HEADER = "X-Nodaro-Client";
|
|
41
41
|
var isBrowser = () => typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
42
42
|
var NodaroClientCore = class {
|
|
@@ -107,7 +107,7 @@ var NodaroClientCore = class {
|
|
|
107
107
|
errBody = await res.json();
|
|
108
108
|
} catch {
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
chunkZ4B4SZ33_cjs.throwFromResponse(res.status, errBody);
|
|
111
111
|
}
|
|
112
112
|
return await read(res);
|
|
113
113
|
} finally {
|
|
@@ -229,5 +229,5 @@ exports.CallbackAuth = CallbackAuth;
|
|
|
229
229
|
exports.NodaroClientCore = NodaroClientCore;
|
|
230
230
|
exports.StaticTokenAuth = StaticTokenAuth;
|
|
231
231
|
exports.supabaseAuth = supabaseAuth;
|
|
232
|
-
//# sourceMappingURL=chunk-
|
|
233
|
-
//# sourceMappingURL=chunk-
|
|
232
|
+
//# sourceMappingURL=chunk-2LOTDOUV.cjs.map
|
|
233
|
+
//# sourceMappingURL=chunk-2LOTDOUV.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/binary-response.ts","../src/core.ts","../src/auth.ts"],"names":["WORKSPACE_HEADER","throwFromResponse"],"mappings":"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","file":"chunk-HRV3VX22.cjs","sourcesContent":["/** Bound decoded response bytes even when Content-Length is absent or compressed. */\nexport async function readBinaryResponse(response: Response, maxBytes: number): Promise<ArrayBuffer> {\n const advertised = response.headers.get(\"content-length\")\n if (advertised !== null && (!/^\\d+$/.test(advertised) || Number(advertised) > maxBytes)) {\n await response.body?.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n if (!response.body) return new ArrayBuffer(0)\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let length = 0\n try {\n while (true) {\n const next = await reader.read()\n if (next.done) break\n length += next.value.byteLength\n if (length > maxBytes) {\n await reader.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n chunks.push(next.value)\n }\n } finally { reader.releaseLock() }\n const bytes = new Uint8Array(length)\n let offset = 0\n for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }\n return bytes.buffer\n}\n","/**\n * The client core: configuration, auth, the request transport and the\n * account-identity calls — everything a resource needs, and none of the\n * resources themselves.\n *\n * {@link NodaroClient} (`createClient`) extends it with every resource. An app\n * that uses only a few resources can extend it with just those, so it loads\n * (and its tests depend on) only the resource modules it actually uses:\n *\n * class AppClient extends NodaroClientCore {\n * readonly jobs = new JobsResource(this)\n * readonly nodes = new NodesResource(this) // needs `jobs` on the same client\n * }\n *\n * A resource that calls another one through the client says so in its\n * constructor type (`NodesResource` takes a core that also has `jobs`), so a\n * client missing it fails to compile rather than at run time.\n */\nimport { readBinaryResponse } from \"./binary-response.js\"\nimport { throwFromResponse } from \"./errors.js\"\nimport type { Auth } from \"./auth.js\"\nimport type { DeveloperAppKind, DeveloperAppScope } from \"./resources/developer-apps.js\"\nimport { WORKSPACE_HEADER, type MeOrganizations } from \"@nodaro/shared/organizations\"\n\n/** Replaced at build time by tsup `define` from package.json. The fallback\n * keeps `tsx`/vitest runs of the source working, where no define applies. */\ndeclare const __SDK_VERSION__: string | undefined\nconst SDK_VERSION = typeof __SDK_VERSION__ === \"string\" ? __SDK_VERSION__ : \"0.0.0-dev\"\n\n/**\n * Identifies the calling package to the backend, which records it as the job's\n * provenance (`jobs.source` / `source_detail`) so an operator can tell a CLI\n * run from an SDK integration from a browser session.\n *\n * Only `sdk/…` and `cli/…` are honoured server-side; anything else is ignored\n * rather than trusted, since this is an unauthenticated header.\n */\nexport const CLIENT_HEADER = \"X-Nodaro-Client\"\n\n/**\n * In a browser the header is redundant AND a liability, so it is not sent.\n *\n * Redundant: the browser already sends `Origin`, which names the actual product\n * (`studio.nodaro.ai`) rather than merely the library it used — and the backend\n * deliberately prefers Origin over this header, so a browser-sent value is\n * discarded on arrival.\n *\n * A liability: `X-Nodaro-Client` is not a CORS-safelisted request header, so a\n * browser sending it requires an exact match in the server's\n * `Access-Control-Allow-Headers`. Any deployment whose backend predates that\n * allowlist entry would fail the PREFLIGHT — breaking every API call from the\n * app, not just its provenance. Self-hosted and lagging deployments make that a\n * real risk, and nothing in a browser app would reveal the cause.\n *\n * An EXPLICIT `clientLabel` is always sent regardless: a caller that names\n * itself has opted in deliberately, and that intent outranks this default.\n */\nconst isBrowser = (): boolean => typeof window !== \"undefined\" && typeof window.document !== \"undefined\"\n\nexport interface ClientOptions {\n /** Backend base URL, e.g. \"https://nodaro.example.com\" or empty string for same-origin. */\n baseUrl: string\n /** Auth provider. Use StaticTokenAuth, supabaseAuth, or CallbackAuth. */\n auth: Auth\n /** Optional fetch override (for tests or custom transports). */\n fetch?: typeof fetch\n /** Default request timeout in ms. Default 60s. */\n timeoutMs?: number\n /**\n * Overrides the `X-Nodaro-Client` label. Defaults to `sdk/<version>`.\n *\n * Exists for `@nodaro/cli`, which is a wrapper AROUND this SDK: without an\n * override every CLI invocation would be recorded as an SDK call and the two\n * surfaces could never be told apart.\n */\n clientLabel?: string\n /**\n * The workspace every request acts in — sent as `X-Nodaro-Workspace`.\n *\n * It selects which workspace a LIST reads from and where a CREATE lands.\n * It never authorizes: reading, updating, deleting or running an\n * identified object is decided by that object's own workspace, so a\n * forgotten header cannot hide work and a forged one cannot reach anyone\n * else's. Omit it to work in the caller's personal space.\n *\n * Prefer {@link NodaroClient.withWorkspace} for anything short-lived — one\n * client per workspace, no shared mutable selection to get wrong.\n */\n workspaceId?: string\n}\n\ninterface RequestOptions {\n body?: unknown\n query?: Record<string, string | number | boolean | undefined>\n headers?: Record<string, string>\n signal?: AbortSignal\n /** This request's timeout in ms, in place of the client's `timeoutMs`. */\n timeoutMs?: number\n}\n\n/**\n * The authenticated user's canonical identity (`GET /v1/me`). A token-\n * introspection primitive: any valid bearer token (first-party Supabase JWT or\n * a developer-app OAuth token) resolves to its owner's identity. Mirrors the\n * `profiles` identity columns server-side — the route is the source of truth.\n */\nexport interface UserIdentity {\n /** Nodaro user id (= the Supabase auth user id). */\n readonly id: string\n readonly email: string\n /** Human-readable display name (from `profiles.full_name`); `null` if unset. */\n readonly displayName: string | null\n /** Avatar URL; `null` if unset. */\n readonly avatarUrl: string | null\n /** Subscription tier (e.g. \"free\", \"pro\"). */\n readonly tier: string\n /**\n * Whether the user holds an admin role. DESCRIPTIVE only — use it to decide\n * whether to render admin surfaces without capability-probing an admin\n * endpoint; every admin API stays enforced server-side regardless.\n */\n readonly isAdmin: boolean\n}\n\n/**\n * An app the user let into their account through OAuth (`GET\n * /v1/me/connected-apps`): a developer app, an AI assistant's MCP connector, or\n * a self-hosted install. One entry per grant still in force.\n */\nexport interface ConnectedApp {\n /** The grant's id — what {@link NodaroClient.revokeConnectedApp} takes. */\n readonly authorizationId: string\n /** The app's name; `null` if it has none. A `dynamic_mcp` client chose its own. */\n readonly name: string | null\n /**\n * How the app came to exist: `user` (a developer app someone registered),\n * `first_party_mcp`, `dynamic_mcp` (an MCP client that registered itself) or\n * `community_instance` (a self-hosted install).\n */\n readonly kind: DeveloperAppKind | (string & {})\n readonly homepageUrl: string | null\n /** The scopes this grant holds. */\n readonly scopes: readonly DeveloperAppScope[]\n /** When the user granted access (ISO 8601). */\n readonly connectedAt: string\n /** The latest use of any token issued under the grant; `null` if never used. */\n readonly lastUsedAt: string | null\n}\n\nexport class NodaroClientCore {\n readonly baseUrl: string\n readonly auth: Auth\n readonly timeoutMs: number\n /** Value sent as `X-Nodaro-Client`; recorded by the backend as job provenance. */\n readonly clientLabel: string\n /** Whether to actually send it — see the note on {@link CLIENT_HEADER}. */\n private readonly sendClientHeader: boolean\n private readonly fetchOverride: typeof fetch | undefined\n\n /**\n * Resolved lazily so consumers can swap `globalThis.fetch` after the\n * client has been constructed (e.g. test mocks). Always rebound to the\n * global object — native fetch throws \"Illegal invocation\" when its\n * `this` is anything else.\n */\n get fetch(): typeof fetch {\n return this.fetchOverride ?? globalThis.fetch.bind(globalThis)\n }\n\n /** The workspace this client acts in; undefined = the personal space. */\n readonly workspaceId: string | undefined\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/$/, \"\") // strip trailing slash\n this.auth = opts.auth\n this.fetchOverride = opts.fetch\n this.timeoutMs = opts.timeoutMs ?? 60_000\n this.clientLabel = opts.clientLabel ?? `sdk/${SDK_VERSION}`\n this.workspaceId = opts.workspaceId\n // Explicit label = deliberate opt-in, always sent. The default is suppressed\n // in browsers — see the note on CLIENT_HEADER.\n this.sendClientHeader = opts.clientLabel !== undefined || !isBrowser()\n }\n\n /**\n * The transport: build the request, send it, throw the typed error on a\n * non-2xx via {@link throwFromResponse}, then read the body via `read` — all\n * inside the abort timer, so `timeoutMs` bounds the body download too (a\n * stalled CSV stream aborts, it does not hang forever). Both the JSON path\n * ({@link request}) and the text path ({@link requestText}) go through here.\n */\n private async send<T>(\n method: string,\n path: string,\n options: RequestOptions,\n read: (res: Response) => Promise<T>,\n ): Promise<T> {\n const url = this.buildUrl(path, options.query)\n\n const token = await this.auth.getToken()\n // A `FormData` body is a multipart upload: let the runtime set\n // `Content-Type: multipart/form-data; boundary=…` itself (a manual JSON\n // content-type corrupts the boundary), and send the body as-is rather than\n // JSON-stringifying it. Every other body stays JSON, exactly as before.\n const isFormData =\n typeof FormData !== \"undefined\" && options.body instanceof FormData\n const headers: Record<string, string> = {\n ...(isFormData ? {} : { \"Content-Type\": \"application/json\" }),\n ...(this.sendClientHeader ? { [CLIENT_HEADER]: this.clientLabel } : {}),\n // Before per-request headers: a resource that needs to reach outside\n // this client's workspace says so explicitly and wins.\n ...(this.workspaceId ? { [WORKSPACE_HEADER]: this.workspaceId } : {}),\n ...(options.headers ?? {}),\n }\n if (token) headers[\"Authorization\"] = `Bearer ${token}`\n\n const ac = new AbortController()\n const timeoutId = setTimeout(() => ac.abort(), options.timeoutMs ?? this.timeoutMs)\n const abort = () => ac.abort(options.signal?.reason)\n options.signal?.addEventListener(\"abort\", abort, { once: true })\n if (options.signal?.aborted) abort()\n\n try {\n ac.signal.throwIfAborted()\n const res = await this.fetch(url, {\n method,\n headers,\n body:\n options.body === undefined\n ? undefined\n : isFormData\n ? (options.body as FormData)\n : JSON.stringify(options.body),\n signal: ac.signal,\n })\n\n if (!res.ok) {\n let errBody: Record<string, unknown> = {}\n try {\n errBody = await res.json() as Record<string, unknown>\n } catch {\n // Empty/non-JSON body — fall through with empty errBody\n }\n throwFromResponse(res.status, errBody)\n }\n\n return await read(res)\n } finally {\n clearTimeout(timeoutId)\n options.signal?.removeEventListener(\"abort\", abort)\n }\n }\n\n /** Authenticated, bounded binary reads through the normal timeout/error transport. */\n async requestBytes(method: string, path: string, options: RequestOptions & { maxBytes: number }): Promise<ArrayBuffer> {\n if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1) throw new Error(\"maxBytes must be a positive integer\")\n return this.send(method, path, options, (response) => readBinaryResponse(response, options.maxBytes))\n }\n\n async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {\n return this.send(method, path, options, async (res) =>\n // 204 No Content\n res.status === 204 ? (undefined as T) : ((await res.json()) as T),\n )\n }\n\n /**\n * Like {@link request}, for endpoints that answer text (CSV exports). Errors\n * are still the JSON envelope and throw the same typed errors, and the read is\n * bounded by the same timeout as the request.\n */\n async requestText(method: string, path: string, options: RequestOptions = {}): Promise<string> {\n return this.send(method, path, options, (res) => res.text())\n }\n\n /**\n * `GET /v1/me` → the authenticated user's identity (see {@link UserIdentity}).\n * Unwraps the `{ data }` envelope. Throws `UnauthorizedError` (401) when the\n * token is missing/invalid, and the SDK's other typed errors as usual.\n *\n * On an instance with organizations it also carries what the caller belongs\n * to. THREE states, and collapsing them is wrong in a way users feel: the\n * organization fields ABSENT means this instance has no organizations at\n * all; present and empty means the account belongs to none; and\n * `organizationsUnavailable` means the lookup failed — keep whatever\n * selection you already had rather than concluding the person was removed\n * from everything.\n */\n async me(): Promise<UserIdentity & MeOrganizations> {\n const res = await this.request<{ data: UserIdentity & MeOrganizations }>(\"GET\", \"/v1/me\")\n return res.data\n }\n\n /**\n * `GET /v1/me/connected-apps` → every app with access to the signed-in\n * user's account (see {@link ConnectedApp}), newest grant first.\n *\n * **A signed-in browser session only.** The route answers a Supabase\n * session token (`supabaseAuth(…)`, `createSharedSupabaseClient`, or a\n * `StaticTokenAuth` holding one) and refuses every other credential with\n * `UnauthorizedError` (\"Sign in required\"): an app token or an API key must\n * not list the grants it lives under. Build your \"Connected apps\" settings\n * screen on this; a server-side integration cannot call it.\n */\n async connectedApps(): Promise<ConnectedApp[]> {\n const res = await this.request<{ apps: ConnectedApp[] }>(\"GET\", \"/v1/me/connected-apps\")\n return res.apps\n }\n\n /**\n * `POST /v1/me/connected-apps/:authorizationId/revoke` — ends an app's\n * access: the grant and every token issued under it, at once (the app's next\n * call is a 401). Resolves when done.\n *\n * A signed-in browser session only, like {@link connectedApps}. Throws\n * `NotFoundError` when the grant is not the user's or is already revoked.\n */\n async revokeConnectedApp(authorizationId: string): Promise<void> {\n await this.request(\"POST\", `/v1/me/connected-apps/${encodeURIComponent(authorizationId)}/revoke`)\n }\n\n private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>): string {\n const base = this.baseUrl || (typeof window !== \"undefined\" ? window.location.origin : \"http://placeholder\")\n const url = new URL(path, base)\n // If baseUrl was empty, strip the placeholder origin\n const fullUrl = this.baseUrl ? url.toString() : url.pathname + url.search\n if (query) {\n const u = new URL(this.baseUrl ? fullUrl : fullUrl, base)\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined) u.searchParams.set(k, String(v))\n }\n return this.baseUrl ? u.toString() : u.pathname + u.search\n }\n return fullUrl\n }\n}\n","/**\n * Auth provides the token used for `Authorization: Bearer <token>` on each request.\n * Implementations:\n * - StaticTokenAuth — fixed string (server-side, OAuth access token, API token)\n * - CallbackAuth — caller-supplied async function (BYO logic)\n * - supabaseAuth — pulls JWT from a Supabase client live (browser frontends)\n */\nexport interface Auth {\n /** Returns the current Bearer token, or null if not authenticated. */\n getToken(): Promise<string | null>\n}\n\nexport class StaticTokenAuth implements Auth {\n constructor(private token: string) {}\n async getToken() {\n return this.token\n }\n}\n\nexport class CallbackAuth implements Auth {\n constructor(private fn: () => string | null | Promise<string | null>) {}\n async getToken() {\n return this.fn()\n }\n}\n\ninterface SupabaseLikeClient {\n auth: {\n getSession(): Promise<{ data: { session: { access_token: string } | null } }>\n }\n}\n\n/** Pulls a JWT from a Supabase v2 client. Caller supplies their own supabase. */\nexport function supabaseAuth(supabase: SupabaseLikeClient): Auth {\n return {\n async getToken() {\n const { data } = await supabase.auth.getSession()\n return data.session?.access_token ?? null\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/binary-response.ts","../src/core.ts","../src/auth.ts"],"names":["WORKSPACE_HEADER","throwFromResponse"],"mappings":";;;;;;AACA,eAAsB,kBAAA,CAAmB,UAAoB,QAAA,EAAwC;AACnG,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAA;AACxD,EAAA,IAAI,UAAA,KAAe,IAAA,KAAS,CAAC,OAAA,CAAQ,IAAA,CAAK,UAAU,CAAA,IAAK,MAAA,CAAO,UAAU,CAAA,GAAI,QAAA,CAAA,EAAW;AACvF,IAAA,MAAM,QAAA,CAAS,MAAM,MAAA,EAAO;AAC5B,IAAA,MAAM,IAAI,MAAM,iDAAiD,CAAA;AAAA,EACnE;AACA,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,EAAM,OAAO,IAAI,YAAY,CAAC,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,CAAK,SAAA,EAAU;AACvC,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,EAAM;AACX,MAAA,MAAM,IAAA,GAAO,MAAM,MAAA,CAAO,IAAA,EAAK;AAC/B,MAAA,IAAI,KAAK,IAAA,EAAM;AACf,MAAA,MAAA,IAAU,KAAK,KAAA,CAAM,UAAA;AACrB,MAAA,IAAI,SAAS,QAAA,EAAU;AACrB,QAAA,MAAM,OAAO,MAAA,EAAO;AACpB,QAAA,MAAM,IAAI,MAAM,iDAAiD,CAAA;AAAA,MACnE;AACA,MAAA,MAAA,CAAO,IAAA,CAAK,KAAK,KAAK,CAAA;AAAA,IACxB;AAAA,EACF,CAAA,SAAE;AAAU,IAAA,MAAA,CAAO,WAAA,EAAY;AAAA,EAAE;AACjC,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAM,CAAA;AACnC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAAE,IAAA,KAAA,CAAM,GAAA,CAAI,OAAO,MAAM,CAAA;AAAG,IAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,EAAW;AACnF,EAAA,OAAO,KAAA,CAAM,MAAA;AACf;ACAA,IAAM,WAAA,GAAoD,QAAA,CAAkB;AAUrE,IAAM,aAAA,GAAgB,iBAAA;AAoB7B,IAAM,YAAY,MAAe,OAAO,WAAW,WAAA,IAAe,OAAO,OAAO,QAAA,KAAa,WAAA;AA4FtF,IAAM,mBAAN,MAAuB;AAAA,EACnB,OAAA;AAAA,EACA,IAAA;AAAA,EACA,SAAA;AAAA;AAAA,EAEA,WAAA;AAAA;AAAA,EAEQ,gBAAA;AAAA,EACA,aAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,IAAI,KAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,aAAA,IAAiB,UAAA,CAAW,KAAA,CAAM,KAAK,UAAU,CAAA;AAAA,EAC/D;AAAA;AAAA,EAGS,WAAA;AAAA,EAET,YAAY,IAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC7C,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,KAAA;AAC1B,IAAA,IAAA,CAAK,SAAA,GAAY,KAAK,SAAA,IAAa,GAAA;AACnC,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA,CAAK,WAAA,IAAe,CAAA,IAAA,EAAO,WAAW,CAAA,CAAA;AACzD,IAAA,IAAA,CAAK,cAAc,IAAA,CAAK,WAAA;AAGxB,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA,CAAK,WAAA,KAAgB,MAAA,IAAa,CAAC,SAAA,EAAU;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,IAAA,CACZ,MAAA,EACA,IAAA,EACA,SACA,IAAA,EACY;AACZ,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,QAAQ,KAAK,CAAA;AAE7C,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,IAAA,CAAK,QAAA,EAAS;AAKvC,IAAA,MAAM,UAAA,GACJ,OAAO,QAAA,KAAa,WAAA,IAAe,QAAQ,IAAA,YAAgB,QAAA;AAC7D,IAAA,MAAM,OAAA,GAAkC;AAAA,MACtC,GAAI,UAAA,GAAa,EAAC,GAAI,EAAE,gBAAgB,kBAAA,EAAmB;AAAA,MAC3D,GAAI,IAAA,CAAK,gBAAA,GAAmB,EAAE,CAAC,aAAa,GAAG,IAAA,CAAK,WAAA,EAAY,GAAI,EAAC;AAAA;AAAA;AAAA,MAGrE,GAAI,IAAA,CAAK,WAAA,GAAc,EAAE,CAACA,8BAAgB,GAAG,IAAA,CAAK,WAAA,EAAY,GAAI,EAAC;AAAA,MACnE,GAAI,OAAA,CAAQ,OAAA,IAAW;AAAC,KAC1B;AACA,IAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,eAAe,CAAA,GAAI,UAAU,KAAK,CAAA,CAAA;AAErD,IAAA,MAAM,EAAA,GAAK,IAAI,eAAA,EAAgB;AAC/B,IAAA,MAAM,SAAA,GAAY,WAAW,MAAM,EAAA,CAAG,OAAM,EAAG,OAAA,CAAQ,SAAA,IAAa,IAAA,CAAK,SAAS,CAAA;AAClF,IAAA,MAAM,QAAQ,MAAM,EAAA,CAAG,KAAA,CAAM,OAAA,CAAQ,QAAQ,MAAM,CAAA;AACnD,IAAA,OAAA,CAAQ,QAAQ,gBAAA,CAAiB,OAAA,EAAS,OAAO,EAAE,IAAA,EAAM,MAAM,CAAA;AAC/D,IAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAA,EAAS,KAAA,EAAM;AAEnC,IAAA,IAAI;AACF,MAAA,EAAA,CAAG,OAAO,cAAA,EAAe;AACzB,MAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA;AAAA,QACA,OAAA;AAAA,QACA,IAAA,EACE,OAAA,CAAQ,IAAA,KAAS,KAAA,CAAA,GACb,KAAA,CAAA,GACA,UAAA,GACG,OAAA,CAAQ,IAAA,GACT,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAI,CAAA;AAAA,QACnC,QAAQ,EAAA,CAAG;AAAA,OACZ,CAAA;AAED,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,IAAI,UAAmC,EAAC;AACxC,QAAA,IAAI;AACF,UAAA,OAAA,GAAU,MAAM,IAAI,IAAA,EAAK;AAAA,QAC3B,CAAA,CAAA,MAAQ;AAAA,QAER;AACA,QAAAC,mCAAA,CAAkB,GAAA,CAAI,QAAQ,OAAO,CAAA;AAAA,MACvC;AAEA,MAAA,OAAO,MAAM,KAAK,GAAG,CAAA;AAAA,IACvB,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,SAAS,CAAA;AACtB,MAAA,OAAA,CAAQ,MAAA,EAAQ,mBAAA,CAAoB,OAAA,EAAS,KAAK,CAAA;AAAA,IACpD;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,YAAA,CAAa,MAAA,EAAgB,IAAA,EAAc,OAAA,EAAsE;AACrH,IAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,OAAA,CAAQ,QAAQ,CAAA,IAAK,OAAA,CAAQ,QAAA,GAAW,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,qCAAqC,CAAA;AAC1H,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,CAAC,QAAA,KAAa,kBAAA,CAAmB,QAAA,EAAU,OAAA,CAAQ,QAAQ,CAAC,CAAA;AAAA,EACtG;AAAA,EAEA,MAAM,OAAA,CAAW,MAAA,EAAgB,IAAA,EAAc,OAAA,GAA0B,EAAC,EAAe;AACvF,IAAA,OAAO,IAAA,CAAK,IAAA;AAAA,MAAK,MAAA;AAAA,MAAQ,IAAA;AAAA,MAAM,OAAA;AAAA,MAAS,OAAO,GAAA;AAAA;AAAA,QAE7C,IAAI,MAAA,KAAW,GAAA,GAAO,MAAA,GAAoB,MAAM,IAAI,IAAA;AAAK;AAAA,KAC3D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAA,CAAY,MAAA,EAAgB,IAAA,EAAc,OAAA,GAA0B,EAAC,EAAoB;AAC7F,IAAA,OAAO,IAAA,CAAK,KAAK,MAAA,EAAQ,IAAA,EAAM,SAAS,CAAC,GAAA,KAAQ,GAAA,CAAI,IAAA,EAAM,CAAA;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,EAAA,GAA8C;AAClD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAkD,OAAO,QAAQ,CAAA;AACxF,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,aAAA,GAAyC;AAC7C,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAkC,OAAO,uBAAuB,CAAA;AACvF,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,mBAAmB,eAAA,EAAwC;AAC/D,IAAA,MAAM,KAAK,OAAA,CAAQ,MAAA,EAAQ,yBAAyB,kBAAA,CAAmB,eAAe,CAAC,CAAA,OAAA,CAAS,CAAA;AAAA,EAClG;AAAA,EAEQ,QAAA,CAAS,MAAc,KAAA,EAAuE;AACpG,IAAA,MAAM,IAAA,GAAO,KAAK,OAAA,KAAY,OAAO,WAAW,WAAA,GAAc,MAAA,CAAO,SAAS,MAAA,GAAS,oBAAA,CAAA;AACvF,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,IAAA,EAAM,IAAI,CAAA;AAE9B,IAAA,MAAM,OAAA,GAAU,KAAK,OAAA,GAAU,GAAA,CAAI,UAAS,GAAI,GAAA,CAAI,WAAW,GAAA,CAAI,MAAA;AACnE,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAM,IAAI,IAAI,GAAA,CAAI,KAAK,OAAA,GAAU,OAAA,GAAU,SAAS,IAAI,CAAA;AACxD,MAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC1C,QAAA,IAAI,CAAA,KAAM,QAAW,CAAA,CAAE,YAAA,CAAa,IAAI,CAAA,EAAG,MAAA,CAAO,CAAC,CAAC,CAAA;AAAA,MACtD;AACA,MAAA,OAAO,KAAK,OAAA,GAAU,CAAA,CAAE,UAAS,GAAI,CAAA,CAAE,WAAW,CAAA,CAAE,MAAA;AAAA,IACtD;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AACF;;;ACnUO,IAAM,kBAAN,MAAsC;AAAA,EAC3C,YAAoB,KAAA,EAAe;AAAf,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAAA,EAAgB;AAAA,EAAhB,KAAA;AAAA,EACpB,MAAM,QAAA,GAAW;AACf,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF;AAEO,IAAM,eAAN,MAAmC;AAAA,EACxC,YAAoB,EAAA,EAAkD;AAAlD,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAAA,EAAmD;AAAA,EAAnD,EAAA;AAAA,EACpB,MAAM,QAAA,GAAW;AACf,IAAA,OAAO,KAAK,EAAA,EAAG;AAAA,EACjB;AACF;AASO,SAAS,aAAa,QAAA,EAAoC;AAC/D,EAAA,OAAO;AAAA,IACL,MAAM,QAAA,GAAW;AACf,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,QAAA,CAAS,KAAK,UAAA,EAAW;AAChD,MAAA,OAAO,IAAA,CAAK,SAAS,YAAA,IAAgB,IAAA;AAAA,IACvC;AAAA,GACF;AACF","file":"chunk-2LOTDOUV.cjs","sourcesContent":["/** Bound decoded response bytes even when Content-Length is absent or compressed. */\nexport async function readBinaryResponse(response: Response, maxBytes: number): Promise<ArrayBuffer> {\n const advertised = response.headers.get(\"content-length\")\n if (advertised !== null && (!/^\\d+$/.test(advertised) || Number(advertised) > maxBytes)) {\n await response.body?.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n if (!response.body) return new ArrayBuffer(0)\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let length = 0\n try {\n while (true) {\n const next = await reader.read()\n if (next.done) break\n length += next.value.byteLength\n if (length > maxBytes) {\n await reader.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n chunks.push(next.value)\n }\n } finally { reader.releaseLock() }\n const bytes = new Uint8Array(length)\n let offset = 0\n for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }\n return bytes.buffer\n}\n","/**\n * The client core: configuration, auth, the request transport and the\n * account-identity calls — everything a resource needs, and none of the\n * resources themselves.\n *\n * {@link NodaroClient} (`createClient`) extends it with every resource. An app\n * that uses only a few resources can extend it with just those, so it loads\n * (and its tests depend on) only the resource modules it actually uses:\n *\n * class AppClient extends NodaroClientCore {\n * readonly jobs = new JobsResource(this)\n * readonly nodes = new NodesResource(this) // needs `jobs` on the same client\n * }\n *\n * A resource that calls another one through the client says so in its\n * constructor type (`NodesResource` takes a core that also has `jobs`), so a\n * client missing it fails to compile rather than at run time.\n */\nimport { readBinaryResponse } from \"./binary-response.js\"\nimport { throwFromResponse } from \"./errors.js\"\nimport type { Auth } from \"./auth.js\"\nimport type { DeveloperAppKind, DeveloperAppScope } from \"./resources/developer-apps.js\"\nimport { WORKSPACE_HEADER, type MeOrganizations } from \"@nodaro/shared/organizations\"\n\n/** Replaced at build time by tsup `define` from package.json. The fallback\n * keeps `tsx`/vitest runs of the source working, where no define applies. */\ndeclare const __SDK_VERSION__: string | undefined\nconst SDK_VERSION = typeof __SDK_VERSION__ === \"string\" ? __SDK_VERSION__ : \"0.0.0-dev\"\n\n/**\n * Identifies the calling package to the backend, which records it as the job's\n * provenance (`jobs.source` / `source_detail`) so an operator can tell a CLI\n * run from an SDK integration from a browser session.\n *\n * Only `sdk/…` and `cli/…` are honoured server-side; anything else is ignored\n * rather than trusted, since this is an unauthenticated header.\n */\nexport const CLIENT_HEADER = \"X-Nodaro-Client\"\n\n/**\n * In a browser the header is redundant AND a liability, so it is not sent.\n *\n * Redundant: the browser already sends `Origin`, which names the actual product\n * (`studio.nodaro.ai`) rather than merely the library it used — and the backend\n * deliberately prefers Origin over this header, so a browser-sent value is\n * discarded on arrival.\n *\n * A liability: `X-Nodaro-Client` is not a CORS-safelisted request header, so a\n * browser sending it requires an exact match in the server's\n * `Access-Control-Allow-Headers`. Any deployment whose backend predates that\n * allowlist entry would fail the PREFLIGHT — breaking every API call from the\n * app, not just its provenance. Self-hosted and lagging deployments make that a\n * real risk, and nothing in a browser app would reveal the cause.\n *\n * An EXPLICIT `clientLabel` is always sent regardless: a caller that names\n * itself has opted in deliberately, and that intent outranks this default.\n */\nconst isBrowser = (): boolean => typeof window !== \"undefined\" && typeof window.document !== \"undefined\"\n\nexport interface ClientOptions {\n /** Backend base URL, e.g. \"https://nodaro.example.com\" or empty string for same-origin. */\n baseUrl: string\n /** Auth provider. Use StaticTokenAuth, supabaseAuth, or CallbackAuth. */\n auth: Auth\n /** Optional fetch override (for tests or custom transports). */\n fetch?: typeof fetch\n /** Default request timeout in ms. Default 60s. */\n timeoutMs?: number\n /**\n * Overrides the `X-Nodaro-Client` label. Defaults to `sdk/<version>`.\n *\n * Exists for `@nodaro/cli`, which is a wrapper AROUND this SDK: without an\n * override every CLI invocation would be recorded as an SDK call and the two\n * surfaces could never be told apart.\n */\n clientLabel?: string\n /**\n * The workspace every request acts in — sent as `X-Nodaro-Workspace`.\n *\n * It selects which workspace a LIST reads from and where a CREATE lands.\n * It never authorizes: reading, updating, deleting or running an\n * identified object is decided by that object's own workspace, so a\n * forgotten header cannot hide work and a forged one cannot reach anyone\n * else's. Omit it to work in the caller's personal space.\n *\n * Prefer {@link NodaroClient.withWorkspace} for anything short-lived — one\n * client per workspace, no shared mutable selection to get wrong.\n */\n workspaceId?: string\n}\n\ninterface RequestOptions {\n body?: unknown\n query?: Record<string, string | number | boolean | undefined>\n headers?: Record<string, string>\n signal?: AbortSignal\n /** This request's timeout in ms, in place of the client's `timeoutMs`. */\n timeoutMs?: number\n}\n\n/**\n * The authenticated user's canonical identity (`GET /v1/me`). A token-\n * introspection primitive: any valid bearer token (first-party Supabase JWT or\n * a developer-app OAuth token) resolves to its owner's identity. Mirrors the\n * `profiles` identity columns server-side — the route is the source of truth.\n */\nexport interface UserIdentity {\n /** Nodaro user id (= the Supabase auth user id). */\n readonly id: string\n readonly email: string\n /** Human-readable display name (from `profiles.full_name`); `null` if unset. */\n readonly displayName: string | null\n /** Avatar URL; `null` if unset. */\n readonly avatarUrl: string | null\n /** Subscription tier (e.g. \"free\", \"pro\"). */\n readonly tier: string\n /**\n * Whether the user holds an admin role. DESCRIPTIVE only — use it to decide\n * whether to render admin surfaces without capability-probing an admin\n * endpoint; every admin API stays enforced server-side regardless.\n */\n readonly isAdmin: boolean\n}\n\n/**\n * An app the user let into their account through OAuth (`GET\n * /v1/me/connected-apps`): a developer app, an AI assistant's MCP connector, or\n * a self-hosted install. One entry per grant still in force.\n */\nexport interface ConnectedApp {\n /** The grant's id — what {@link NodaroClient.revokeConnectedApp} takes. */\n readonly authorizationId: string\n /** The app's name; `null` if it has none. A `dynamic_mcp` client chose its own. */\n readonly name: string | null\n /**\n * How the app came to exist: `user` (a developer app someone registered),\n * `first_party_mcp`, `dynamic_mcp` (an MCP client that registered itself) or\n * `community_instance` (a self-hosted install).\n */\n readonly kind: DeveloperAppKind | (string & {})\n readonly homepageUrl: string | null\n /** The scopes this grant holds. */\n readonly scopes: readonly DeveloperAppScope[]\n /** When the user granted access (ISO 8601). */\n readonly connectedAt: string\n /** The latest use of any token issued under the grant; `null` if never used. */\n readonly lastUsedAt: string | null\n}\n\nexport class NodaroClientCore {\n readonly baseUrl: string\n readonly auth: Auth\n readonly timeoutMs: number\n /** Value sent as `X-Nodaro-Client`; recorded by the backend as job provenance. */\n readonly clientLabel: string\n /** Whether to actually send it — see the note on {@link CLIENT_HEADER}. */\n private readonly sendClientHeader: boolean\n private readonly fetchOverride: typeof fetch | undefined\n\n /**\n * Resolved lazily so consumers can swap `globalThis.fetch` after the\n * client has been constructed (e.g. test mocks). Always rebound to the\n * global object — native fetch throws \"Illegal invocation\" when its\n * `this` is anything else.\n */\n get fetch(): typeof fetch {\n return this.fetchOverride ?? globalThis.fetch.bind(globalThis)\n }\n\n /** The workspace this client acts in; undefined = the personal space. */\n readonly workspaceId: string | undefined\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/$/, \"\") // strip trailing slash\n this.auth = opts.auth\n this.fetchOverride = opts.fetch\n this.timeoutMs = opts.timeoutMs ?? 60_000\n this.clientLabel = opts.clientLabel ?? `sdk/${SDK_VERSION}`\n this.workspaceId = opts.workspaceId\n // Explicit label = deliberate opt-in, always sent. The default is suppressed\n // in browsers — see the note on CLIENT_HEADER.\n this.sendClientHeader = opts.clientLabel !== undefined || !isBrowser()\n }\n\n /**\n * The transport: build the request, send it, throw the typed error on a\n * non-2xx via {@link throwFromResponse}, then read the body via `read` — all\n * inside the abort timer, so `timeoutMs` bounds the body download too (a\n * stalled CSV stream aborts, it does not hang forever). Both the JSON path\n * ({@link request}) and the text path ({@link requestText}) go through here.\n */\n private async send<T>(\n method: string,\n path: string,\n options: RequestOptions,\n read: (res: Response) => Promise<T>,\n ): Promise<T> {\n const url = this.buildUrl(path, options.query)\n\n const token = await this.auth.getToken()\n // A `FormData` body is a multipart upload: let the runtime set\n // `Content-Type: multipart/form-data; boundary=…` itself (a manual JSON\n // content-type corrupts the boundary), and send the body as-is rather than\n // JSON-stringifying it. Every other body stays JSON, exactly as before.\n const isFormData =\n typeof FormData !== \"undefined\" && options.body instanceof FormData\n const headers: Record<string, string> = {\n ...(isFormData ? {} : { \"Content-Type\": \"application/json\" }),\n ...(this.sendClientHeader ? { [CLIENT_HEADER]: this.clientLabel } : {}),\n // Before per-request headers: a resource that needs to reach outside\n // this client's workspace says so explicitly and wins.\n ...(this.workspaceId ? { [WORKSPACE_HEADER]: this.workspaceId } : {}),\n ...(options.headers ?? {}),\n }\n if (token) headers[\"Authorization\"] = `Bearer ${token}`\n\n const ac = new AbortController()\n const timeoutId = setTimeout(() => ac.abort(), options.timeoutMs ?? this.timeoutMs)\n const abort = () => ac.abort(options.signal?.reason)\n options.signal?.addEventListener(\"abort\", abort, { once: true })\n if (options.signal?.aborted) abort()\n\n try {\n ac.signal.throwIfAborted()\n const res = await this.fetch(url, {\n method,\n headers,\n body:\n options.body === undefined\n ? undefined\n : isFormData\n ? (options.body as FormData)\n : JSON.stringify(options.body),\n signal: ac.signal,\n })\n\n if (!res.ok) {\n let errBody: Record<string, unknown> = {}\n try {\n errBody = await res.json() as Record<string, unknown>\n } catch {\n // Empty/non-JSON body — fall through with empty errBody\n }\n throwFromResponse(res.status, errBody)\n }\n\n return await read(res)\n } finally {\n clearTimeout(timeoutId)\n options.signal?.removeEventListener(\"abort\", abort)\n }\n }\n\n /** Authenticated, bounded binary reads through the normal timeout/error transport. */\n async requestBytes(method: string, path: string, options: RequestOptions & { maxBytes: number }): Promise<ArrayBuffer> {\n if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1) throw new Error(\"maxBytes must be a positive integer\")\n return this.send(method, path, options, (response) => readBinaryResponse(response, options.maxBytes))\n }\n\n async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {\n return this.send(method, path, options, async (res) =>\n // 204 No Content\n res.status === 204 ? (undefined as T) : ((await res.json()) as T),\n )\n }\n\n /**\n * Like {@link request}, for endpoints that answer text (CSV exports). Errors\n * are still the JSON envelope and throw the same typed errors, and the read is\n * bounded by the same timeout as the request.\n */\n async requestText(method: string, path: string, options: RequestOptions = {}): Promise<string> {\n return this.send(method, path, options, (res) => res.text())\n }\n\n /**\n * `GET /v1/me` → the authenticated user's identity (see {@link UserIdentity}).\n * Unwraps the `{ data }` envelope. Throws `UnauthorizedError` (401) when the\n * token is missing/invalid, and the SDK's other typed errors as usual.\n *\n * On an instance with organizations it also carries what the caller belongs\n * to. THREE states, and collapsing them is wrong in a way users feel: the\n * organization fields ABSENT means this instance has no organizations at\n * all; present and empty means the account belongs to none; and\n * `organizationsUnavailable` means the lookup failed — keep whatever\n * selection you already had rather than concluding the person was removed\n * from everything.\n */\n async me(): Promise<UserIdentity & MeOrganizations> {\n const res = await this.request<{ data: UserIdentity & MeOrganizations }>(\"GET\", \"/v1/me\")\n return res.data\n }\n\n /**\n * `GET /v1/me/connected-apps` → every app with access to the signed-in\n * user's account (see {@link ConnectedApp}), newest grant first.\n *\n * **A signed-in browser session only.** The route answers a Supabase\n * session token (`supabaseAuth(…)`, `createSharedSupabaseClient`, or a\n * `StaticTokenAuth` holding one) and refuses every other credential with\n * `UnauthorizedError` (\"Sign in required\"): an app token or an API key must\n * not list the grants it lives under. Build your \"Connected apps\" settings\n * screen on this; a server-side integration cannot call it.\n */\n async connectedApps(): Promise<ConnectedApp[]> {\n const res = await this.request<{ apps: ConnectedApp[] }>(\"GET\", \"/v1/me/connected-apps\")\n return res.apps\n }\n\n /**\n * `POST /v1/me/connected-apps/:authorizationId/revoke` — ends an app's\n * access: the grant and every token issued under it, at once (the app's next\n * call is a 401). Resolves when done.\n *\n * A signed-in browser session only, like {@link connectedApps}. Throws\n * `NotFoundError` when the grant is not the user's or is already revoked.\n */\n async revokeConnectedApp(authorizationId: string): Promise<void> {\n await this.request(\"POST\", `/v1/me/connected-apps/${encodeURIComponent(authorizationId)}/revoke`)\n }\n\n private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>): string {\n const base = this.baseUrl || (typeof window !== \"undefined\" ? window.location.origin : \"http://placeholder\")\n const url = new URL(path, base)\n // If baseUrl was empty, strip the placeholder origin\n const fullUrl = this.baseUrl ? url.toString() : url.pathname + url.search\n if (query) {\n const u = new URL(this.baseUrl ? fullUrl : fullUrl, base)\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined) u.searchParams.set(k, String(v))\n }\n return this.baseUrl ? u.toString() : u.pathname + u.search\n }\n return fullUrl\n }\n}\n","/**\n * Auth provides the token used for `Authorization: Bearer <token>` on each request.\n * Implementations:\n * - StaticTokenAuth — fixed string (server-side, OAuth access token, API token)\n * - CallbackAuth — caller-supplied async function (BYO logic)\n * - supabaseAuth — pulls JWT from a Supabase client live (browser frontends)\n */\nexport interface Auth {\n /** Returns the current Bearer token, or null if not authenticated. */\n getToken(): Promise<string | null>\n}\n\nexport class StaticTokenAuth implements Auth {\n constructor(private token: string) {}\n async getToken() {\n return this.token\n }\n}\n\nexport class CallbackAuth implements Auth {\n constructor(private fn: () => string | null | Promise<string | null>) {}\n async getToken() {\n return this.fn()\n }\n}\n\ninterface SupabaseLikeClient {\n auth: {\n getSession(): Promise<{ data: { session: { access_token: string } | null } }>\n }\n}\n\n/** Pulls a JWT from a Supabase v2 client. Caller supplies their own supabase. */\nexport function supabaseAuth(supabase: SupabaseLikeClient): Auth {\n return {\n async getToken() {\n const { data } = await supabase.auth.getSession()\n return data.session?.access_token ?? null\n },\n }\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { throwFromResponse } from './chunk-
|
|
1
|
+
import { throwFromResponse } from './chunk-4VP2GFIZ.js';
|
|
2
2
|
import { WORKSPACE_HEADER } from '@nodaro/shared/organizations';
|
|
3
3
|
|
|
4
4
|
// src/binary-response.ts
|
|
@@ -34,7 +34,7 @@ async function readBinaryResponse(response, maxBytes) {
|
|
|
34
34
|
}
|
|
35
35
|
return bytes.buffer;
|
|
36
36
|
}
|
|
37
|
-
var SDK_VERSION = "2.
|
|
37
|
+
var SDK_VERSION = "2.56.0" ;
|
|
38
38
|
var CLIENT_HEADER = "X-Nodaro-Client";
|
|
39
39
|
var isBrowser = () => typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
40
40
|
var NodaroClientCore = class {
|
|
@@ -224,5 +224,5 @@ function supabaseAuth(supabase) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
export { CallbackAuth, NodaroClientCore, StaticTokenAuth, supabaseAuth };
|
|
227
|
-
//# sourceMappingURL=chunk-
|
|
228
|
-
//# sourceMappingURL=chunk-
|
|
227
|
+
//# sourceMappingURL=chunk-37LL6BPM.js.map
|
|
228
|
+
//# sourceMappingURL=chunk-37LL6BPM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/binary-response.ts","../src/core.ts","../src/auth.ts"],"names":[],"mappings":";;;;AACA,eAAsB,kBAAA,CAAmB,UAAoB,QAAA,EAAwC;AACnG,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAA;AACxD,EAAA,IAAI,UAAA,KAAe,IAAA,KAAS,CAAC,OAAA,CAAQ,IAAA,CAAK,UAAU,CAAA,IAAK,MAAA,CAAO,UAAU,CAAA,GAAI,QAAA,CAAA,EAAW;AACvF,IAAA,MAAM,QAAA,CAAS,MAAM,MAAA,EAAO;AAC5B,IAAA,MAAM,IAAI,MAAM,iDAAiD,CAAA;AAAA,EACnE;AACA,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,EAAM,OAAO,IAAI,YAAY,CAAC,CAAA;AAC5C,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,IAAA,CAAK,SAAA,EAAU;AACvC,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,EAAM;AACX,MAAA,MAAM,IAAA,GAAO,MAAM,MAAA,CAAO,IAAA,EAAK;AAC/B,MAAA,IAAI,KAAK,IAAA,EAAM;AACf,MAAA,MAAA,IAAU,KAAK,KAAA,CAAM,UAAA;AACrB,MAAA,IAAI,SAAS,QAAA,EAAU;AACrB,QAAA,MAAM,OAAO,MAAA,EAAO;AACpB,QAAA,MAAM,IAAI,MAAM,iDAAiD,CAAA;AAAA,MACnE;AACA,MAAA,MAAA,CAAO,IAAA,CAAK,KAAK,KAAK,CAAA;AAAA,IACxB;AAAA,EACF,CAAA,SAAE;AAAU,IAAA,MAAA,CAAO,WAAA,EAAY;AAAA,EAAE;AACjC,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAM,CAAA;AACnC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAAE,IAAA,KAAA,CAAM,GAAA,CAAI,OAAO,MAAM,CAAA;AAAG,IAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,EAAW;AACnF,EAAA,OAAO,KAAA,CAAM,MAAA;AACf;ACAA,IAAM,WAAA,GAAoD,QAAA,CAAkB;AAUrE,IAAM,aAAA,GAAgB,iBAAA;AAoB7B,IAAM,YAAY,MAAe,OAAO,WAAW,WAAA,IAAe,OAAO,OAAO,QAAA,KAAa,WAAA;AA4FtF,IAAM,mBAAN,MAAuB;AAAA,EACnB,OAAA;AAAA,EACA,IAAA;AAAA,EACA,SAAA;AAAA;AAAA,EAEA,WAAA;AAAA;AAAA,EAEQ,gBAAA;AAAA,EACA,aAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQjB,IAAI,KAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,aAAA,IAAiB,UAAA,CAAW,KAAA,CAAM,KAAK,UAAU,CAAA;AAAA,EAC/D;AAAA;AAAA,EAGS,WAAA;AAAA,EAET,YAAY,IAAA,EAAqB;AAC/B,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC7C,IAAA,IAAA,CAAK,OAAO,IAAA,CAAK,IAAA;AACjB,IAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,KAAA;AAC1B,IAAA,IAAA,CAAK,SAAA,GAAY,KAAK,SAAA,IAAa,GAAA;AACnC,IAAA,IAAA,CAAK,WAAA,GAAc,IAAA,CAAK,WAAA,IAAe,CAAA,IAAA,EAAO,WAAW,CAAA,CAAA;AACzD,IAAA,IAAA,CAAK,cAAc,IAAA,CAAK,WAAA;AAGxB,IAAA,IAAA,CAAK,gBAAA,GAAmB,IAAA,CAAK,WAAA,KAAgB,MAAA,IAAa,CAAC,SAAA,EAAU;AAAA,EACvE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAc,IAAA,CACZ,MAAA,EACA,IAAA,EACA,SACA,IAAA,EACY;AACZ,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,QAAQ,KAAK,CAAA;AAE7C,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,IAAA,CAAK,QAAA,EAAS;AAKvC,IAAA,MAAM,UAAA,GACJ,OAAO,QAAA,KAAa,WAAA,IAAe,QAAQ,IAAA,YAAgB,QAAA;AAC7D,IAAA,MAAM,OAAA,GAAkC;AAAA,MACtC,GAAI,UAAA,GAAa,EAAC,GAAI,EAAE,gBAAgB,kBAAA,EAAmB;AAAA,MAC3D,GAAI,IAAA,CAAK,gBAAA,GAAmB,EAAE,CAAC,aAAa,GAAG,IAAA,CAAK,WAAA,EAAY,GAAI,EAAC;AAAA;AAAA;AAAA,MAGrE,GAAI,IAAA,CAAK,WAAA,GAAc,EAAE,CAAC,gBAAgB,GAAG,IAAA,CAAK,WAAA,EAAY,GAAI,EAAC;AAAA,MACnE,GAAI,OAAA,CAAQ,OAAA,IAAW;AAAC,KAC1B;AACA,IAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,eAAe,CAAA,GAAI,UAAU,KAAK,CAAA,CAAA;AAErD,IAAA,MAAM,EAAA,GAAK,IAAI,eAAA,EAAgB;AAC/B,IAAA,MAAM,SAAA,GAAY,WAAW,MAAM,EAAA,CAAG,OAAM,EAAG,OAAA,CAAQ,SAAA,IAAa,IAAA,CAAK,SAAS,CAAA;AAClF,IAAA,MAAM,QAAQ,MAAM,EAAA,CAAG,KAAA,CAAM,OAAA,CAAQ,QAAQ,MAAM,CAAA;AACnD,IAAA,OAAA,CAAQ,QAAQ,gBAAA,CAAiB,OAAA,EAAS,OAAO,EAAE,IAAA,EAAM,MAAM,CAAA;AAC/D,IAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAA,EAAS,KAAA,EAAM;AAEnC,IAAA,IAAI;AACF,MAAA,EAAA,CAAG,OAAO,cAAA,EAAe;AACzB,MAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,EAAK;AAAA,QAChC,MAAA;AAAA,QACA,OAAA;AAAA,QACA,IAAA,EACE,OAAA,CAAQ,IAAA,KAAS,KAAA,CAAA,GACb,KAAA,CAAA,GACA,UAAA,GACG,OAAA,CAAQ,IAAA,GACT,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,IAAI,CAAA;AAAA,QACnC,QAAQ,EAAA,CAAG;AAAA,OACZ,CAAA;AAED,MAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,QAAA,IAAI,UAAmC,EAAC;AACxC,QAAA,IAAI;AACF,UAAA,OAAA,GAAU,MAAM,IAAI,IAAA,EAAK;AAAA,QAC3B,CAAA,CAAA,MAAQ;AAAA,QAER;AACA,QAAA,iBAAA,CAAkB,GAAA,CAAI,QAAQ,OAAO,CAAA;AAAA,MACvC;AAEA,MAAA,OAAO,MAAM,KAAK,GAAG,CAAA;AAAA,IACvB,CAAA,SAAE;AACA,MAAA,YAAA,CAAa,SAAS,CAAA;AACtB,MAAA,OAAA,CAAQ,MAAA,EAAQ,mBAAA,CAAoB,OAAA,EAAS,KAAK,CAAA;AAAA,IACpD;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,YAAA,CAAa,MAAA,EAAgB,IAAA,EAAc,OAAA,EAAsE;AACrH,IAAA,IAAI,CAAC,MAAA,CAAO,aAAA,CAAc,OAAA,CAAQ,QAAQ,CAAA,IAAK,OAAA,CAAQ,QAAA,GAAW,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,qCAAqC,CAAA;AAC1H,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,MAAA,EAAQ,IAAA,EAAM,OAAA,EAAS,CAAC,QAAA,KAAa,kBAAA,CAAmB,QAAA,EAAU,OAAA,CAAQ,QAAQ,CAAC,CAAA;AAAA,EACtG;AAAA,EAEA,MAAM,OAAA,CAAW,MAAA,EAAgB,IAAA,EAAc,OAAA,GAA0B,EAAC,EAAe;AACvF,IAAA,OAAO,IAAA,CAAK,IAAA;AAAA,MAAK,MAAA;AAAA,MAAQ,IAAA;AAAA,MAAM,OAAA;AAAA,MAAS,OAAO,GAAA;AAAA;AAAA,QAE7C,IAAI,MAAA,KAAW,GAAA,GAAO,MAAA,GAAoB,MAAM,IAAI,IAAA;AAAK;AAAA,KAC3D;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAAA,CAAY,MAAA,EAAgB,IAAA,EAAc,OAAA,GAA0B,EAAC,EAAoB;AAC7F,IAAA,OAAO,IAAA,CAAK,KAAK,MAAA,EAAQ,IAAA,EAAM,SAAS,CAAC,GAAA,KAAQ,GAAA,CAAI,IAAA,EAAM,CAAA;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAeA,MAAM,EAAA,GAA8C;AAClD,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAkD,OAAO,QAAQ,CAAA;AACxF,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,aAAA,GAAyC;AAC7C,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,OAAA,CAAkC,OAAO,uBAAuB,CAAA;AACvF,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,mBAAmB,eAAA,EAAwC;AAC/D,IAAA,MAAM,KAAK,OAAA,CAAQ,MAAA,EAAQ,yBAAyB,kBAAA,CAAmB,eAAe,CAAC,CAAA,OAAA,CAAS,CAAA;AAAA,EAClG;AAAA,EAEQ,QAAA,CAAS,MAAc,KAAA,EAAuE;AACpG,IAAA,MAAM,IAAA,GAAO,KAAK,OAAA,KAAY,OAAO,WAAW,WAAA,GAAc,MAAA,CAAO,SAAS,MAAA,GAAS,oBAAA,CAAA;AACvF,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,IAAA,EAAM,IAAI,CAAA;AAE9B,IAAA,MAAM,OAAA,GAAU,KAAK,OAAA,GAAU,GAAA,CAAI,UAAS,GAAI,GAAA,CAAI,WAAW,GAAA,CAAI,MAAA;AACnE,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,MAAM,IAAI,IAAI,GAAA,CAAI,KAAK,OAAA,GAAU,OAAA,GAAU,SAAS,IAAI,CAAA;AACxD,MAAA,KAAA,MAAW,CAAC,CAAA,EAAG,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC1C,QAAA,IAAI,CAAA,KAAM,QAAW,CAAA,CAAE,YAAA,CAAa,IAAI,CAAA,EAAG,MAAA,CAAO,CAAC,CAAC,CAAA;AAAA,MACtD;AACA,MAAA,OAAO,KAAK,OAAA,GAAU,CAAA,CAAE,UAAS,GAAI,CAAA,CAAE,WAAW,CAAA,CAAE,MAAA;AAAA,IACtD;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AACF;;;ACnUO,IAAM,kBAAN,MAAsC;AAAA,EAC3C,YAAoB,KAAA,EAAe;AAAf,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAAA,EAAgB;AAAA,EAAhB,KAAA;AAAA,EACpB,MAAM,QAAA,GAAW;AACf,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AACF;AAEO,IAAM,eAAN,MAAmC;AAAA,EACxC,YAAoB,EAAA,EAAkD;AAAlD,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAAA,EAAmD;AAAA,EAAnD,EAAA;AAAA,EACpB,MAAM,QAAA,GAAW;AACf,IAAA,OAAO,KAAK,EAAA,EAAG;AAAA,EACjB;AACF;AASO,SAAS,aAAa,QAAA,EAAoC;AAC/D,EAAA,OAAO;AAAA,IACL,MAAM,QAAA,GAAW;AACf,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,QAAA,CAAS,KAAK,UAAA,EAAW;AAChD,MAAA,OAAO,IAAA,CAAK,SAAS,YAAA,IAAgB,IAAA;AAAA,IACvC;AAAA,GACF;AACF","file":"chunk-CV2ZZAUO.js","sourcesContent":["/** Bound decoded response bytes even when Content-Length is absent or compressed. */\nexport async function readBinaryResponse(response: Response, maxBytes: number): Promise<ArrayBuffer> {\n const advertised = response.headers.get(\"content-length\")\n if (advertised !== null && (!/^\\d+$/.test(advertised) || Number(advertised) > maxBytes)) {\n await response.body?.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n if (!response.body) return new ArrayBuffer(0)\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let length = 0\n try {\n while (true) {\n const next = await reader.read()\n if (next.done) break\n length += next.value.byteLength\n if (length > maxBytes) {\n await reader.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n chunks.push(next.value)\n }\n } finally { reader.releaseLock() }\n const bytes = new Uint8Array(length)\n let offset = 0\n for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }\n return bytes.buffer\n}\n","/**\n * The client core: configuration, auth, the request transport and the\n * account-identity calls — everything a resource needs, and none of the\n * resources themselves.\n *\n * {@link NodaroClient} (`createClient`) extends it with every resource. An app\n * that uses only a few resources can extend it with just those, so it loads\n * (and its tests depend on) only the resource modules it actually uses:\n *\n * class AppClient extends NodaroClientCore {\n * readonly jobs = new JobsResource(this)\n * readonly nodes = new NodesResource(this) // needs `jobs` on the same client\n * }\n *\n * A resource that calls another one through the client says so in its\n * constructor type (`NodesResource` takes a core that also has `jobs`), so a\n * client missing it fails to compile rather than at run time.\n */\nimport { readBinaryResponse } from \"./binary-response.js\"\nimport { throwFromResponse } from \"./errors.js\"\nimport type { Auth } from \"./auth.js\"\nimport type { DeveloperAppKind, DeveloperAppScope } from \"./resources/developer-apps.js\"\nimport { WORKSPACE_HEADER, type MeOrganizations } from \"@nodaro/shared/organizations\"\n\n/** Replaced at build time by tsup `define` from package.json. The fallback\n * keeps `tsx`/vitest runs of the source working, where no define applies. */\ndeclare const __SDK_VERSION__: string | undefined\nconst SDK_VERSION = typeof __SDK_VERSION__ === \"string\" ? __SDK_VERSION__ : \"0.0.0-dev\"\n\n/**\n * Identifies the calling package to the backend, which records it as the job's\n * provenance (`jobs.source` / `source_detail`) so an operator can tell a CLI\n * run from an SDK integration from a browser session.\n *\n * Only `sdk/…` and `cli/…` are honoured server-side; anything else is ignored\n * rather than trusted, since this is an unauthenticated header.\n */\nexport const CLIENT_HEADER = \"X-Nodaro-Client\"\n\n/**\n * In a browser the header is redundant AND a liability, so it is not sent.\n *\n * Redundant: the browser already sends `Origin`, which names the actual product\n * (`studio.nodaro.ai`) rather than merely the library it used — and the backend\n * deliberately prefers Origin over this header, so a browser-sent value is\n * discarded on arrival.\n *\n * A liability: `X-Nodaro-Client` is not a CORS-safelisted request header, so a\n * browser sending it requires an exact match in the server's\n * `Access-Control-Allow-Headers`. Any deployment whose backend predates that\n * allowlist entry would fail the PREFLIGHT — breaking every API call from the\n * app, not just its provenance. Self-hosted and lagging deployments make that a\n * real risk, and nothing in a browser app would reveal the cause.\n *\n * An EXPLICIT `clientLabel` is always sent regardless: a caller that names\n * itself has opted in deliberately, and that intent outranks this default.\n */\nconst isBrowser = (): boolean => typeof window !== \"undefined\" && typeof window.document !== \"undefined\"\n\nexport interface ClientOptions {\n /** Backend base URL, e.g. \"https://nodaro.example.com\" or empty string for same-origin. */\n baseUrl: string\n /** Auth provider. Use StaticTokenAuth, supabaseAuth, or CallbackAuth. */\n auth: Auth\n /** Optional fetch override (for tests or custom transports). */\n fetch?: typeof fetch\n /** Default request timeout in ms. Default 60s. */\n timeoutMs?: number\n /**\n * Overrides the `X-Nodaro-Client` label. Defaults to `sdk/<version>`.\n *\n * Exists for `@nodaro/cli`, which is a wrapper AROUND this SDK: without an\n * override every CLI invocation would be recorded as an SDK call and the two\n * surfaces could never be told apart.\n */\n clientLabel?: string\n /**\n * The workspace every request acts in — sent as `X-Nodaro-Workspace`.\n *\n * It selects which workspace a LIST reads from and where a CREATE lands.\n * It never authorizes: reading, updating, deleting or running an\n * identified object is decided by that object's own workspace, so a\n * forgotten header cannot hide work and a forged one cannot reach anyone\n * else's. Omit it to work in the caller's personal space.\n *\n * Prefer {@link NodaroClient.withWorkspace} for anything short-lived — one\n * client per workspace, no shared mutable selection to get wrong.\n */\n workspaceId?: string\n}\n\ninterface RequestOptions {\n body?: unknown\n query?: Record<string, string | number | boolean | undefined>\n headers?: Record<string, string>\n signal?: AbortSignal\n /** This request's timeout in ms, in place of the client's `timeoutMs`. */\n timeoutMs?: number\n}\n\n/**\n * The authenticated user's canonical identity (`GET /v1/me`). A token-\n * introspection primitive: any valid bearer token (first-party Supabase JWT or\n * a developer-app OAuth token) resolves to its owner's identity. Mirrors the\n * `profiles` identity columns server-side — the route is the source of truth.\n */\nexport interface UserIdentity {\n /** Nodaro user id (= the Supabase auth user id). */\n readonly id: string\n readonly email: string\n /** Human-readable display name (from `profiles.full_name`); `null` if unset. */\n readonly displayName: string | null\n /** Avatar URL; `null` if unset. */\n readonly avatarUrl: string | null\n /** Subscription tier (e.g. \"free\", \"pro\"). */\n readonly tier: string\n /**\n * Whether the user holds an admin role. DESCRIPTIVE only — use it to decide\n * whether to render admin surfaces without capability-probing an admin\n * endpoint; every admin API stays enforced server-side regardless.\n */\n readonly isAdmin: boolean\n}\n\n/**\n * An app the user let into their account through OAuth (`GET\n * /v1/me/connected-apps`): a developer app, an AI assistant's MCP connector, or\n * a self-hosted install. One entry per grant still in force.\n */\nexport interface ConnectedApp {\n /** The grant's id — what {@link NodaroClient.revokeConnectedApp} takes. */\n readonly authorizationId: string\n /** The app's name; `null` if it has none. A `dynamic_mcp` client chose its own. */\n readonly name: string | null\n /**\n * How the app came to exist: `user` (a developer app someone registered),\n * `first_party_mcp`, `dynamic_mcp` (an MCP client that registered itself) or\n * `community_instance` (a self-hosted install).\n */\n readonly kind: DeveloperAppKind | (string & {})\n readonly homepageUrl: string | null\n /** The scopes this grant holds. */\n readonly scopes: readonly DeveloperAppScope[]\n /** When the user granted access (ISO 8601). */\n readonly connectedAt: string\n /** The latest use of any token issued under the grant; `null` if never used. */\n readonly lastUsedAt: string | null\n}\n\nexport class NodaroClientCore {\n readonly baseUrl: string\n readonly auth: Auth\n readonly timeoutMs: number\n /** Value sent as `X-Nodaro-Client`; recorded by the backend as job provenance. */\n readonly clientLabel: string\n /** Whether to actually send it — see the note on {@link CLIENT_HEADER}. */\n private readonly sendClientHeader: boolean\n private readonly fetchOverride: typeof fetch | undefined\n\n /**\n * Resolved lazily so consumers can swap `globalThis.fetch` after the\n * client has been constructed (e.g. test mocks). Always rebound to the\n * global object — native fetch throws \"Illegal invocation\" when its\n * `this` is anything else.\n */\n get fetch(): typeof fetch {\n return this.fetchOverride ?? globalThis.fetch.bind(globalThis)\n }\n\n /** The workspace this client acts in; undefined = the personal space. */\n readonly workspaceId: string | undefined\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/$/, \"\") // strip trailing slash\n this.auth = opts.auth\n this.fetchOverride = opts.fetch\n this.timeoutMs = opts.timeoutMs ?? 60_000\n this.clientLabel = opts.clientLabel ?? `sdk/${SDK_VERSION}`\n this.workspaceId = opts.workspaceId\n // Explicit label = deliberate opt-in, always sent. The default is suppressed\n // in browsers — see the note on CLIENT_HEADER.\n this.sendClientHeader = opts.clientLabel !== undefined || !isBrowser()\n }\n\n /**\n * The transport: build the request, send it, throw the typed error on a\n * non-2xx via {@link throwFromResponse}, then read the body via `read` — all\n * inside the abort timer, so `timeoutMs` bounds the body download too (a\n * stalled CSV stream aborts, it does not hang forever). Both the JSON path\n * ({@link request}) and the text path ({@link requestText}) go through here.\n */\n private async send<T>(\n method: string,\n path: string,\n options: RequestOptions,\n read: (res: Response) => Promise<T>,\n ): Promise<T> {\n const url = this.buildUrl(path, options.query)\n\n const token = await this.auth.getToken()\n // A `FormData` body is a multipart upload: let the runtime set\n // `Content-Type: multipart/form-data; boundary=…` itself (a manual JSON\n // content-type corrupts the boundary), and send the body as-is rather than\n // JSON-stringifying it. Every other body stays JSON, exactly as before.\n const isFormData =\n typeof FormData !== \"undefined\" && options.body instanceof FormData\n const headers: Record<string, string> = {\n ...(isFormData ? {} : { \"Content-Type\": \"application/json\" }),\n ...(this.sendClientHeader ? { [CLIENT_HEADER]: this.clientLabel } : {}),\n // Before per-request headers: a resource that needs to reach outside\n // this client's workspace says so explicitly and wins.\n ...(this.workspaceId ? { [WORKSPACE_HEADER]: this.workspaceId } : {}),\n ...(options.headers ?? {}),\n }\n if (token) headers[\"Authorization\"] = `Bearer ${token}`\n\n const ac = new AbortController()\n const timeoutId = setTimeout(() => ac.abort(), options.timeoutMs ?? this.timeoutMs)\n const abort = () => ac.abort(options.signal?.reason)\n options.signal?.addEventListener(\"abort\", abort, { once: true })\n if (options.signal?.aborted) abort()\n\n try {\n ac.signal.throwIfAborted()\n const res = await this.fetch(url, {\n method,\n headers,\n body:\n options.body === undefined\n ? undefined\n : isFormData\n ? (options.body as FormData)\n : JSON.stringify(options.body),\n signal: ac.signal,\n })\n\n if (!res.ok) {\n let errBody: Record<string, unknown> = {}\n try {\n errBody = await res.json() as Record<string, unknown>\n } catch {\n // Empty/non-JSON body — fall through with empty errBody\n }\n throwFromResponse(res.status, errBody)\n }\n\n return await read(res)\n } finally {\n clearTimeout(timeoutId)\n options.signal?.removeEventListener(\"abort\", abort)\n }\n }\n\n /** Authenticated, bounded binary reads through the normal timeout/error transport. */\n async requestBytes(method: string, path: string, options: RequestOptions & { maxBytes: number }): Promise<ArrayBuffer> {\n if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1) throw new Error(\"maxBytes must be a positive integer\")\n return this.send(method, path, options, (response) => readBinaryResponse(response, options.maxBytes))\n }\n\n async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {\n return this.send(method, path, options, async (res) =>\n // 204 No Content\n res.status === 204 ? (undefined as T) : ((await res.json()) as T),\n )\n }\n\n /**\n * Like {@link request}, for endpoints that answer text (CSV exports). Errors\n * are still the JSON envelope and throw the same typed errors, and the read is\n * bounded by the same timeout as the request.\n */\n async requestText(method: string, path: string, options: RequestOptions = {}): Promise<string> {\n return this.send(method, path, options, (res) => res.text())\n }\n\n /**\n * `GET /v1/me` → the authenticated user's identity (see {@link UserIdentity}).\n * Unwraps the `{ data }` envelope. Throws `UnauthorizedError` (401) when the\n * token is missing/invalid, and the SDK's other typed errors as usual.\n *\n * On an instance with organizations it also carries what the caller belongs\n * to. THREE states, and collapsing them is wrong in a way users feel: the\n * organization fields ABSENT means this instance has no organizations at\n * all; present and empty means the account belongs to none; and\n * `organizationsUnavailable` means the lookup failed — keep whatever\n * selection you already had rather than concluding the person was removed\n * from everything.\n */\n async me(): Promise<UserIdentity & MeOrganizations> {\n const res = await this.request<{ data: UserIdentity & MeOrganizations }>(\"GET\", \"/v1/me\")\n return res.data\n }\n\n /**\n * `GET /v1/me/connected-apps` → every app with access to the signed-in\n * user's account (see {@link ConnectedApp}), newest grant first.\n *\n * **A signed-in browser session only.** The route answers a Supabase\n * session token (`supabaseAuth(…)`, `createSharedSupabaseClient`, or a\n * `StaticTokenAuth` holding one) and refuses every other credential with\n * `UnauthorizedError` (\"Sign in required\"): an app token or an API key must\n * not list the grants it lives under. Build your \"Connected apps\" settings\n * screen on this; a server-side integration cannot call it.\n */\n async connectedApps(): Promise<ConnectedApp[]> {\n const res = await this.request<{ apps: ConnectedApp[] }>(\"GET\", \"/v1/me/connected-apps\")\n return res.apps\n }\n\n /**\n * `POST /v1/me/connected-apps/:authorizationId/revoke` — ends an app's\n * access: the grant and every token issued under it, at once (the app's next\n * call is a 401). Resolves when done.\n *\n * A signed-in browser session only, like {@link connectedApps}. Throws\n * `NotFoundError` when the grant is not the user's or is already revoked.\n */\n async revokeConnectedApp(authorizationId: string): Promise<void> {\n await this.request(\"POST\", `/v1/me/connected-apps/${encodeURIComponent(authorizationId)}/revoke`)\n }\n\n private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>): string {\n const base = this.baseUrl || (typeof window !== \"undefined\" ? window.location.origin : \"http://placeholder\")\n const url = new URL(path, base)\n // If baseUrl was empty, strip the placeholder origin\n const fullUrl = this.baseUrl ? url.toString() : url.pathname + url.search\n if (query) {\n const u = new URL(this.baseUrl ? fullUrl : fullUrl, base)\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined) u.searchParams.set(k, String(v))\n }\n return this.baseUrl ? u.toString() : u.pathname + u.search\n }\n return fullUrl\n }\n}\n","/**\n * Auth provides the token used for `Authorization: Bearer <token>` on each request.\n * Implementations:\n * - StaticTokenAuth — fixed string (server-side, OAuth access token, API token)\n * - CallbackAuth — caller-supplied async function (BYO logic)\n * - supabaseAuth — pulls JWT from a Supabase client live (browser frontends)\n */\nexport interface Auth {\n /** Returns the current Bearer token, or null if not authenticated. */\n getToken(): Promise<string | null>\n}\n\nexport class StaticTokenAuth implements Auth {\n constructor(private token: string) {}\n async getToken() {\n return this.token\n }\n}\n\nexport class CallbackAuth implements Auth {\n constructor(private fn: () => string | null | Promise<string | null>) {}\n async getToken() {\n return this.fn()\n }\n}\n\ninterface SupabaseLikeClient {\n auth: {\n getSession(): Promise<{ data: { session: { access_token: string } | null } }>\n }\n}\n\n/** Pulls a JWT from a Supabase v2 client. Caller supplies their own supabase. */\nexport function supabaseAuth(supabase: SupabaseLikeClient): Auth {\n return {\n async getToken() {\n const { data } = await supabase.auth.getSession()\n return data.session?.access_token ?? null\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/binary-response.ts","../src/core.ts","../src/auth.ts"],"names":[],"mappings":"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","file":"chunk-37LL6BPM.js","sourcesContent":["/** Bound decoded response bytes even when Content-Length is absent or compressed. */\nexport async function readBinaryResponse(response: Response, maxBytes: number): Promise<ArrayBuffer> {\n const advertised = response.headers.get(\"content-length\")\n if (advertised !== null && (!/^\\d+$/.test(advertised) || Number(advertised) > maxBytes)) {\n await response.body?.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n if (!response.body) return new ArrayBuffer(0)\n const reader = response.body.getReader()\n const chunks: Uint8Array[] = []\n let length = 0\n try {\n while (true) {\n const next = await reader.read()\n if (next.done) break\n length += next.value.byteLength\n if (length > maxBytes) {\n await reader.cancel()\n throw new Error(\"Binary response exceeds its declared size limit\")\n }\n chunks.push(next.value)\n }\n } finally { reader.releaseLock() }\n const bytes = new Uint8Array(length)\n let offset = 0\n for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }\n return bytes.buffer\n}\n","/**\n * The client core: configuration, auth, the request transport and the\n * account-identity calls — everything a resource needs, and none of the\n * resources themselves.\n *\n * {@link NodaroClient} (`createClient`) extends it with every resource. An app\n * that uses only a few resources can extend it with just those, so it loads\n * (and its tests depend on) only the resource modules it actually uses:\n *\n * class AppClient extends NodaroClientCore {\n * readonly jobs = new JobsResource(this)\n * readonly nodes = new NodesResource(this) // needs `jobs` on the same client\n * }\n *\n * A resource that calls another one through the client says so in its\n * constructor type (`NodesResource` takes a core that also has `jobs`), so a\n * client missing it fails to compile rather than at run time.\n */\nimport { readBinaryResponse } from \"./binary-response.js\"\nimport { throwFromResponse } from \"./errors.js\"\nimport type { Auth } from \"./auth.js\"\nimport type { DeveloperAppKind, DeveloperAppScope } from \"./resources/developer-apps.js\"\nimport { WORKSPACE_HEADER, type MeOrganizations } from \"@nodaro/shared/organizations\"\n\n/** Replaced at build time by tsup `define` from package.json. The fallback\n * keeps `tsx`/vitest runs of the source working, where no define applies. */\ndeclare const __SDK_VERSION__: string | undefined\nconst SDK_VERSION = typeof __SDK_VERSION__ === \"string\" ? __SDK_VERSION__ : \"0.0.0-dev\"\n\n/**\n * Identifies the calling package to the backend, which records it as the job's\n * provenance (`jobs.source` / `source_detail`) so an operator can tell a CLI\n * run from an SDK integration from a browser session.\n *\n * Only `sdk/…` and `cli/…` are honoured server-side; anything else is ignored\n * rather than trusted, since this is an unauthenticated header.\n */\nexport const CLIENT_HEADER = \"X-Nodaro-Client\"\n\n/**\n * In a browser the header is redundant AND a liability, so it is not sent.\n *\n * Redundant: the browser already sends `Origin`, which names the actual product\n * (`studio.nodaro.ai`) rather than merely the library it used — and the backend\n * deliberately prefers Origin over this header, so a browser-sent value is\n * discarded on arrival.\n *\n * A liability: `X-Nodaro-Client` is not a CORS-safelisted request header, so a\n * browser sending it requires an exact match in the server's\n * `Access-Control-Allow-Headers`. Any deployment whose backend predates that\n * allowlist entry would fail the PREFLIGHT — breaking every API call from the\n * app, not just its provenance. Self-hosted and lagging deployments make that a\n * real risk, and nothing in a browser app would reveal the cause.\n *\n * An EXPLICIT `clientLabel` is always sent regardless: a caller that names\n * itself has opted in deliberately, and that intent outranks this default.\n */\nconst isBrowser = (): boolean => typeof window !== \"undefined\" && typeof window.document !== \"undefined\"\n\nexport interface ClientOptions {\n /** Backend base URL, e.g. \"https://nodaro.example.com\" or empty string for same-origin. */\n baseUrl: string\n /** Auth provider. Use StaticTokenAuth, supabaseAuth, or CallbackAuth. */\n auth: Auth\n /** Optional fetch override (for tests or custom transports). */\n fetch?: typeof fetch\n /** Default request timeout in ms. Default 60s. */\n timeoutMs?: number\n /**\n * Overrides the `X-Nodaro-Client` label. Defaults to `sdk/<version>`.\n *\n * Exists for `@nodaro/cli`, which is a wrapper AROUND this SDK: without an\n * override every CLI invocation would be recorded as an SDK call and the two\n * surfaces could never be told apart.\n */\n clientLabel?: string\n /**\n * The workspace every request acts in — sent as `X-Nodaro-Workspace`.\n *\n * It selects which workspace a LIST reads from and where a CREATE lands.\n * It never authorizes: reading, updating, deleting or running an\n * identified object is decided by that object's own workspace, so a\n * forgotten header cannot hide work and a forged one cannot reach anyone\n * else's. Omit it to work in the caller's personal space.\n *\n * Prefer {@link NodaroClient.withWorkspace} for anything short-lived — one\n * client per workspace, no shared mutable selection to get wrong.\n */\n workspaceId?: string\n}\n\ninterface RequestOptions {\n body?: unknown\n query?: Record<string, string | number | boolean | undefined>\n headers?: Record<string, string>\n signal?: AbortSignal\n /** This request's timeout in ms, in place of the client's `timeoutMs`. */\n timeoutMs?: number\n}\n\n/**\n * The authenticated user's canonical identity (`GET /v1/me`). A token-\n * introspection primitive: any valid bearer token (first-party Supabase JWT or\n * a developer-app OAuth token) resolves to its owner's identity. Mirrors the\n * `profiles` identity columns server-side — the route is the source of truth.\n */\nexport interface UserIdentity {\n /** Nodaro user id (= the Supabase auth user id). */\n readonly id: string\n readonly email: string\n /** Human-readable display name (from `profiles.full_name`); `null` if unset. */\n readonly displayName: string | null\n /** Avatar URL; `null` if unset. */\n readonly avatarUrl: string | null\n /** Subscription tier (e.g. \"free\", \"pro\"). */\n readonly tier: string\n /**\n * Whether the user holds an admin role. DESCRIPTIVE only — use it to decide\n * whether to render admin surfaces without capability-probing an admin\n * endpoint; every admin API stays enforced server-side regardless.\n */\n readonly isAdmin: boolean\n}\n\n/**\n * An app the user let into their account through OAuth (`GET\n * /v1/me/connected-apps`): a developer app, an AI assistant's MCP connector, or\n * a self-hosted install. One entry per grant still in force.\n */\nexport interface ConnectedApp {\n /** The grant's id — what {@link NodaroClient.revokeConnectedApp} takes. */\n readonly authorizationId: string\n /** The app's name; `null` if it has none. A `dynamic_mcp` client chose its own. */\n readonly name: string | null\n /**\n * How the app came to exist: `user` (a developer app someone registered),\n * `first_party_mcp`, `dynamic_mcp` (an MCP client that registered itself) or\n * `community_instance` (a self-hosted install).\n */\n readonly kind: DeveloperAppKind | (string & {})\n readonly homepageUrl: string | null\n /** The scopes this grant holds. */\n readonly scopes: readonly DeveloperAppScope[]\n /** When the user granted access (ISO 8601). */\n readonly connectedAt: string\n /** The latest use of any token issued under the grant; `null` if never used. */\n readonly lastUsedAt: string | null\n}\n\nexport class NodaroClientCore {\n readonly baseUrl: string\n readonly auth: Auth\n readonly timeoutMs: number\n /** Value sent as `X-Nodaro-Client`; recorded by the backend as job provenance. */\n readonly clientLabel: string\n /** Whether to actually send it — see the note on {@link CLIENT_HEADER}. */\n private readonly sendClientHeader: boolean\n private readonly fetchOverride: typeof fetch | undefined\n\n /**\n * Resolved lazily so consumers can swap `globalThis.fetch` after the\n * client has been constructed (e.g. test mocks). Always rebound to the\n * global object — native fetch throws \"Illegal invocation\" when its\n * `this` is anything else.\n */\n get fetch(): typeof fetch {\n return this.fetchOverride ?? globalThis.fetch.bind(globalThis)\n }\n\n /** The workspace this client acts in; undefined = the personal space. */\n readonly workspaceId: string | undefined\n\n constructor(opts: ClientOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/$/, \"\") // strip trailing slash\n this.auth = opts.auth\n this.fetchOverride = opts.fetch\n this.timeoutMs = opts.timeoutMs ?? 60_000\n this.clientLabel = opts.clientLabel ?? `sdk/${SDK_VERSION}`\n this.workspaceId = opts.workspaceId\n // Explicit label = deliberate opt-in, always sent. The default is suppressed\n // in browsers — see the note on CLIENT_HEADER.\n this.sendClientHeader = opts.clientLabel !== undefined || !isBrowser()\n }\n\n /**\n * The transport: build the request, send it, throw the typed error on a\n * non-2xx via {@link throwFromResponse}, then read the body via `read` — all\n * inside the abort timer, so `timeoutMs` bounds the body download too (a\n * stalled CSV stream aborts, it does not hang forever). Both the JSON path\n * ({@link request}) and the text path ({@link requestText}) go through here.\n */\n private async send<T>(\n method: string,\n path: string,\n options: RequestOptions,\n read: (res: Response) => Promise<T>,\n ): Promise<T> {\n const url = this.buildUrl(path, options.query)\n\n const token = await this.auth.getToken()\n // A `FormData` body is a multipart upload: let the runtime set\n // `Content-Type: multipart/form-data; boundary=…` itself (a manual JSON\n // content-type corrupts the boundary), and send the body as-is rather than\n // JSON-stringifying it. Every other body stays JSON, exactly as before.\n const isFormData =\n typeof FormData !== \"undefined\" && options.body instanceof FormData\n const headers: Record<string, string> = {\n ...(isFormData ? {} : { \"Content-Type\": \"application/json\" }),\n ...(this.sendClientHeader ? { [CLIENT_HEADER]: this.clientLabel } : {}),\n // Before per-request headers: a resource that needs to reach outside\n // this client's workspace says so explicitly and wins.\n ...(this.workspaceId ? { [WORKSPACE_HEADER]: this.workspaceId } : {}),\n ...(options.headers ?? {}),\n }\n if (token) headers[\"Authorization\"] = `Bearer ${token}`\n\n const ac = new AbortController()\n const timeoutId = setTimeout(() => ac.abort(), options.timeoutMs ?? this.timeoutMs)\n const abort = () => ac.abort(options.signal?.reason)\n options.signal?.addEventListener(\"abort\", abort, { once: true })\n if (options.signal?.aborted) abort()\n\n try {\n ac.signal.throwIfAborted()\n const res = await this.fetch(url, {\n method,\n headers,\n body:\n options.body === undefined\n ? undefined\n : isFormData\n ? (options.body as FormData)\n : JSON.stringify(options.body),\n signal: ac.signal,\n })\n\n if (!res.ok) {\n let errBody: Record<string, unknown> = {}\n try {\n errBody = await res.json() as Record<string, unknown>\n } catch {\n // Empty/non-JSON body — fall through with empty errBody\n }\n throwFromResponse(res.status, errBody)\n }\n\n return await read(res)\n } finally {\n clearTimeout(timeoutId)\n options.signal?.removeEventListener(\"abort\", abort)\n }\n }\n\n /** Authenticated, bounded binary reads through the normal timeout/error transport. */\n async requestBytes(method: string, path: string, options: RequestOptions & { maxBytes: number }): Promise<ArrayBuffer> {\n if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1) throw new Error(\"maxBytes must be a positive integer\")\n return this.send(method, path, options, (response) => readBinaryResponse(response, options.maxBytes))\n }\n\n async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {\n return this.send(method, path, options, async (res) =>\n // 204 No Content\n res.status === 204 ? (undefined as T) : ((await res.json()) as T),\n )\n }\n\n /**\n * Like {@link request}, for endpoints that answer text (CSV exports). Errors\n * are still the JSON envelope and throw the same typed errors, and the read is\n * bounded by the same timeout as the request.\n */\n async requestText(method: string, path: string, options: RequestOptions = {}): Promise<string> {\n return this.send(method, path, options, (res) => res.text())\n }\n\n /**\n * `GET /v1/me` → the authenticated user's identity (see {@link UserIdentity}).\n * Unwraps the `{ data }` envelope. Throws `UnauthorizedError` (401) when the\n * token is missing/invalid, and the SDK's other typed errors as usual.\n *\n * On an instance with organizations it also carries what the caller belongs\n * to. THREE states, and collapsing them is wrong in a way users feel: the\n * organization fields ABSENT means this instance has no organizations at\n * all; present and empty means the account belongs to none; and\n * `organizationsUnavailable` means the lookup failed — keep whatever\n * selection you already had rather than concluding the person was removed\n * from everything.\n */\n async me(): Promise<UserIdentity & MeOrganizations> {\n const res = await this.request<{ data: UserIdentity & MeOrganizations }>(\"GET\", \"/v1/me\")\n return res.data\n }\n\n /**\n * `GET /v1/me/connected-apps` → every app with access to the signed-in\n * user's account (see {@link ConnectedApp}), newest grant first.\n *\n * **A signed-in browser session only.** The route answers a Supabase\n * session token (`supabaseAuth(…)`, `createSharedSupabaseClient`, or a\n * `StaticTokenAuth` holding one) and refuses every other credential with\n * `UnauthorizedError` (\"Sign in required\"): an app token or an API key must\n * not list the grants it lives under. Build your \"Connected apps\" settings\n * screen on this; a server-side integration cannot call it.\n */\n async connectedApps(): Promise<ConnectedApp[]> {\n const res = await this.request<{ apps: ConnectedApp[] }>(\"GET\", \"/v1/me/connected-apps\")\n return res.apps\n }\n\n /**\n * `POST /v1/me/connected-apps/:authorizationId/revoke` — ends an app's\n * access: the grant and every token issued under it, at once (the app's next\n * call is a 401). Resolves when done.\n *\n * A signed-in browser session only, like {@link connectedApps}. Throws\n * `NotFoundError` when the grant is not the user's or is already revoked.\n */\n async revokeConnectedApp(authorizationId: string): Promise<void> {\n await this.request(\"POST\", `/v1/me/connected-apps/${encodeURIComponent(authorizationId)}/revoke`)\n }\n\n private buildUrl(path: string, query?: Record<string, string | number | boolean | undefined>): string {\n const base = this.baseUrl || (typeof window !== \"undefined\" ? window.location.origin : \"http://placeholder\")\n const url = new URL(path, base)\n // If baseUrl was empty, strip the placeholder origin\n const fullUrl = this.baseUrl ? url.toString() : url.pathname + url.search\n if (query) {\n const u = new URL(this.baseUrl ? fullUrl : fullUrl, base)\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined) u.searchParams.set(k, String(v))\n }\n return this.baseUrl ? u.toString() : u.pathname + u.search\n }\n return fullUrl\n }\n}\n","/**\n * Auth provides the token used for `Authorization: Bearer <token>` on each request.\n * Implementations:\n * - StaticTokenAuth — fixed string (server-side, OAuth access token, API token)\n * - CallbackAuth — caller-supplied async function (BYO logic)\n * - supabaseAuth — pulls JWT from a Supabase client live (browser frontends)\n */\nexport interface Auth {\n /** Returns the current Bearer token, or null if not authenticated. */\n getToken(): Promise<string | null>\n}\n\nexport class StaticTokenAuth implements Auth {\n constructor(private token: string) {}\n async getToken() {\n return this.token\n }\n}\n\nexport class CallbackAuth implements Auth {\n constructor(private fn: () => string | null | Promise<string | null>) {}\n async getToken() {\n return this.fn()\n }\n}\n\ninterface SupabaseLikeClient {\n auth: {\n getSession(): Promise<{ data: { session: { access_token: string } | null } }>\n }\n}\n\n/** Pulls a JWT from a Supabase v2 client. Caller supplies their own supabase. */\nexport function supabaseAuth(supabase: SupabaseLikeClient): Auth {\n return {\n async getToken() {\n const { data } = await supabase.auth.getSession()\n return data.session?.access_token ?? null\n },\n }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkQ4MFQC2J_cjs = require('./chunk-Q4MFQC2J.cjs');
|
|
4
4
|
|
|
5
5
|
// src/resources/media.ts
|
|
6
6
|
var MediaResource = class {
|
|
@@ -44,7 +44,7 @@ var MediaResource = class {
|
|
|
44
44
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
45
45
|
signal: opts.signal
|
|
46
46
|
});
|
|
47
|
-
yield*
|
|
47
|
+
yield* chunkQ4MFQC2J_cjs.readSseStream(res, { label: "progress stream" });
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Copy an external media URL into your Nodaro storage (`POST /v1/save-to-storage`)
|
|
@@ -341,5 +341,5 @@ var MediaResource = class {
|
|
|
341
341
|
};
|
|
342
342
|
|
|
343
343
|
exports.MediaResource = MediaResource;
|
|
344
|
-
//# sourceMappingURL=chunk-
|
|
345
|
-
//# sourceMappingURL=chunk-
|
|
344
|
+
//# sourceMappingURL=chunk-3CVWTUK5.cjs.map
|
|
345
|
+
//# sourceMappingURL=chunk-3CVWTUK5.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resources/media.ts"],"names":["readSseStream"],"mappings":"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","file":"chunk-XALPYUZI.cjs","sourcesContent":["import type { NodaroClientCore } from \"../core.js\"\nimport { readSseStream } from \"../sse.js\"\nimport type { OverlayAnchor, VideoMaskRequest, MaskCompositeRequest } from \"@nodaro/shared/media\"\nimport type { CaptionStyle, CaptionLookId, SupportedFontName } from \"@nodaro/shared/captions\"\nimport type { TranscribeLane } from \"@nodaro/shared/models\"\nimport type { VideoOverlayRequest } from \"@nodaro/shared/video\"\n\n/** One word-timed caption entry (one per WORD for the kinetic styles). */\nexport interface CaptionEntry {\n text: string\n startMs: number\n endMs: number\n timestampMs?: number | null\n confidence?: number | null\n}\n\n/** Look + motion levers — see {@link MediaResource.addCaptions}. `look` is a\n * named preset the explicit levers override; an unset look renders as `outline`\n * (the default). The STYLING levers (`look`, `fontFamily`, `fontWeight`,\n * `strokeColor`/`strokeWidth`, `uppercase`, `positionY`, `maxWordsPerLine`) now\n * apply to the static `subtitle` style too — a subtitle carrying any of them\n * renders via Remotion. Only `highlightColor` (no spoken-word cursor to colour)\n * and `animate` (no motion to freeze) stay kinetic-only and are rejected (400)\n * on `subtitle`. */\nexport interface CaptionLookInput {\n look?: CaptionLookId\n fontFamily?: SupportedFontName\n fontWeight?: number\n strokeColor?: string\n strokeWidth?: number\n highlightColor?: string\n uppercase?: boolean\n positionY?: number\n /** Cap on how many words one caption LINE (or `tiktok-words` page) may hold,\n * 1-20, on TOP of the frame-width budget, sentence ends and pauses that\n * already close a line — 1-2 gives the punchy CapCut read, unset fits the\n * width. Applies to `word-highlight` / `karaoke` / `bouncy` / `tiktok-words`\n * and to a Remotion-rendered `subtitle`; inert on `word-pop` (always one\n * word). */\n maxWordsPerLine?: number\n /** Kinetic styles only. `false` freezes the per-word MOTION — the grouping,\n * line-holding and spoken-word highlight stay, only the movement stops;\n * default `true`. Rejected (400) on the static `subtitle` style. */\n animate?: boolean\n}\n\n/** One caption SEGMENT: a time range with optional style/look overrides and its\n * own words (`text`/`captions`, else the shared transcript filtered to range).\n * A segmented render is entirely Remotion, so any `style` is valid here. */\nexport interface CaptionSegmentInput extends CaptionLookInput {\n startMs: number\n endMs: number\n style?: CaptionStyle\n position?: \"bottom\" | \"top\" | \"center\"\n fontSize?: number\n color?: string\n backgroundColor?: string\n text?: string\n captions?: CaptionEntry[]\n}\n\n/** {@link MediaResource.addCaptions} input. */\nexport interface AddCaptionsInput extends CaptionLookInput {\n videoUrl: string\n text?: string\n captions?: CaptionEntry[]\n /** Transcribe the video's audio when no text/captions are given (default true). */\n autoTranscribe?: boolean\n transcribeProvider?: TranscribeLane\n style?: CaptionStyle\n position?: \"bottom\" | \"top\" | \"center\"\n fontSize?: number\n color?: string\n backgroundColor?: string\n /** Apply different treatments to time ranges in one call (non-overlapping). */\n segments?: CaptionSegmentInput[]\n}\n\n/**\n * Media ingestion + trimming — the source-preparation steps a Voice Changer Pro\n * flow (or any pipeline) needs before it has a clip to work on: pull a social\n * video into storage, copy a remote URL into storage, trim a video/audio to a\n * range, and probe a video's metadata. Each generation-style op returns a job id\n * to poll (`jobs.get(jobId)`); `videoMetadata` is a direct read.\n */\n/**\n * Where one overlay layer should go, in {@link MediaResource.imageOverlay}'s\n * percent units — the answer from {@link MediaResource.suggestOverlayPlacement}.\n */\nexport interface OverlayPlacement {\n /** The point on the base image the layer attaches to. */\n anchor: OverlayAnchor\n /** Offset from the anchor in % of the base width (negative on a right anchor = inward). */\n x: number\n /** Offset from the anchor in % of the base height (negative on a bottom anchor = inward). */\n y: number\n /** The layer's width in % of the base width. */\n width: number\n /** One sentence, plain language, on why this spot. */\n reason: string\n}\n\nexport class MediaResource {\n constructor(private client: NodaroClientCore) {}\n\n /**\n * Download a social video (YouTube / TikTok / Instagram / X / Facebook) into\n * your storage (`POST /v1/download-video`). `maxHeight` caps the resolution\n * (default \"best\"); `sectionStartSec` + `sectionEndSec` (both-or-neither) fetch\n * ONLY that time range instead of the whole video. A download that arrives\n * with no audio stream FAILS by default (it is usually a degraded source\n * response, and is retried through other routes first); pass\n * `requireAudio: false` to accept a clip that really has no sound. Returns a\n * `downloadId`; progress streams from\n * `GET /v1/download-video/progress/:downloadId` (server-sent events) and the\n * finished file lands in your library. A section is fetched with ±3 s of\n * padding for the caller to trim; `exactSection: true` cuts it exactly at\n * `sectionStartSec`–`sectionEndSec` instead.\n */\n downloadVideo(input: {\n url: string\n maxHeight?: number\n sectionStartSec?: number\n sectionEndSec?: number\n exactSection?: boolean\n requireAudio?: boolean\n }): Promise<{ downloadId: string }> {\n return this.client.request<{ downloadId: string }>(\"POST\", \"/v1/download-video\", { body: input })\n }\n\n /**\n * Stream the live progress of a {@link MediaResource.downloadVideo} import\n * (`GET /v1/download-video/progress/:downloadId`, server-sent events) as an\n * async iterable. Yields a {@link DownloadVideoProgress} roughly every 500ms\n * until the download reaches `completed` (its event carries the stored\n * `videoUrl`) or `failed` (its event carries `error`), then ends. The progress\n * state expires server-side shortly after the download starts existing, so\n * start iterating promptly after `downloadVideo` returns.\n *\n * No request timeout is applied (a large import legitimately takes minutes) —\n * pass an `AbortSignal` to cancel from the caller.\n */\n async *downloadVideoProgress(\n downloadId: string,\n opts: { signal?: AbortSignal } = {},\n ): AsyncGenerator<DownloadVideoProgress, void, undefined> {\n const url = `${this.client.baseUrl}/v1/download-video/progress/${encodeURIComponent(downloadId)}`\n const token = await this.client.auth.getToken()\n const res = await this.client.fetch(url, {\n headers: token ? { Authorization: `Bearer ${token}` } : {},\n signal: opts.signal,\n })\n // The shared reader owns the non-OK throw, the empty-body guard, the frame\n // parse and the cancel-on-early-exit — this route emits the same\n // `data: <json>` frames every other stream here does.\n yield* readSseStream<DownloadVideoProgress>(res, { label: \"progress stream\" })\n }\n\n /**\n * Copy an external media URL into your Nodaro storage (`POST /v1/save-to-storage`)\n * — a server-side fetch, so nothing round-trips through the client. Poll\n * `jobs.get(jobId)`.\n */\n saveToStorage(input: { mediaUrl: string; filename?: string; mediaType?: \"image\" | \"video\" | \"audio\" }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/save-to-storage\", { body: input })\n }\n\n /**\n * Composite 2–30 images into ONE large 2K/4K collage (`POST /v1/image-collage`).\n * `layout` `\"smart\"` (default — justified rows at each image's exact aspect\n * ratio, no cropping; the output height floats) or `\"grid\"` (uniform,\n * letterboxed cells). `imageSizes` is index-aligned with `imageUrls` and\n * gives per-image RELATIVE size hints for the smart layout: `0` auto\n * (\"don't care\", default), `1` big (~2× linear vs medium), `2` medium,\n * `3` small (~½ linear). All-equal hints change nothing; grid ignores them.\n * For storyboards, set `numbered` to stamp a 1-based sequence badge at each\n * image's corner (top-left by default; `badgePosition: \"top-right\"` moves it;\n * in `imageUrls` order) and pass `imageLabels`\n * (index-aligned with `imageUrls`; `null`/`\"\"`/omitted = no caption for that\n * image) to caption images after the number, e.g. `3 · Close-up`. Badges are\n * an overlay only — they never change the layout, the output size or the\n * credit cost. Poll `jobs.get(jobId)` for the finished image.\n */\n imageCollage(input: {\n imageUrls: string[]\n imageSizes?: Array<0 | 1 | 2 | 3>\n numbered?: boolean\n imageLabels?: Array<string | null>\n /** Corner the badges sit in — `\"top-left\"` (default, storyboard convention) or `\"top-right\"`. */\n badgePosition?: \"top-left\" | \"top-right\"\n layout?: \"smart\" | \"grid\"\n resolution?: \"2K\" | \"4K\"\n aspectRatio?: string\n gap?: number\n backgroundColor?: string\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/image-collage\", { body: input })\n }\n\n /**\n * Place 1–12 layers — pictures, real text, QR codes, shapes — on a base image,\n * pixel-exactly (`POST /v1/image-overlay`) — local, deterministic, no AI.\n * Every layer position/size is a PERCENT of the base image: `anchor` (nine\n * positions, default `\"center\"`), `x`/`y` (offset from the anchor in % of\n * the base width/height; negative on a right/bottom anchor moves inward),\n * `width` (% of the base width, default 25 — height follows the layer's\n * aspect unless `height` is set). Plus `opacity` (0..1), `rotation`\n * (degrees), `blend` (`\"over\"` | `\"multiply\"` | `\"screen\"`), `fit`,\n * `shadow` and `roundedCorners`. The output keeps the base's pixel size\n * unless `canvas` is set. Flat credit cost. Poll `jobs.get(jobId)` for the\n * finished image.\n */\n imageOverlay(input: {\n imageUrl: string\n layers: Array<{\n imageUrl: string\n anchor?: OverlayAnchor\n x?: number\n y?: number\n width?: number\n height?: number\n opacity?: number\n rotation?: number\n blend?: \"over\" | \"multiply\" | \"screen\"\n fit?: \"contain\" | \"cover\" | \"stretch\"\n shadow?: { blur: number; offsetX: number; offsetY: number; color: string; opacity: number }\n roundedCorners?: number\n zIndex?: number\n /** \"image\" (default, needs imageUrl) | \"text\" | \"qr\" | \"shape\" — see @nodaro/shared image-overlay-layers. */\n kind?: \"image\" | \"text\" | \"qr\" | \"shape\"\n text?: Record<string, unknown>\n qr?: Record<string, unknown>\n shape?: Record<string, unknown>\n effects?: Record<string, unknown>\n }>\n canvas?: { width: number; height: number; backgroundColor?: string }\n baseFit?: \"contain\" | \"cover\"\n outputFormat?: \"png\" | \"jpg\" | \"webp\"\n /** Extra platform renders (see OVERLAY_PLATFORMS in @nodaro/shared); returned as output `variants[]`. */\n variants?: string[]\n /** The mask the job also emits as output `maskUrl` (white = may change). Default \"around\". */\n maskMode?: \"none\" | \"layers\" | \"around\" | \"outside\"\n maskSpread?: number\n /** Fills every QR layer whose `qr.fromInput` is true — the node's QR link handle. */\n qrText?: string\n }): Promise<{ jobId: string }> {\n // The finished job's output: { imageUrl, width, height, maskUrl?, variants?: [{ id, label, width, height, url }] }.\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/image-overlay\", { body: input })\n }\n\n /**\n * Ask a vision model WHERE one overlay layer should sit on a base image\n * (`POST /v1/image-overlay/suggest-placement`) — it reads the picture and\n * keeps the element off the faces, the subject and the busiest texture.\n * The answer comes back in {@link MediaResource.imageOverlay}'s own percent\n * units — `anchor`, `x`/`y` offsets, `width` — so it drops straight onto a\n * layer (`const { reason, ...box } = placement`), plus a one-sentence\n * `reason` you can show a user. Nothing is composited here: apply the\n * placement yourself.\n *\n * `layerAspect` is the element's width / height (1 = square, the default) so\n * the proposed box stays in proportion; `intent` says what the element is\n * (\"a logo\", \"a price badge\"); `safeArea` is the always-visible region as\n * fractions of the canvas (a platform preset's safe area), which the\n * placement is kept inside. Unlike the other media calls this one answers\n * synchronously — there is nothing to poll; `jobId` is the billing record\n * (one image-to-text call).\n */\n suggestOverlayPlacement(input: {\n imageUrl: string\n layerAspect?: number\n intent?: string\n safeArea?: { x: number; y: number; w: number; h: number }\n llmModel?: string\n }): Promise<{ jobId: string; placement: OverlayPlacement }> {\n return this.client.request<{ jobId: string; placement: OverlayPlacement }>(\n \"POST\",\n \"/v1/image-overlay/suggest-placement\",\n { body: input },\n )\n }\n\n /**\n * Trim a video to a range (`POST /v1/trim-video`). Give the range in whichever\n * unit fits: `startTime`/`endTime` seconds, `trim*Frames`, `trim*Seconds`, or\n * `keepFirst`/`keepLastSeconds`. Poll `jobs.get(jobId)`.\n */\n trimVideo(input: {\n videoUrl: string\n startTime?: number\n endTime?: number\n trimStartFrames?: number\n trimEndFrames?: number\n trimStartSeconds?: number\n trimEndSeconds?: number\n keepFirstSeconds?: number\n keepLastSeconds?: number\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/trim-video\", { body: input })\n }\n\n /**\n * Burn captions into a video (`POST /v1/add-captions`). Give the words as\n * `text`, word-timed `captions[]`, or let it transcribe (`autoTranscribe`,\n * the default when neither is set).\n *\n * `style: \"subtitle\"` renders statically (FFmpeg) UNLESS it carries a styling\n * lever, in which case it — like the KINETIC styles (`word-highlight` /\n * `karaoke` / `tiktok-words` / `word-pop` / `bouncy`) — renders via Remotion.\n * `look` picks a preset — `outline` (Montserrat 900, UPPERCASE, black outline,\n * yellow spoken word — the TikTok/Reels read) or `clean`; an UNSET look renders\n * as `outline`. The explicit STYLING levers (`fontFamily`, `fontWeight`,\n * `strokeColor`/`strokeWidth`, `uppercase`, `positionY`, `maxWordsPerLine`)\n * override individual fields of it and now apply to `subtitle` too. Only\n * `highlightColor` (the spoken-word cursor) and `animate` stay kinetic-only\n * and are REJECTED on `subtitle`.\n *\n * `animate` (default true) freezes the per-word MOTION on the kinetic styles\n * when set to false — the grouping, line-holding and spoken-word highlight\n * stay; only the movement stops.\n *\n * `maxWordsPerLine` (1-20) caps how many words a line — or a `tiktok-words`\n * page — may hold, on top of the width budget / sentence ends / pauses that\n * already close one; it is inert on `word-pop`, which is always one word.\n * Like every lever here it exists top-level AND per segment, and a segment\n * that does not name its own inherits the top-level value.\n *\n * `segments[]` applies DIFFERENT treatments to non-overlapping time ranges in\n * one call (e.g. a large top intro, then a small bottom body); a segment that\n * names its own `look` starts fresh from that preset and does not inherit the\n * top-level explicit levers. Poll `jobs.get(jobId)`.\n *\n * `word-highlight` shows ONE held line at a time and a word's\n * `startMs`/`endMs` is its SPOKEN window (what times the highlight, not how\n * long the text is on screen) — so `captions[]` can be handed over verbatim\n * from an `audio.transcribe()` job's `output_data.words`, with\n * `autoTranscribe: false`.\n */\n addCaptions(input: AddCaptionsInput): Promise<{ jobId: string }> {\n const { autoTranscribe, transcribeProvider, ...rest } = input\n // The route body is camelCase except these two flags (worker snake_case);\n // omit each when unset so an absent flag stays absent on the wire.\n const body: Record<string, unknown> = {\n ...rest,\n ...(autoTranscribe !== undefined ? { auto_transcribe: autoTranscribe } : {}),\n ...(transcribeProvider !== undefined ? { transcribe_provider: transcribeProvider } : {}),\n }\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/add-captions\", { body })\n }\n\n /**\n * Trim (and extract) audio from a video or audio source\n * (`POST /v1/trim-audio`) to `[startTime, endTime]` seconds, in `audioFormat`\n * (`mp3` default / `wav` / `aac`). Poll `jobs.get(jobId)`.\n */\n trimAudio(input: {\n videoUrl?: string\n audioUrl?: string\n audioFormat?: \"mp3\" | \"wav\" | \"aac\"\n startTime?: number\n endTime?: number\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/trim-audio\", { body: input })\n }\n\n /**\n * Cut a video and/or an audio file into consecutive parts\n * (`POST /v1/split-media`). Every part is at most `chunkDuration` seconds and\n * at least `minPartDuration` (default 4, never above `chunkDuration`) — a\n * short leftover is evened out with the parts before it. Video is cut on\n * whole frames with each part's sound matching its picture; audio parts are\n * sample-exact, in `audioFormat` (`mp3` default / `wav` / `aac`). With both\n * sources each is split on its own. At most 200 parts per input. Poll\n * `jobs.get(jobId)`; the parts are on the completed job's `output_data` —\n * see {@link SplitMediaOutput}.\n */\n splitMedia(input: SplitMediaInput): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/split-media\", { body: input })\n }\n\n /**\n * Turn one still image + one audio track into an MP4\n * (`POST /v1/still-to-video`) — locally rendered (FFmpeg), no AI model,\n * zero credits. The output duration IS the audio's duration; there is no\n * duration field. Optional `motion` animates the still (zoom / pan /\n * ken-burns) at `intensity` 1–10. `fit: \"contain\"` letterboxes with\n * `padColor` instead of cropping. Poll `jobs.get(jobId)`.\n */\n stillToVideo(input: {\n imageUrl: string\n audioUrl: string\n motion?: \"none\" | \"zoom-in\" | \"zoom-out\" | \"pan-left\" | \"pan-right\" | \"ken-burns\"\n intensity?: number\n resolution?: \"720p\" | \"1080p\" | \"4K\"\n aspectRatio?: \"16:9\" | \"9:16\" | \"1:1\" | \"4:3\"\n fps?: 24 | 30\n fit?: \"cover\" | \"contain\"\n padColor?: string\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/still-to-video\", { body: input })\n }\n\n /**\n * Turn 2–100 images + one optional audio track into an MP4 slideshow\n * (`POST /v1/slideshow`) — locally rendered (FFmpeg), zero credits. With\n * audio, the output duration IS the audio's duration (equal split unless\n * `imageDurations` pins rows — null entries = auto; mismatched pinned sums\n * scale proportionally and the factor is disclosed in the job output).\n * Without audio: N × `perImageDuration`, silent output. Transitions\n * consume the outgoing slide, so totals stay exact. For a single image use\n * `stillToVideo`. Poll `jobs.get(jobId)`.\n */\n slideshow(input: {\n imageUrls: string[]\n audioUrl?: string\n imageDurations?: Array<number | null>\n perImageDuration?: number\n transition?: string\n transitionDuration?: number\n motion?: \"none\" | \"zoom-in\" | \"zoom-out\" | \"ken-burns\" | \"alternate\"\n intensity?: number\n resolution?: \"720p\" | \"1080p\" | \"4K\"\n aspectRatio?: \"16:9\" | \"9:16\" | \"1:1\" | \"4:3\"\n fps?: 24 | 30\n fit?: \"cover\" | \"contain\"\n padColor?: string\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/slideshow\", { body: input })\n }\n\n /**\n * Place 1–20 timed image layers over a video (`POST /v1/video-overlay`) —\n * rendered locally (FFmpeg) in one pass, no AI; the base audio is kept\n * untouched. Each layer has an `imageUrl`, a `start` and an optional `end`\n * in seconds (no `end` = to the end of the video) and a placement: a\n * `preset` (`\"card\"`, `\"corner-badge\"` with a `corner`, `\"full-frame\"`) or\n * an explicit box in {@link MediaResource.imageOverlay}'s percent vocabulary\n * (`anchor`, `x`, `y`, `width`, `height`, `fit`) — an explicit box field\n * overrides the preset, and a layer with neither is a corner badge\n * (bottom-right, or the `corner` it names). `outputAspect` renders onto a\n * 16:9 / 9:16 / 1:1 / 4:5 canvas\n * (`baseFit` cover by default; `backgroundColor` pads `contain`). Poll\n * `jobs.get(jobId)`: the output carries `videoUrl`, `thumbnailUrl`, `width`,\n * `height`, `durationSec` and `warnings[]` (a layer clipped or skipped at the\n * video's end, an animated image's first frame, re-encoded audio).\n */\n videoOverlay(input: VideoOverlayRequest): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/video-overlay\", { body: input })\n }\n\n /**\n * Track a subject described in plain text through a video and get a\n * black-and-white MASK VIDEO back (`POST /v1/video-mask`, SAM 3.1 by default,\n * or `provider: \"sam-3\"` at half the price) — white = the subject. Clips up\n * to 5 minutes at 30 fps (a faster clip counts\n * proportionally longer), priced by the clip's length. Poll\n * `jobs.get(jobId)`: the output carries `videoUrl` (the mask), `width`,\n * `height`, `frames`, `coverage` (the fraction of pixels selected, rounded\n * to 4 decimals) and `kept` (any region left on any frame: false only when\n * the size / zone filters left an empty mask — read it rather than\n * `coverage`, which a few pixels round to 0), and `recipe` (the version of\n * the masking algorithm that made it, `VIDEO_MASK_RECIPE`: it changes\n * whenever the same clip and settings would give a different mask). Feed the mask to\n * {@link MediaResource.maskComposite}.\n *\n * `holdLastMask: true` keeps the last mask on frames where the subject is\n * lost (a mask covering under 0.002 % of the frame, e.g. a face behind a\n * hand): each such frame after the first detection repeats the most recent\n * non-empty one, for at most 1 s of frames (`VIDEO_MASK_LIMITS.HOLD_MAX_SEC`;\n * longer gaps stay empty after that); the output then also carries `heldFrames`.\n * `minSubjectHeightPct` (0–100, default 0 = off) drops every connected\n * region whose bounding box is shorter than that % of the frame height\n * (extras' small faces behind the main subject); it runs before\n * `holdLastMask`, and the output then carries `droppedRegions`.\n * `maxSubjectHeightPct` (0–100, default 0 = off) is its mirror: it drops\n * regions TALLER than that % (keep only the small background faces). With\n * both set a region is kept when min ≤ height ≤ max; min > max is a 400.\n * `zone` (`{ leftPct, rightPct, topPct, bottomPct }`, each 0–49 % of the\n * frame, a margin left out = 0; default null = off) keeps only the regions\n * whose bounding-box centre lies inside the frame minus those margins (after\n * the size cuts, before `holdLastMask`). `perShot: true` finds the clip's\n * scene cuts first and segments each shot on its own: the subject is\n * detected again after every cut, `holdLastMask` never crosses a cut, and\n * each shot's mask is fitted to that shot's exact frame count; on a clip\n * with no cut it gives the same mask, fitted to the clip's exact frame\n * count. The output carries `shots` (1 when off). `redetectEverySec` (0 =\n * off, the default, or 1–10) also detects the subject again every that\n * many seconds — the clip (each shot, with `perShot`) is sent to the model\n * in pieces of at most that length, a last piece under 8 frames joining the\n * one before — so someone who walks in or turns to the camera mid-shot is\n * masked from the next boundary on. These boundaries are not cuts:\n * `holdLastMask` holds across them. Each piece is one more model request\n * (three run at a time), so it is slower; the price does not change. The\n * output's `chunks` counts the requests.\n */\n videoMask(input: VideoMaskRequest): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/video-mask\", { body: input })\n }\n\n /**\n * Place a foreground video over a background video through a mask video\n * (`POST /v1/mask-composite`; white = foreground) — local FFmpeg, no AI.\n * The result follows the foreground's size, frame rate and length; the\n * background is fitted (`backgroundFit`, default cover) and loops when\n * shorter; the mask holds its last frame when shorter. Optional\n * `invertMask`, `feather` (0–20 px), `grow` (−10–10 px), `opacity` (0–1,\n * default 1: how strongly the foreground covers the background inside the\n * mask) and `audioSource` (foreground / background / mix / none). Priced per started 5 seconds of\n * the result (the video it follows up to 300 s). Poll `jobs.get(jobId)` for `videoUrl`.\n *\n * `fill` picks what fills the masked area: `video` (default — the\n * foreground, required then), `pixelate` (the background itself in\n * blocks of `pixelSizePct` % of the frame width, 0.2–10, default 1.25) or\n * `blur` (the background blurred, sigma `blurStrengthPct` % of the frame\n * width, 0.1–5, default 2). Pixelate and blur take no\n * `foregroundUrl` (one sent is ignored): the result follows the background\n * and is priced on its length, and it always carries the background's sound\n * (`audioSource` is ignored).\n */\n maskComposite(input: MaskCompositeRequest): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/mask-composite\", { body: input })\n }\n\n /**\n * Probe a social video's metadata (`POST /v1/video-metadata`) — duration,\n * dimensions, title, live status — WITHOUT downloading it. A direct read, not a\n * job. Use it to decide whether to trim before importing.\n */\n videoMetadata(input: { url: string }): Promise<VideoMetadata> {\n return this.client.request<VideoMetadata>(\"POST\", \"/v1/video-metadata\", { body: input })\n }\n\n /**\n * Cut or crop a stored file (`POST /v1/media/process`) — synchronous and\n * free, the source-preparation sibling of the priced `trimVideo` node.\n * `trim` is `[startTime, endTime]` in seconds; `deleteSource: true` removes\n * the source object afterwards when it is yours and nothing else references\n * it (\"the cut replaces the original\"). Answers the stored file. The\n * platform may take up to 300 s on a long file: pass `timeoutMs` to wait\n * longer than the client's default.\n */\n process(\n input: MediaProcessInput,\n options: { timeoutMs?: number; signal?: AbortSignal } = {},\n ): Promise<{ data: MediaProcessResult }> {\n return this.client.request<{ data: MediaProcessResult }>(\"POST\", \"/v1/media/process\", { body: input, ...options })\n }\n\n /**\n * Throw away a file you just made and no longer need (`POST /v1/media/discard`):\n * a link download replaced before use, a cut superseded by a newer one, a\n * picture imported then cancelled. Not a general delete. The file goes only\n * when it is your own, was made by a link download (`downloadVideo`), a cut\n * (`process`) or a URL import, is under 24 hours old, and no job of yours\n * uses it (as an output or as an input such as a run's `videoUrl`);\n * otherwise it is left untouched and the call fails with 409\n * `not_discardable`. `discarded: false` means there was nothing of yours at\n * that address (already gone, or not yours).\n */\n discard(input: { url: string }): Promise<{ data: MediaDiscardResult }> {\n return this.client.request<{ data: MediaDiscardResult }>(\"POST\", \"/v1/media/discard\", { body: input })\n }\n}\n\n/**\n * One event from {@link MediaResource.downloadVideoProgress}. The stream ends\n * after a `completed` event (which carries the stored `videoUrl` + an optional\n * `thumbnailUrl`) or a `failed` event (which carries `error`).\n */\nexport interface DownloadVideoProgress {\n phase: \"downloading\" | \"processing\" | \"uploading\" | \"completed\" | \"failed\"\n /** Download percent (0–100). Section fetches report jumpy percents — display, don't sum. */\n percent: number\n /** The imported video's storage URL — set on the `completed` event. */\n videoUrl?: string\n /** Thumbnail storage URL — set on the `completed` event when one was captured. */\n thumbnailUrl?: string\n /** What went wrong — set on the `failed` event. */\n error?: string\n}\n\n/** Result of {@link MediaResource.videoMetadata}. Fields are best-effort — a probe may omit some. */\nexport interface VideoMetadata {\n durationSec?: number | null\n width?: number | null\n height?: number | null\n title?: string | null\n isLive?: boolean\n [key: string]: unknown\n}\n\n/** Body of {@link MediaResource.splitMedia} (`POST /v1/split-media`). At least one of `videoUrl` / `audioUrl`. */\nexport interface SplitMediaInput {\n /** Video to split. */\n videoUrl?: string\n /** Audio file to split (split on its own when a video is given too). */\n audioUrl?: string\n /** The longest a part may be, in seconds (at least 1). Every part is at most this long. */\n chunkDuration: number\n /** The shortest a part may be, in seconds (0 or more, default 4). Must not exceed `chunkDuration` (400 otherwise). */\n minPartDuration?: number\n /** Format of the audio parts. Default `mp3`. */\n audioFormat?: \"mp3\" | \"wav\" | \"aac\"\n}\n\n/** A completed split-media job's `output_data`. */\nexport interface SplitMediaOutput {\n /** The video parts, in order (when a video was split). */\n videoUrls?: string[]\n /** The audio parts, in order (when an audio file was split). */\n audioUrls?: string[]\n /** The number of parts — the larger of the two lists. */\n chunkCount: number\n /** Each part's exact length in seconds: the video parts' when a video was split, else the audio parts'. */\n partDurationsSec?: number[]\n /** The audio parts' lengths, only when BOTH a video and an audio file were split. */\n audioPartDurationsSec?: number[]\n}\n\nexport interface MediaProcessInput {\n sourceUrl: string\n type: \"video\" | \"audio\"\n trim?: { startTime: number; endTime: number }\n crop?: { x: number; y: number; width: number; height: number }\n format?: \"mp4\" | \"webm\" | \"mp3\" | \"wav\" | \"m4a\" | \"aac\"\n deleteSource?: boolean\n}\n\n/** Result of {@link MediaResource.discard}. */\nexport interface MediaDiscardResult {\n /** True when the file was deleted; false when there was nothing of yours at that address. */\n discarded: boolean\n}\n\nexport interface MediaProcessResult {\n url: string\n thumbnailUrl: string | null\n assetId: string | null\n sizeBytes: number\n mimeType: string\n metadata: Record<string, unknown>\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/resources/media.ts"],"names":["readSseStream"],"mappings":"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","file":"chunk-3CVWTUK5.cjs","sourcesContent":["import type { NodaroClientCore } from \"../core.js\"\nimport { readSseStream } from \"../sse.js\"\nimport type { OverlayAnchor, VideoMaskRequest, MaskCompositeRequest } from \"@nodaro/shared/media\"\nimport type { CaptionStyle, CaptionLookId, SupportedFontName } from \"@nodaro/shared/captions\"\nimport type { TranscribeLane } from \"@nodaro/shared/models\"\nimport type { VideoOverlayRequest } from \"@nodaro/shared/video\"\n\n/** One word-timed caption entry (one per WORD for the kinetic styles). */\nexport interface CaptionEntry {\n text: string\n startMs: number\n endMs: number\n timestampMs?: number | null\n confidence?: number | null\n}\n\n/** Look + motion levers — see {@link MediaResource.addCaptions}. `look` is a\n * named preset the explicit levers override; an unset look renders as `outline`\n * (the default). The STYLING levers (`look`, `fontFamily`, `fontWeight`,\n * `strokeColor`/`strokeWidth`, `uppercase`, `positionY`, `maxWordsPerLine`) now\n * apply to the static `subtitle` style too — a subtitle carrying any of them\n * renders via Remotion. Only `highlightColor` (no spoken-word cursor to colour)\n * and `animate` (no motion to freeze) stay kinetic-only and are rejected (400)\n * on `subtitle`. */\nexport interface CaptionLookInput {\n look?: CaptionLookId\n fontFamily?: SupportedFontName\n fontWeight?: number\n strokeColor?: string\n strokeWidth?: number\n highlightColor?: string\n uppercase?: boolean\n positionY?: number\n /** Cap on how many words one caption LINE (or `tiktok-words` page) may hold,\n * 1-20, on TOP of the frame-width budget, sentence ends and pauses that\n * already close a line — 1-2 gives the punchy CapCut read, unset fits the\n * width. Applies to `word-highlight` / `karaoke` / `bouncy` / `tiktok-words`\n * and to a Remotion-rendered `subtitle`; inert on `word-pop` (always one\n * word). */\n maxWordsPerLine?: number\n /** Kinetic styles only. `false` freezes the per-word MOTION — the grouping,\n * line-holding and spoken-word highlight stay, only the movement stops;\n * default `true`. Rejected (400) on the static `subtitle` style. */\n animate?: boolean\n}\n\n/** One caption SEGMENT: a time range with optional style/look overrides and its\n * own words (`text`/`captions`, else the shared transcript filtered to range).\n * A segmented render is entirely Remotion, so any `style` is valid here. */\nexport interface CaptionSegmentInput extends CaptionLookInput {\n startMs: number\n endMs: number\n style?: CaptionStyle\n position?: \"bottom\" | \"top\" | \"center\"\n fontSize?: number\n color?: string\n backgroundColor?: string\n text?: string\n captions?: CaptionEntry[]\n}\n\n/** {@link MediaResource.addCaptions} input. */\nexport interface AddCaptionsInput extends CaptionLookInput {\n videoUrl: string\n text?: string\n captions?: CaptionEntry[]\n /** Transcribe the video's audio when no text/captions are given (default true). */\n autoTranscribe?: boolean\n transcribeProvider?: TranscribeLane\n style?: CaptionStyle\n position?: \"bottom\" | \"top\" | \"center\"\n fontSize?: number\n color?: string\n backgroundColor?: string\n /** Apply different treatments to time ranges in one call (non-overlapping). */\n segments?: CaptionSegmentInput[]\n}\n\n/**\n * Media ingestion + trimming — the source-preparation steps a Voice Changer Pro\n * flow (or any pipeline) needs before it has a clip to work on: pull a social\n * video into storage, copy a remote URL into storage, trim a video/audio to a\n * range, and probe a video's metadata. Each generation-style op returns a job id\n * to poll (`jobs.get(jobId)`); `videoMetadata` is a direct read.\n */\n/**\n * Where one overlay layer should go, in {@link MediaResource.imageOverlay}'s\n * percent units — the answer from {@link MediaResource.suggestOverlayPlacement}.\n */\nexport interface OverlayPlacement {\n /** The point on the base image the layer attaches to. */\n anchor: OverlayAnchor\n /** Offset from the anchor in % of the base width (negative on a right anchor = inward). */\n x: number\n /** Offset from the anchor in % of the base height (negative on a bottom anchor = inward). */\n y: number\n /** The layer's width in % of the base width. */\n width: number\n /** One sentence, plain language, on why this spot. */\n reason: string\n}\n\nexport class MediaResource {\n constructor(private client: NodaroClientCore) {}\n\n /**\n * Download a social video (YouTube / TikTok / Instagram / X / Facebook) into\n * your storage (`POST /v1/download-video`). `maxHeight` caps the resolution\n * (default \"best\"); `sectionStartSec` + `sectionEndSec` (both-or-neither) fetch\n * ONLY that time range instead of the whole video. A download that arrives\n * with no audio stream FAILS by default (it is usually a degraded source\n * response, and is retried through other routes first); pass\n * `requireAudio: false` to accept a clip that really has no sound. Returns a\n * `downloadId`; progress streams from\n * `GET /v1/download-video/progress/:downloadId` (server-sent events) and the\n * finished file lands in your library. A section is fetched with ±3 s of\n * padding for the caller to trim; `exactSection: true` cuts it exactly at\n * `sectionStartSec`–`sectionEndSec` instead.\n */\n downloadVideo(input: {\n url: string\n maxHeight?: number\n sectionStartSec?: number\n sectionEndSec?: number\n exactSection?: boolean\n requireAudio?: boolean\n }): Promise<{ downloadId: string }> {\n return this.client.request<{ downloadId: string }>(\"POST\", \"/v1/download-video\", { body: input })\n }\n\n /**\n * Stream the live progress of a {@link MediaResource.downloadVideo} import\n * (`GET /v1/download-video/progress/:downloadId`, server-sent events) as an\n * async iterable. Yields a {@link DownloadVideoProgress} roughly every 500ms\n * until the download reaches `completed` (its event carries the stored\n * `videoUrl`) or `failed` (its event carries `error`), then ends. The progress\n * state expires server-side shortly after the download starts existing, so\n * start iterating promptly after `downloadVideo` returns.\n *\n * No request timeout is applied (a large import legitimately takes minutes) —\n * pass an `AbortSignal` to cancel from the caller.\n */\n async *downloadVideoProgress(\n downloadId: string,\n opts: { signal?: AbortSignal } = {},\n ): AsyncGenerator<DownloadVideoProgress, void, undefined> {\n const url = `${this.client.baseUrl}/v1/download-video/progress/${encodeURIComponent(downloadId)}`\n const token = await this.client.auth.getToken()\n const res = await this.client.fetch(url, {\n headers: token ? { Authorization: `Bearer ${token}` } : {},\n signal: opts.signal,\n })\n // The shared reader owns the non-OK throw, the empty-body guard, the frame\n // parse and the cancel-on-early-exit — this route emits the same\n // `data: <json>` frames every other stream here does.\n yield* readSseStream<DownloadVideoProgress>(res, { label: \"progress stream\" })\n }\n\n /**\n * Copy an external media URL into your Nodaro storage (`POST /v1/save-to-storage`)\n * — a server-side fetch, so nothing round-trips through the client. Poll\n * `jobs.get(jobId)`.\n */\n saveToStorage(input: { mediaUrl: string; filename?: string; mediaType?: \"image\" | \"video\" | \"audio\" }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/save-to-storage\", { body: input })\n }\n\n /**\n * Composite 2–30 images into ONE large 2K/4K collage (`POST /v1/image-collage`).\n * `layout` `\"smart\"` (default — justified rows at each image's exact aspect\n * ratio, no cropping; the output height floats) or `\"grid\"` (uniform,\n * letterboxed cells). `imageSizes` is index-aligned with `imageUrls` and\n * gives per-image RELATIVE size hints for the smart layout: `0` auto\n * (\"don't care\", default), `1` big (~2× linear vs medium), `2` medium,\n * `3` small (~½ linear). All-equal hints change nothing; grid ignores them.\n * For storyboards, set `numbered` to stamp a 1-based sequence badge at each\n * image's corner (top-left by default; `badgePosition: \"top-right\"` moves it;\n * in `imageUrls` order) and pass `imageLabels`\n * (index-aligned with `imageUrls`; `null`/`\"\"`/omitted = no caption for that\n * image) to caption images after the number, e.g. `3 · Close-up`. Badges are\n * an overlay only — they never change the layout, the output size or the\n * credit cost. Poll `jobs.get(jobId)` for the finished image.\n */\n imageCollage(input: {\n imageUrls: string[]\n imageSizes?: Array<0 | 1 | 2 | 3>\n numbered?: boolean\n imageLabels?: Array<string | null>\n /** Corner the badges sit in — `\"top-left\"` (default, storyboard convention) or `\"top-right\"`. */\n badgePosition?: \"top-left\" | \"top-right\"\n layout?: \"smart\" | \"grid\"\n resolution?: \"2K\" | \"4K\"\n aspectRatio?: string\n gap?: number\n backgroundColor?: string\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/image-collage\", { body: input })\n }\n\n /**\n * Place 1–12 layers — pictures, real text, QR codes, shapes — on a base image,\n * pixel-exactly (`POST /v1/image-overlay`) — local, deterministic, no AI.\n * Every layer position/size is a PERCENT of the base image: `anchor` (nine\n * positions, default `\"center\"`), `x`/`y` (offset from the anchor in % of\n * the base width/height; negative on a right/bottom anchor moves inward),\n * `width` (% of the base width, default 25 — height follows the layer's\n * aspect unless `height` is set). Plus `opacity` (0..1), `rotation`\n * (degrees), `blend` (`\"over\"` | `\"multiply\"` | `\"screen\"`), `fit`,\n * `shadow` and `roundedCorners`. The output keeps the base's pixel size\n * unless `canvas` is set. Flat credit cost. Poll `jobs.get(jobId)` for the\n * finished image.\n */\n imageOverlay(input: {\n imageUrl: string\n layers: Array<{\n imageUrl: string\n anchor?: OverlayAnchor\n x?: number\n y?: number\n width?: number\n height?: number\n opacity?: number\n rotation?: number\n blend?: \"over\" | \"multiply\" | \"screen\"\n fit?: \"contain\" | \"cover\" | \"stretch\"\n shadow?: { blur: number; offsetX: number; offsetY: number; color: string; opacity: number }\n roundedCorners?: number\n zIndex?: number\n /** \"image\" (default, needs imageUrl) | \"text\" | \"qr\" | \"shape\" — see @nodaro/shared image-overlay-layers. */\n kind?: \"image\" | \"text\" | \"qr\" | \"shape\"\n text?: Record<string, unknown>\n qr?: Record<string, unknown>\n shape?: Record<string, unknown>\n effects?: Record<string, unknown>\n }>\n canvas?: { width: number; height: number; backgroundColor?: string }\n baseFit?: \"contain\" | \"cover\"\n outputFormat?: \"png\" | \"jpg\" | \"webp\"\n /** Extra platform renders (see OVERLAY_PLATFORMS in @nodaro/shared); returned as output `variants[]`. */\n variants?: string[]\n /** The mask the job also emits as output `maskUrl` (white = may change). Default \"around\". */\n maskMode?: \"none\" | \"layers\" | \"around\" | \"outside\"\n maskSpread?: number\n /** Fills every QR layer whose `qr.fromInput` is true — the node's QR link handle. */\n qrText?: string\n }): Promise<{ jobId: string }> {\n // The finished job's output: { imageUrl, width, height, maskUrl?, variants?: [{ id, label, width, height, url }] }.\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/image-overlay\", { body: input })\n }\n\n /**\n * Ask a vision model WHERE one overlay layer should sit on a base image\n * (`POST /v1/image-overlay/suggest-placement`) — it reads the picture and\n * keeps the element off the faces, the subject and the busiest texture.\n * The answer comes back in {@link MediaResource.imageOverlay}'s own percent\n * units — `anchor`, `x`/`y` offsets, `width` — so it drops straight onto a\n * layer (`const { reason, ...box } = placement`), plus a one-sentence\n * `reason` you can show a user. Nothing is composited here: apply the\n * placement yourself.\n *\n * `layerAspect` is the element's width / height (1 = square, the default) so\n * the proposed box stays in proportion; `intent` says what the element is\n * (\"a logo\", \"a price badge\"); `safeArea` is the always-visible region as\n * fractions of the canvas (a platform preset's safe area), which the\n * placement is kept inside. Unlike the other media calls this one answers\n * synchronously — there is nothing to poll; `jobId` is the billing record\n * (one image-to-text call).\n */\n suggestOverlayPlacement(input: {\n imageUrl: string\n layerAspect?: number\n intent?: string\n safeArea?: { x: number; y: number; w: number; h: number }\n llmModel?: string\n }): Promise<{ jobId: string; placement: OverlayPlacement }> {\n return this.client.request<{ jobId: string; placement: OverlayPlacement }>(\n \"POST\",\n \"/v1/image-overlay/suggest-placement\",\n { body: input },\n )\n }\n\n /**\n * Trim a video to a range (`POST /v1/trim-video`). Give the range in whichever\n * unit fits: `startTime`/`endTime` seconds, `trim*Frames`, `trim*Seconds`, or\n * `keepFirst`/`keepLastSeconds`. Poll `jobs.get(jobId)`.\n */\n trimVideo(input: {\n videoUrl: string\n startTime?: number\n endTime?: number\n trimStartFrames?: number\n trimEndFrames?: number\n trimStartSeconds?: number\n trimEndSeconds?: number\n keepFirstSeconds?: number\n keepLastSeconds?: number\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/trim-video\", { body: input })\n }\n\n /**\n * Burn captions into a video (`POST /v1/add-captions`). Give the words as\n * `text`, word-timed `captions[]`, or let it transcribe (`autoTranscribe`,\n * the default when neither is set).\n *\n * `style: \"subtitle\"` renders statically (FFmpeg) UNLESS it carries a styling\n * lever, in which case it — like the KINETIC styles (`word-highlight` /\n * `karaoke` / `tiktok-words` / `word-pop` / `bouncy`) — renders via Remotion.\n * `look` picks a preset — `outline` (Montserrat 900, UPPERCASE, black outline,\n * yellow spoken word — the TikTok/Reels read) or `clean`; an UNSET look renders\n * as `outline`. The explicit STYLING levers (`fontFamily`, `fontWeight`,\n * `strokeColor`/`strokeWidth`, `uppercase`, `positionY`, `maxWordsPerLine`)\n * override individual fields of it and now apply to `subtitle` too. Only\n * `highlightColor` (the spoken-word cursor) and `animate` stay kinetic-only\n * and are REJECTED on `subtitle`.\n *\n * `animate` (default true) freezes the per-word MOTION on the kinetic styles\n * when set to false — the grouping, line-holding and spoken-word highlight\n * stay; only the movement stops.\n *\n * `maxWordsPerLine` (1-20) caps how many words a line — or a `tiktok-words`\n * page — may hold, on top of the width budget / sentence ends / pauses that\n * already close one; it is inert on `word-pop`, which is always one word.\n * Like every lever here it exists top-level AND per segment, and a segment\n * that does not name its own inherits the top-level value.\n *\n * `segments[]` applies DIFFERENT treatments to non-overlapping time ranges in\n * one call (e.g. a large top intro, then a small bottom body); a segment that\n * names its own `look` starts fresh from that preset and does not inherit the\n * top-level explicit levers. Poll `jobs.get(jobId)`.\n *\n * `word-highlight` shows ONE held line at a time and a word's\n * `startMs`/`endMs` is its SPOKEN window (what times the highlight, not how\n * long the text is on screen) — so `captions[]` can be handed over verbatim\n * from an `audio.transcribe()` job's `output_data.words`, with\n * `autoTranscribe: false`.\n */\n addCaptions(input: AddCaptionsInput): Promise<{ jobId: string }> {\n const { autoTranscribe, transcribeProvider, ...rest } = input\n // The route body is camelCase except these two flags (worker snake_case);\n // omit each when unset so an absent flag stays absent on the wire.\n const body: Record<string, unknown> = {\n ...rest,\n ...(autoTranscribe !== undefined ? { auto_transcribe: autoTranscribe } : {}),\n ...(transcribeProvider !== undefined ? { transcribe_provider: transcribeProvider } : {}),\n }\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/add-captions\", { body })\n }\n\n /**\n * Trim (and extract) audio from a video or audio source\n * (`POST /v1/trim-audio`) to `[startTime, endTime]` seconds, in `audioFormat`\n * (`mp3` default / `wav` / `aac`). Poll `jobs.get(jobId)`.\n */\n trimAudio(input: {\n videoUrl?: string\n audioUrl?: string\n audioFormat?: \"mp3\" | \"wav\" | \"aac\"\n startTime?: number\n endTime?: number\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/trim-audio\", { body: input })\n }\n\n /**\n * Cut a video and/or an audio file into consecutive parts\n * (`POST /v1/split-media`). Every part is at most `chunkDuration` seconds and\n * at least `minPartDuration` (default 4, never above `chunkDuration`) — a\n * short leftover is evened out with the parts before it. Video is cut on\n * whole frames with each part's sound matching its picture; audio parts are\n * sample-exact, in `audioFormat` (`mp3` default / `wav` / `aac`). With both\n * sources each is split on its own. At most 200 parts per input. Poll\n * `jobs.get(jobId)`; the parts are on the completed job's `output_data` —\n * see {@link SplitMediaOutput}.\n */\n splitMedia(input: SplitMediaInput): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/split-media\", { body: input })\n }\n\n /**\n * Turn one still image + one audio track into an MP4\n * (`POST /v1/still-to-video`) — locally rendered (FFmpeg), no AI model,\n * zero credits. The output duration IS the audio's duration; there is no\n * duration field. Optional `motion` animates the still (zoom / pan /\n * ken-burns) at `intensity` 1–10. `fit: \"contain\"` letterboxes with\n * `padColor` instead of cropping. Poll `jobs.get(jobId)`.\n */\n stillToVideo(input: {\n imageUrl: string\n audioUrl: string\n motion?: \"none\" | \"zoom-in\" | \"zoom-out\" | \"pan-left\" | \"pan-right\" | \"ken-burns\"\n intensity?: number\n resolution?: \"720p\" | \"1080p\" | \"4K\"\n aspectRatio?: \"16:9\" | \"9:16\" | \"1:1\" | \"4:3\"\n fps?: 24 | 30\n fit?: \"cover\" | \"contain\"\n padColor?: string\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/still-to-video\", { body: input })\n }\n\n /**\n * Turn 2–100 images + one optional audio track into an MP4 slideshow\n * (`POST /v1/slideshow`) — locally rendered (FFmpeg), zero credits. With\n * audio, the output duration IS the audio's duration (equal split unless\n * `imageDurations` pins rows — null entries = auto; mismatched pinned sums\n * scale proportionally and the factor is disclosed in the job output).\n * Without audio: N × `perImageDuration`, silent output. Transitions\n * consume the outgoing slide, so totals stay exact. For a single image use\n * `stillToVideo`. Poll `jobs.get(jobId)`.\n */\n slideshow(input: {\n imageUrls: string[]\n audioUrl?: string\n imageDurations?: Array<number | null>\n perImageDuration?: number\n transition?: string\n transitionDuration?: number\n motion?: \"none\" | \"zoom-in\" | \"zoom-out\" | \"ken-burns\" | \"alternate\"\n intensity?: number\n resolution?: \"720p\" | \"1080p\" | \"4K\"\n aspectRatio?: \"16:9\" | \"9:16\" | \"1:1\" | \"4:3\"\n fps?: 24 | 30\n fit?: \"cover\" | \"contain\"\n padColor?: string\n }): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/slideshow\", { body: input })\n }\n\n /**\n * Place 1–20 timed image layers over a video (`POST /v1/video-overlay`) —\n * rendered locally (FFmpeg) in one pass, no AI; the base audio is kept\n * untouched. Each layer has an `imageUrl`, a `start` and an optional `end`\n * in seconds (no `end` = to the end of the video) and a placement: a\n * `preset` (`\"card\"`, `\"corner-badge\"` with a `corner`, `\"full-frame\"`) or\n * an explicit box in {@link MediaResource.imageOverlay}'s percent vocabulary\n * (`anchor`, `x`, `y`, `width`, `height`, `fit`) — an explicit box field\n * overrides the preset, and a layer with neither is a corner badge\n * (bottom-right, or the `corner` it names). `outputAspect` renders onto a\n * 16:9 / 9:16 / 1:1 / 4:5 canvas\n * (`baseFit` cover by default; `backgroundColor` pads `contain`). Poll\n * `jobs.get(jobId)`: the output carries `videoUrl`, `thumbnailUrl`, `width`,\n * `height`, `durationSec` and `warnings[]` (a layer clipped or skipped at the\n * video's end, an animated image's first frame, re-encoded audio).\n */\n videoOverlay(input: VideoOverlayRequest): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/video-overlay\", { body: input })\n }\n\n /**\n * Track a subject described in plain text through a video and get a\n * black-and-white MASK VIDEO back (`POST /v1/video-mask`, SAM 3.1 by default,\n * or `provider: \"sam-3\"` at half the price) — white = the subject. Clips up\n * to 5 minutes at 30 fps (a faster clip counts\n * proportionally longer), priced by the clip's length. Poll\n * `jobs.get(jobId)`: the output carries `videoUrl` (the mask), `width`,\n * `height`, `frames`, `coverage` (the fraction of pixels selected, rounded\n * to 4 decimals) and `kept` (any region left on any frame: false only when\n * the size / zone filters left an empty mask — read it rather than\n * `coverage`, which a few pixels round to 0), and `recipe` (the version of\n * the masking algorithm that made it, `VIDEO_MASK_RECIPE`: it changes\n * whenever the same clip and settings would give a different mask). Feed the mask to\n * {@link MediaResource.maskComposite}.\n *\n * `holdLastMask: true` keeps the last mask on frames where the subject is\n * lost (a mask covering under 0.002 % of the frame, e.g. a face behind a\n * hand): each such frame after the first detection repeats the most recent\n * non-empty one, for at most 1 s of frames (`VIDEO_MASK_LIMITS.HOLD_MAX_SEC`;\n * longer gaps stay empty after that); the output then also carries `heldFrames`.\n * `minSubjectHeightPct` (0–100, default 0 = off) drops every connected\n * region whose bounding box is shorter than that % of the frame height\n * (extras' small faces behind the main subject); it runs before\n * `holdLastMask`, and the output then carries `droppedRegions`.\n * `maxSubjectHeightPct` (0–100, default 0 = off) is its mirror: it drops\n * regions TALLER than that % (keep only the small background faces). With\n * both set a region is kept when min ≤ height ≤ max; min > max is a 400.\n * `zone` (`{ leftPct, rightPct, topPct, bottomPct }`, each 0–49 % of the\n * frame, a margin left out = 0; default null = off) keeps only the regions\n * whose bounding-box centre lies inside the frame minus those margins (after\n * the size cuts, before `holdLastMask`). `perShot: true` finds the clip's\n * scene cuts first and segments each shot on its own: the subject is\n * detected again after every cut, `holdLastMask` never crosses a cut, and\n * each shot's mask is fitted to that shot's exact frame count; on a clip\n * with no cut it gives the same mask, fitted to the clip's exact frame\n * count. The output carries `shots` (1 when off). `redetectEverySec` (0 =\n * off, the default, or 1–10) also detects the subject again every that\n * many seconds — the clip (each shot, with `perShot`) is sent to the model\n * in pieces of at most that length, a last piece under 8 frames joining the\n * one before — so someone who walks in or turns to the camera mid-shot is\n * masked from the next boundary on. These boundaries are not cuts:\n * `holdLastMask` holds across them. Each piece is one more model request\n * (three run at a time), so it is slower; the price does not change. The\n * output's `chunks` counts the requests.\n */\n videoMask(input: VideoMaskRequest): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/video-mask\", { body: input })\n }\n\n /**\n * Place a foreground video over a background video through a mask video\n * (`POST /v1/mask-composite`; white = foreground) — local FFmpeg, no AI.\n * The result follows the foreground's size, frame rate and length; the\n * background is fitted (`backgroundFit`, default cover) and loops when\n * shorter; the mask holds its last frame when shorter. Optional\n * `invertMask`, `feather` (0–20 px), `grow` (−10–10 px), `opacity` (0–1,\n * default 1: how strongly the foreground covers the background inside the\n * mask) and `audioSource` (foreground / background / mix / none). Priced per started 5 seconds of\n * the result (the video it follows up to 300 s). Poll `jobs.get(jobId)` for `videoUrl`.\n *\n * `fill` picks what fills the masked area: `video` (default — the\n * foreground, required then), `pixelate` (the background itself in\n * blocks of `pixelSizePct` % of the frame width, 0.2–10, default 1.25) or\n * `blur` (the background blurred, sigma `blurStrengthPct` % of the frame\n * width, 0.1–5, default 2). Pixelate and blur take no\n * `foregroundUrl` (one sent is ignored): the result follows the background\n * and is priced on its length, and it always carries the background's sound\n * (`audioSource` is ignored).\n */\n maskComposite(input: MaskCompositeRequest): Promise<{ jobId: string }> {\n return this.client.request<{ jobId: string }>(\"POST\", \"/v1/mask-composite\", { body: input })\n }\n\n /**\n * Probe a social video's metadata (`POST /v1/video-metadata`) — duration,\n * dimensions, title, live status — WITHOUT downloading it. A direct read, not a\n * job. Use it to decide whether to trim before importing.\n */\n videoMetadata(input: { url: string }): Promise<VideoMetadata> {\n return this.client.request<VideoMetadata>(\"POST\", \"/v1/video-metadata\", { body: input })\n }\n\n /**\n * Cut or crop a stored file (`POST /v1/media/process`) — synchronous and\n * free, the source-preparation sibling of the priced `trimVideo` node.\n * `trim` is `[startTime, endTime]` in seconds; `deleteSource: true` removes\n * the source object afterwards when it is yours and nothing else references\n * it (\"the cut replaces the original\"). Answers the stored file. The\n * platform may take up to 300 s on a long file: pass `timeoutMs` to wait\n * longer than the client's default.\n */\n process(\n input: MediaProcessInput,\n options: { timeoutMs?: number; signal?: AbortSignal } = {},\n ): Promise<{ data: MediaProcessResult }> {\n return this.client.request<{ data: MediaProcessResult }>(\"POST\", \"/v1/media/process\", { body: input, ...options })\n }\n\n /**\n * Throw away a file you just made and no longer need (`POST /v1/media/discard`):\n * a link download replaced before use, a cut superseded by a newer one, a\n * picture imported then cancelled. Not a general delete. The file goes only\n * when it is your own, was made by a link download (`downloadVideo`), a cut\n * (`process`) or a URL import, is under 24 hours old, and no job of yours\n * uses it (as an output or as an input such as a run's `videoUrl`);\n * otherwise it is left untouched and the call fails with 409\n * `not_discardable`. `discarded: false` means there was nothing of yours at\n * that address (already gone, or not yours).\n */\n discard(input: { url: string }): Promise<{ data: MediaDiscardResult }> {\n return this.client.request<{ data: MediaDiscardResult }>(\"POST\", \"/v1/media/discard\", { body: input })\n }\n}\n\n/**\n * One event from {@link MediaResource.downloadVideoProgress}. The stream ends\n * after a `completed` event (which carries the stored `videoUrl` + an optional\n * `thumbnailUrl`) or a `failed` event (which carries `error`).\n */\nexport interface DownloadVideoProgress {\n phase: \"downloading\" | \"processing\" | \"uploading\" | \"completed\" | \"failed\"\n /** Download percent (0–100). Section fetches report jumpy percents — display, don't sum. */\n percent: number\n /** The imported video's storage URL — set on the `completed` event. */\n videoUrl?: string\n /** Thumbnail storage URL — set on the `completed` event when one was captured. */\n thumbnailUrl?: string\n /** What went wrong — set on the `failed` event. */\n error?: string\n}\n\n/** Result of {@link MediaResource.videoMetadata}. Fields are best-effort — a probe may omit some. */\nexport interface VideoMetadata {\n durationSec?: number | null\n width?: number | null\n height?: number | null\n title?: string | null\n isLive?: boolean\n [key: string]: unknown\n}\n\n/** Body of {@link MediaResource.splitMedia} (`POST /v1/split-media`). At least one of `videoUrl` / `audioUrl`. */\nexport interface SplitMediaInput {\n /** Video to split. */\n videoUrl?: string\n /** Audio file to split (split on its own when a video is given too). */\n audioUrl?: string\n /** The longest a part may be, in seconds (at least 1). Every part is at most this long. */\n chunkDuration: number\n /** The shortest a part may be, in seconds (0 or more, default 4). Must not exceed `chunkDuration` (400 otherwise). */\n minPartDuration?: number\n /** Format of the audio parts. Default `mp3`. */\n audioFormat?: \"mp3\" | \"wav\" | \"aac\"\n}\n\n/** A completed split-media job's `output_data`. */\nexport interface SplitMediaOutput {\n /** The video parts, in order (when a video was split). */\n videoUrls?: string[]\n /** The audio parts, in order (when an audio file was split). */\n audioUrls?: string[]\n /** The number of parts — the larger of the two lists. */\n chunkCount: number\n /** Each part's exact length in seconds: the video parts' when a video was split, else the audio parts'. */\n partDurationsSec?: number[]\n /** The audio parts' lengths, only when BOTH a video and an audio file were split. */\n audioPartDurationsSec?: number[]\n}\n\nexport interface MediaProcessInput {\n sourceUrl: string\n type: \"video\" | \"audio\"\n trim?: { startTime: number; endTime: number }\n crop?: { x: number; y: number; width: number; height: number }\n format?: \"mp4\" | \"webm\" | \"mp3\" | \"wav\" | \"m4a\" | \"aac\"\n deleteSource?: boolean\n}\n\n/** Result of {@link MediaResource.discard}. */\nexport interface MediaDiscardResult {\n /** True when the file was deleted; false when there was nothing of yours at that address. */\n discarded: boolean\n}\n\nexport interface MediaProcessResult {\n url: string\n thumbnailUrl: string | null\n assetId: string | null\n sizeBytes: number\n mimeType: string\n metadata: Record<string, unknown>\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { throwFromResponse, NodaroError } from './chunk-
|
|
1
|
+
import { throwFromResponse, NodaroError } from './chunk-4VP2GFIZ.js';
|
|
2
2
|
|
|
3
3
|
// src/sse.ts
|
|
4
4
|
async function* readSseStream(res, opts = {}) {
|
|
@@ -42,5 +42,5 @@ async function* readSseStream(res, opts = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export { readSseStream };
|
|
45
|
-
//# sourceMappingURL=chunk-
|
|
46
|
-
//# sourceMappingURL=chunk-
|
|
45
|
+
//# sourceMappingURL=chunk-4FSWJTRS.js.map
|
|
46
|
+
//# sourceMappingURL=chunk-4FSWJTRS.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sse.ts"],"names":[],"mappings":";;;AAyBA,gBAAuB,aAAA,CACrB,GAAA,EACA,IAAA,GAGI,EAAC,EAC+B;AACpC,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,IAAI,UAAmC,EAAC;AACxC,IAAA,IAAI;AACF,MAAA,OAAA,GAAW,MAAM,IAAI,IAAA,EAAK;AAAA,IAC5B,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,iBAAA,CAAkB,GAAA,CAAI,QAAQ,OAAO,CAAA;AAAA,EACvC;AACA,EAAA,IAAI,CAAC,IAAI,IAAA,EAAM;AACb,IAAA,MAAM,IAAI,YAAY,CAAA,EAAG,IAAA,CAAK,SAAS,cAAc,CAAA,qBAAA,CAAA,EAAyB,cAAA,EAAgB,GAAA,CAAI,MAAM,CAAA;AAAA,EAC1G;AAEA,EAAA,MAAM,MAAA,GAAS,GAAA,CAAI,IAAA,CAAK,SAAA,EAAU;AAClC,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,IAAI;AACF,IAAA,WAAS;AACP,MAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,MAAA,IAAI,IAAA,EAAM;AACV,MAAA,MAAA,IAAU,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAChD,MAAA,IAAI,GAAA;AACJ,MAAA,OAAA,CAAQ,GAAA,GAAM,MAAA,CAAO,OAAA,CAAQ,MAAM,MAAM,CAAA,EAAG;AAC1C,QAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AACjC,QAAA,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AAC7B,QAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,EAAG;AACpC,UAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG;AAC/B,UAAA,IAAI;AACF,YAAA,MAAM,KAAK,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC,CAAA,CAAE,MAAM,CAAA;AAAA,UACvC,CAAA,CAAA,MAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA,SAAE;AAEA,IAAA,MAAA,CAAO,WAAA,EAAY;AACnB,IAAA,MAAM,GAAA,CAAI,IAAA,CAAK,MAAA,EAAO,CAAE,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EACxC;AACF","file":"chunk-
|
|
1
|
+
{"version":3,"sources":["../src/sse.ts"],"names":[],"mappings":";;;AAyBA,gBAAuB,aAAA,CACrB,GAAA,EACA,IAAA,GAGI,EAAC,EAC+B;AACpC,EAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,IAAA,IAAI,UAAmC,EAAC;AACxC,IAAA,IAAI;AACF,MAAA,OAAA,GAAW,MAAM,IAAI,IAAA,EAAK;AAAA,IAC5B,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,iBAAA,CAAkB,GAAA,CAAI,QAAQ,OAAO,CAAA;AAAA,EACvC;AACA,EAAA,IAAI,CAAC,IAAI,IAAA,EAAM;AACb,IAAA,MAAM,IAAI,YAAY,CAAA,EAAG,IAAA,CAAK,SAAS,cAAc,CAAA,qBAAA,CAAA,EAAyB,cAAA,EAAgB,GAAA,CAAI,MAAM,CAAA;AAAA,EAC1G;AAEA,EAAA,MAAM,MAAA,GAAS,GAAA,CAAI,IAAA,CAAK,SAAA,EAAU;AAClC,EAAA,MAAM,OAAA,GAAU,IAAI,WAAA,EAAY;AAChC,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,IAAI;AACF,IAAA,WAAS;AACP,MAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,MAAA,IAAI,IAAA,EAAM;AACV,MAAA,MAAA,IAAU,QAAQ,MAAA,CAAO,KAAA,EAAO,EAAE,MAAA,EAAQ,MAAM,CAAA;AAChD,MAAA,IAAI,GAAA;AACJ,MAAA,OAAA,CAAQ,GAAA,GAAM,MAAA,CAAO,OAAA,CAAQ,MAAM,MAAM,CAAA,EAAG;AAC1C,QAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AACjC,QAAA,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,GAAA,GAAM,CAAC,CAAA;AAC7B,QAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,CAAM,KAAA,CAAM,IAAI,CAAA,EAAG;AACpC,UAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG;AAC/B,UAAA,IAAI;AACF,YAAA,MAAM,KAAK,KAAA,CAAM,IAAA,CAAK,MAAM,CAAC,CAAA,CAAE,MAAM,CAAA;AAAA,UACvC,CAAA,CAAA,MAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA,SAAE;AAEA,IAAA,MAAA,CAAO,WAAA,EAAY;AACnB,IAAA,MAAM,GAAA,CAAI,IAAA,CAAK,MAAA,EAAO,CAAE,MAAM,MAAM;AAAA,IAAC,CAAC,CAAA;AAAA,EACxC;AACF","file":"chunk-4FSWJTRS.js","sourcesContent":["import { NodaroError, throwFromResponse } from \"./errors.js\"\n\n/**\n * The ONE server-sent-event reader in this package.\n *\n * Every streaming route here writes the same minimal shape — `data: <json>\\n\\n`\n * frames, with the event kind INSIDE the JSON rather than on an `event:` line —\n * so one reader serves them all, and a second hand-rolled parse loop is the\n * thing this file exists to prevent. Deliberately minimal, matching the\n * server's writer rather than the whole SSE grammar: split on blank lines,\n * JSON-parse each `data:` line on its own, skip what does not parse (the next\n * frame re-reports state), and ignore every other line — keepalive comments\n * (`: keepalive`) included.\n *\n * Frames are separated by a bare `\\n\\n` and each `data:` line is parsed on its\n * own, exactly as the server writes them. Both halves are literal: a stream\n * whose frames arrive CRLF-terminated, or whose JSON was split across two\n * `data:` lines, yields NOTHING rather than erroring — so an intermediary that\n * normalises line endings would show up as an empty stream, not a parse\n * failure. The server's writer produces neither shape.\n *\n * No timeout is applied. The caller owns the lifetime: pass the `AbortSignal`\n * to the `fetch` that produced `res`, or just stop iterating — the `finally`\n * cancels the body, which ends the HTTP request.\n */\nexport async function* readSseStream<T>(\n res: Response,\n opts: {\n /** Names the stream in the \"no response body\" error. */\n label?: string\n } = {},\n): AsyncGenerator<T, void, undefined> {\n if (!res.ok) {\n let errBody: Record<string, unknown> = {}\n try {\n errBody = (await res.json()) as Record<string, unknown>\n } catch {\n // Empty/non-JSON body — fall through with empty errBody\n }\n throwFromResponse(res.status, errBody)\n }\n if (!res.body) {\n throw new NodaroError(`${opts.label ?? \"event stream\"} has no response body`, \"empty_stream\", res.status)\n }\n\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = \"\"\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n let sep: number\n while ((sep = buffer.indexOf(\"\\n\\n\")) >= 0) {\n const frame = buffer.slice(0, sep)\n buffer = buffer.slice(sep + 2)\n for (const line of frame.split(\"\\n\")) {\n if (!line.startsWith(\"data:\")) continue\n try {\n yield JSON.parse(line.slice(5).trim()) as T\n } catch {\n // Skip malformed frames — the next tick re-reports full state.\n }\n }\n }\n }\n } finally {\n // Ends the HTTP request when the consumer breaks out of the loop early.\n reader.releaseLock()\n await res.body.cancel().catch(() => {})\n }\n}\n"]}
|