@decartai/sdk 0.2.1 → 0.2.3

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/README.md CHANGED
@@ -82,6 +82,27 @@ Options:
82
82
  - `"auto"` — mirror when the input track reports `facingMode: "user"` (mobile front cameras).
83
83
  - `true` — always mirror (e.g. desktop webcams).
84
84
 
85
+ #### Fast mode
86
+
87
+ Fast mode (`speed: "fast"`) serves the session from a higher-compute tier for lower latency and higher throughput; output quality is unchanged. It is currently available for `lucy-2.5` / `lucy-latest` and `lucy-vton-3.5` / `lucy-vton-latest`, in the US region only, and is billed at 2x the standard realtime rate for those models. Other models ignore the option. Omit it (the default) for standard mode.
88
+
89
+ ```ts
90
+ const realtimeClient = await client.realtime.connect(stream, {
91
+ model: models.realtime("lucy-2.5"),
92
+ speed: "fast",
93
+ // ...
94
+ });
95
+ ```
96
+
97
+ Models that offer fast mode list it in their definition's `supportedSpeeds`:
98
+
99
+ ```ts
100
+ models.realtime("lucy-2.5").supportedSpeeds; // ["fast"]
101
+ models.realtime("lucy-2.1").supportedSpeeds; // undefined
102
+ ```
103
+
104
+ Passing `speed` for a model without the capability logs a warning through the client's logger and the session runs at standard speed.
105
+
85
106
  ### Watch a Stream
86
107
 
87
108
  A connected realtime session exposes an SDK `subscribeToken` once it reaches a
@@ -269,6 +290,38 @@ if (status.status === "completed") {
269
290
  }
270
291
  ```
271
292
 
293
+ ### Client Tokens
294
+
295
+ Mint short-lived client tokens on your server instead of shipping your API key.
296
+ The response also carries a signed JWT (`token`) that any server or edge runtime
297
+ can verify **offline** against the platform's public JWKS — no API key, no
298
+ round-trip to Decart:
299
+
300
+ ```ts
301
+ import { createDecartClient, verifyClientToken, decodeClientToken } from "@decartai/sdk";
302
+
303
+ const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
304
+ const { apiKey, token } = await client.tokens.create({ expiresIn: 300, metadata: { service_tier: 0 } });
305
+
306
+ // Verify offline (EdDSA signature, exp, iss, aud) and read the claims
307
+ const claims = await verifyClientToken(token); // or client.tokens.verify(token)
308
+ claims.serviceTier; // 0
309
+ claims.pool; // "free" for tier 0, otherwise "paid"
310
+ // also: userId, organizationId, apiKeyName, apiKeyId, allowedModels, allowedOrigins,
311
+ // expiresAt, realtimeConcurrentSessionLimit, zeroDataRetention, attribution, raw
312
+
313
+ // Read claims WITHOUT verifying — no network, no crypto. Untrusted: display/logging only.
314
+ const untrusted = decodeClientToken(token);
315
+ ```
316
+
317
+ The JWKS comes from `https://platform.decart.ai/api/auth/jwks` (the platform
318
+ host, not `baseUrl`) and is cached; the JWT library is loaded lazily so bundles
319
+ that never verify do not ship it. Errors: `TOKEN_INVALID`, `TOKEN_EXPIRED`,
320
+ `TOKEN_VERIFY_ERROR` (JWKS unreachable). Options: `{ jwksUrl, issuer, audience,
321
+ clockTolerance }`. This is an offline check, not an API call. It needs WebCrypto: on
322
+ runtimes without it (React Native) `verify` rejects with `UNSUPPORTED_PLATFORM_FEATURE`;
323
+ `decodeClientToken` works everywhere.
324
+
272
325
  ### React Native / Expo
273
326
 
274
327
  React Native realtime requires [LiveKit's React Native packages](https://github.com/livekit/client-sdk-react-native)
@@ -175,7 +175,7 @@ const createDecartClientForPlatform = (createRealtime, options = {}) => {
175
175
  */
176
176
  queue,
177
177
  /**
178
- * Client for creating client tokens.
178
+ * Client for creating, verifying and decoding client tokens.
179
179
  * Client tokens are short-lived API keys safe for client-side use.
180
180
  *
181
181
  * @example
@@ -183,7 +183,11 @@ const createDecartClientForPlatform = (createRealtime, options = {}) => {
183
183
  * // Server-side: Create a client token
184
184
  * const serverClient = createDecartClient({ apiKey: process.env.DECART_API_KEY });
185
185
  * const token = await serverClient.tokens.create();
186
- * // Returns: { apiKey: "ek_...", expiresAt: "2024-12-15T12:10:00Z" }
186
+ * // Returns: { apiKey: "ek_...", token: "eyJ...", expiresAt: "2024-12-15T12:10:00Z" }
187
+ *
188
+ * // Server-side: Verify a client token offline (JWKS) and read its claims
189
+ * const claims = await serverClient.tokens.verify(token.token);
190
+ * console.log(claims.serviceTier, claims.pool, claims.organizationId);
187
191
  *
188
192
  * // Client-side: Use the client token
189
193
  * const client = createDecartClient({ apiKey: token.apiKey });
@@ -12,6 +12,7 @@ import { ReactNativeFile } from "../process/types.js";
12
12
  interface FileReference {
13
13
  id: string;
14
14
  filename: string | null;
15
+ /** Type of the stored file. The API re-encodes non-JPEG images to JPEG, so a PNG upload reports `image/jpeg`. */
15
16
  mime_type: string;
16
17
  size_bytes: number;
17
18
  /**
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CanonicalModel, CustomModelDefinition, ImageModelDefinition, ImageModels, ListedModelDefinition, Model, ModelDefinition, ModelFps, ModelKind, RealTimeModels, VideoModelDefinition, VideoModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias } from "./shared/model.js";
1
+ import { CanonicalModel, CustomModelDefinition, ImageModelDefinition, ImageModels, ListedModelDefinition, Model, ModelDefinition, ModelFps, ModelKind, RealTimeModels, RealtimeSpeed, VideoModelDefinition, VideoModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias } from "./shared/model.js";
2
2
  import { FileInput, ProcessOptions, ReactNativeFile } from "./process/types.js";
3
3
  import { FileReference, FileUploadInput } from "./files/types.js";
4
4
  import { FilesClient, UploadFileOptions } from "./files/client.js";
@@ -17,6 +17,8 @@ import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientIni
17
17
  import { CheckConnectivityOptions, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport } from "./realtime/preflight-types.js";
18
18
  import { RealTimeSubscribeClient, SubscribeEvents, SubscribeOptions } from "./realtime/subscribe-client.js";
19
19
  import { ModelState } from "./shared/types.js";
20
+ import { ClientTokenClaims, decodeClientToken } from "./tokens/claims.js";
21
+ import { VerifyClientTokenOptions, verifyClientToken } from "./tokens/verify.js";
20
22
  import { CreateTokenOptions, CreateTokenResponse, TokensClient } from "./tokens/client.js";
21
23
  import { DecartClientOptions } from "./create-client.js";
22
24
 
@@ -33,4 +35,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
33
35
  files: FilesClient;
34
36
  };
35
37
  //#endregion
36
- export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
38
+ export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ClientTokenClaims, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, RealtimeSpeed, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VerifyClientTokenOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ERROR_CODES } from "./utils/errors.js";
2
+ import { decodeClientToken } from "./tokens/claims.js";
3
+ import { verifyClientToken } from "./tokens/verify.js";
2
4
  import { createConsoleLogger, noopLogger } from "./utils/logger.js";
3
5
  import { createDecartClientForPlatform } from "./create-client.js";
4
6
  import { isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias } from "./shared/model.js";
@@ -6,4 +8,4 @@ import { createBrowserRealtime } from "./realtime/browser/index.js";
6
8
  //#region src/index.ts
7
9
  const createDecartClient = (options = {}) => createDecartClientForPlatform(createBrowserRealtime, options);
8
10
  //#endregion
9
- export { ERROR_CODES, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
11
+ export { ERROR_CODES, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
@@ -1,4 +1,4 @@
1
- import { CanonicalModel, CustomModelDefinition, ImageModelDefinition, ImageModels, ListedModelDefinition, Model, ModelDefinition, ModelFps, ModelKind, RealTimeModels, VideoModelDefinition, VideoModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias } from "./shared/model.js";
1
+ import { CanonicalModel, CustomModelDefinition, ImageModelDefinition, ImageModels, ListedModelDefinition, Model, ModelDefinition, ModelFps, ModelKind, RealTimeModels, RealtimeSpeed, VideoModelDefinition, VideoModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias } from "./shared/model.js";
2
2
  import { FileInput, ProcessOptions, ReactNativeFile } from "./process/types.js";
3
3
  import { FileReference, FileUploadInput } from "./files/types.js";
4
4
  import { FilesClient, UploadFileOptions } from "./files/client.js";
@@ -17,6 +17,8 @@ import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientIni
17
17
  import { CheckConnectivityOptions, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport } from "./realtime/preflight-types.js";
18
18
  import { RealTimeSubscribeClient, SubscribeEvents, SubscribeOptions } from "./realtime/subscribe-client.js";
19
19
  import { ModelState } from "./shared/types.js";
20
+ import { ClientTokenClaims, decodeClientToken } from "./tokens/claims.js";
21
+ import { VerifyClientTokenOptions, verifyClientToken } from "./tokens/verify.js";
20
22
  import { CreateTokenOptions, CreateTokenResponse, TokensClient } from "./tokens/client.js";
21
23
  import { DecartClientOptions } from "./create-client.js";
22
24
 
@@ -33,4 +35,4 @@ declare const createDecartClient: (options?: DecartClientOptions) => {
33
35
  files: FilesClient;
34
36
  };
35
37
  //#endregion
36
- export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
38
+ export { CanonicalModel, CheckConnectivityOptions, ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, ClientTokenClaims, ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport, ConnectionState, ConnectivityMetrics, ConnectivityReport, ConnectivityTransport, CreateTokenOptions, CreateTokenResponse, CustomModelDefinition, type DecartClientOptions, DecartSDKError, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ERROR_CODES, FileInput, FileReference, FileUploadInput, FilesClient, G2GMetrics, GenerationEndedMessage, ImageModelDefinition, ImageModels, JobStatus, JobStatusResponse, JobSubmitResponse, ListedModelDefinition, LogLevel, Logger, Model, ModelDefinition, ModelFps, ModelKind, ModelState, ProcessClient, ProcessOptions, QueueClient, QueueJobResult, QueuePosition, QueuePositionMessage, QueueSubmitAndPollOptions, QueueSubmitOptions, ReactNativeFile, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, Events as RealTimeEvents, RealTimeModels, RealTimeSubscribeClient, RealtimeMediaStream, RealtimeSpeed, ReconnectEvent, SessionEnded, SetInput, SubscribeEvents, SubscribeOptions, TokensClient, UploadFileOptions, VerifyClientTokenOptions, VideoModelDefinition, VideoModels, VideoStallEvent, WebRTCStats, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
@@ -1,4 +1,6 @@
1
1
  import { ERROR_CODES } from "./utils/errors.js";
2
+ import { decodeClientToken } from "./tokens/claims.js";
3
+ import { verifyClientToken } from "./tokens/verify.js";
2
4
  import { createConsoleLogger, noopLogger } from "./utils/logger.js";
3
5
  import { createDecartClientForPlatform } from "./create-client.js";
4
6
  import { isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias } from "./shared/model.js";
@@ -6,4 +8,4 @@ import { createReactNativeRealtime } from "./realtime/react-native/index.js";
6
8
  //#region src/index.react-native.ts
7
9
  const createDecartClient = (options = {}) => createDecartClientForPlatform(createReactNativeRealtime, options);
8
10
  //#endregion
9
- export { ERROR_CODES, createConsoleLogger, createDecartClient, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias };
11
+ export { ERROR_CODES, createConsoleLogger, createDecartClient, decodeClientToken, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, models, noopLogger, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, verifyClientToken };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.2.1";
2
+ var version = "0.2.3";
3
3
  //#endregion
4
4
  export { version };
@@ -36,6 +36,9 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
36
36
  width: z.ZodNumber;
37
37
  height: z.ZodNumber;
38
38
  inputSchema: z.ZodOptional<z.ZodAny>;
39
+ supportedSpeeds: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodEnum<{
40
+ fast: "fast";
41
+ }>>>>;
39
42
  }, z.core.$strip>;
40
43
  onRemoteStream: z.ZodCustom<OnRemoteStreamFn, OnRemoteStreamFn>;
41
44
  onConnectionChange: z.ZodOptional<z.ZodCustom<OnConnectionChangeFn, OnConnectionChangeFn>>;
@@ -55,6 +58,9 @@ declare const realTimeClientConnectOptionsSchema: z.ZodObject<{
55
58
  "720p": "720p";
56
59
  "1080p": "1080p";
57
60
  }>>;
61
+ speed: z.ZodOptional<z.ZodEnum<{
62
+ fast: "fast";
63
+ }>>;
58
64
  preferredVideoCodec: z.ZodOptional<z.ZodEnum<{
59
65
  h264: "h264";
60
66
  vp8: "vp8";
@@ -1,7 +1,7 @@
1
1
  import { classifyWebrtcError } from "../utils/errors.js";
2
2
  import { createConsoleLogger } from "../utils/logger.js";
3
3
  import { isFileRefId } from "../files/types.js";
4
- import { modelDefinitionSchema, resolveFpsNumber } from "../shared/model.js";
4
+ import { modelDefinitionSchema, realtimeSpeedSchema, resolveFpsNumber } from "../shared/model.js";
5
5
  import { modelStateSchema } from "../shared/types.js";
6
6
  import { imageToBase64 } from "../utils/media.js";
7
7
  import { createEventBuffer } from "./event-buffer.js";
@@ -20,6 +20,14 @@ const realTimeClientConnectOptionsSchema = z.object({
20
20
  queryParams: z.record(z.string(), z.string()).optional(),
21
21
  mirror: z.union([z.literal("auto"), z.boolean()]).optional(),
22
22
  resolution: z.enum(["720p", "1080p"]).optional(),
23
+ /**
24
+ * Realtime speed tier. Fast mode (`"fast"`) serves the session from a higher-compute tier for lower
25
+ * latency and higher throughput; output quality is unchanged. It is currently available for `lucy-2.5` /
26
+ * `lucy-latest` and `lucy-vton-3.5` / `lucy-vton-latest`, in the US region only, and is billed at 2x the
27
+ * standard realtime rate for those models. Other models ignore the option (the SDK logs a warning when the
28
+ * model does not list it in `supportedSpeeds`). Omit it (the default) for standard mode.
29
+ */
30
+ speed: realtimeSpeedSchema.optional(),
23
31
  /** Local track publish codec. Desktop Safari is always pinned to vp8 and ignores this value. */
24
32
  preferredVideoCodec: z.enum([
25
33
  "h264",
@@ -39,7 +47,7 @@ const createRealTimeClient = (opts) => {
39
47
  const connect = async (stream, options) => {
40
48
  const parsedOptions = realTimeClientConnectOptionsSchema.safeParse(options);
41
49
  if (!parsedOptions.success) throw parsedOptions.error;
42
- const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, preferredVideoCodec } = parsedOptions.data;
50
+ const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, speed, preferredVideoCodec } = parsedOptions.data;
43
51
  const mirror = parsedOptions.data.mirror ?? false;
44
52
  let session;
45
53
  let observability;
@@ -74,13 +82,19 @@ const createRealTimeClient = (opts) => {
74
82
  }
75
83
  });
76
84
  observability = preparedConnection.observability;
85
+ if (speed && !options.model.supportedSpeeds?.includes(speed)) logger.warn("realtime: model does not support the requested speed tier; the server serves it at standard speed", {
86
+ model: options.model.name,
87
+ speed,
88
+ supportedSpeeds: options.model.supportedSpeeds ?? []
89
+ });
77
90
  session = new StreamSession({
78
91
  url: `${url}?${new URLSearchParams({
79
92
  ...preparedConnection.queryParams ?? {},
80
93
  ...options.queryParams ?? {},
81
94
  api_key: apiKey,
82
95
  model: options.model.name,
83
- ...resolution ? { resolution } : {}
96
+ ...resolution ? { resolution } : {},
97
+ ...speed ? { speed } : {}
84
98
  }).toString()}`,
85
99
  integration,
86
100
  observability,
@@ -289,6 +289,14 @@ type ModelFps = number | {
289
289
  ideal?: number;
290
290
  exact?: number;
291
291
  };
292
+ /**
293
+ * Realtime speed tiers selectable with the `speed` connect option.
294
+ * `"fast"` is the only tier today; the standard tier has no value (omit the option).
295
+ */
296
+ declare const realtimeSpeedSchema: z.ZodEnum<{
297
+ fast: "fast";
298
+ }>;
299
+ type RealtimeSpeed = z.infer<typeof realtimeSpeedSchema>;
292
300
  type ModelDefinition<T extends Model = Model> = {
293
301
  name: T;
294
302
  urlPath: string;
@@ -297,6 +305,11 @@ type ModelDefinition<T extends Model = Model> = {
297
305
  width: number;
298
306
  height: number;
299
307
  inputSchema: T extends keyof ModelInputSchemas ? ModelInputSchemas[T] : z.ZodTypeAny;
308
+ /**
309
+ * Realtime speed tiers this model can be served from via the `speed` connect option.
310
+ * Absent when the model only offers the standard tier (the server ignores `speed` for it).
311
+ */
312
+ supportedSpeeds?: readonly RealtimeSpeed[];
300
313
  };
301
314
  declare function resolveFpsNumber(fps: ModelFps, fallback?: number): number;
302
315
  /**
@@ -375,4 +388,4 @@ declare const models: {
375
388
  };
376
389
  };
377
390
  //#endregion
378
- export { CanonicalModel, CustomModelDefinition, ImageModelDefinition, ImageModels, ListedModelDefinition, Model, ModelDefinition, ModelFps, ModelInputSchemas, ModelKind, RealTimeModels, VideoModelDefinition, VideoModels, canonicalModelSchema, imageModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, modelInputSchemas, modelSchema, models, realtimeModels, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, videoModels };
391
+ export { CanonicalModel, CustomModelDefinition, ImageModelDefinition, ImageModels, ListedModelDefinition, Model, ModelDefinition, ModelFps, ModelInputSchemas, ModelKind, RealTimeModels, RealtimeSpeed, VideoModelDefinition, VideoModels, canonicalModelSchema, imageModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, modelInputSchemas, modelSchema, models, realtimeModels, realtimeSpeedSchema, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, videoModels };
@@ -185,6 +185,11 @@ const modelInputSchemas = {
185
185
  "lucy-pro-i2i": imageEditSchema,
186
186
  "lucy-restyle-v2v": restyleSchema
187
187
  };
188
+ /**
189
+ * Realtime speed tiers selectable with the `speed` connect option.
190
+ * `"fast"` is the only tier today; the standard tier has no value (omit the option).
191
+ */
192
+ const realtimeSpeedSchema = z.enum(["fast"]);
188
193
  function resolveFpsNumber(fps, fallback = 30) {
189
194
  if (typeof fps === "number") return fps;
190
195
  return fps.ideal ?? fps.max ?? fps.exact ?? fps.min ?? fallback;
@@ -201,7 +206,8 @@ const modelDefinitionSchema = z.object({
201
206
  })]),
202
207
  width: z.number().min(1),
203
208
  height: z.number().min(1),
204
- inputSchema: z.any().optional()
209
+ inputSchema: z.any().optional(),
210
+ supportedSpeeds: z.array(realtimeSpeedSchema).readonly().optional()
205
211
  });
206
212
  const _models = {
207
213
  realtime: {
@@ -225,7 +231,8 @@ const _models = {
225
231
  },
226
232
  width: 1280,
227
233
  height: 720,
228
- inputSchema: z.object({})
234
+ inputSchema: z.object({}),
235
+ supportedSpeeds: ["fast"]
229
236
  },
230
237
  "lucy-vton-3.5": {
231
238
  urlPath: "/v1/stream",
@@ -236,7 +243,8 @@ const _models = {
236
243
  },
237
244
  width: 1280,
238
245
  height: 720,
239
- inputSchema: z.object({})
246
+ inputSchema: z.object({}),
247
+ supportedSpeeds: ["fast"]
240
248
  },
241
249
  "lucy-restyle-2": {
242
250
  urlPath: "/v1/stream",
@@ -258,7 +266,8 @@ const _models = {
258
266
  },
259
267
  width: 1088,
260
268
  height: 624,
261
- inputSchema: z.object({})
269
+ inputSchema: z.object({}),
270
+ supportedSpeeds: ["fast"]
262
271
  },
263
272
  "lucy-vton-latest": {
264
273
  urlPath: "/v1/stream",
@@ -269,7 +278,8 @@ const _models = {
269
278
  },
270
279
  width: 1280,
271
280
  height: 720,
272
- inputSchema: z.object({})
281
+ inputSchema: z.object({}),
282
+ supportedSpeeds: ["fast"]
273
283
  },
274
284
  "lucy-restyle-latest": {
275
285
  urlPath: "/v1/stream",
@@ -483,4 +493,4 @@ const models = {
483
493
  }
484
494
  };
485
495
  //#endregion
486
- export { canonicalImageModels, canonicalModelSchema, canonicalRealtimeModels, canonicalVideoModels, imageModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, modelDefinitionSchema, modelInputSchemas, modelSchema, models, realtimeModels, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, videoModels };
496
+ export { canonicalImageModels, canonicalModelSchema, canonicalRealtimeModels, canonicalVideoModels, imageModels, isCanonicalModel, isImageModel, isModel, isRealtimeModel, isVideoModel, listModels, modelAliases, modelDefinitionSchema, modelInputSchemas, modelSchema, models, realtimeModels, realtimeSpeedSchema, resolveCanonicalModelAlias, resolveFpsNumber, resolveModelAlias, videoModels };
@@ -0,0 +1,44 @@
1
+ import { Model } from "../shared/model.js";
2
+
3
+ //#region src/tokens/claims.d.ts
4
+
5
+ /**
6
+ * Client-token claims mapped to SDK names. Trusted when returned by `verifyClientToken` /
7
+ * `client.tokens.verify`; UNTRUSTED when returned by `decodeClientToken` / `client.tokens.decode`.
8
+ */
9
+ type ClientTokenClaims = {
10
+ /** `metadata.service_tier` at mint time (`0` is the free tier); `null` when unset. */
11
+ serviceTier: number | null;
12
+ /** `"free"` when `serviceTier` is `0`, otherwise `"paid"`. */
13
+ pool: "free" | "paid";
14
+ /** `sub` — the owning user (the organization owner for org tokens). */
15
+ userId: string;
16
+ organizationId: string | null;
17
+ apiKeyName: string | null;
18
+ /** Parent permanent key id when present, else the ephemeral key's own id (`jti`). */
19
+ apiKeyId: string | null;
20
+ /** `null` = unrestricted. */
21
+ allowedModels: (Model | (string & {}))[] | null;
22
+ /** `null` = unrestricted. */
23
+ allowedOrigins: string[] | null;
24
+ /** ISO 8601, like `CreateTokenResponse.expiresAt`. */
25
+ expiresAt: string;
26
+ /** `null` when the owning key is exempt from the limit. */
27
+ realtimeConcurrentSessionLimit: number | null;
28
+ zeroDataRetention: boolean;
29
+ attribution: Record<string, string> | null;
30
+ /** The full JWT payload, for anything not mapped above. */
31
+ raw: Record<string, unknown>;
32
+ };
33
+ /** JOSE header prefix plus three segments; opaque `ek_*` / `dct_*` keys never match. */
34
+
35
+ /**
36
+ * Read a client token's claims WITHOUT verifying it — no network, no crypto, so it works everywhere
37
+ * including React Native. The result is UNTRUSTED: use it for display or logging, never for
38
+ * authorization (use `verifyClientToken` / `client.tokens.verify` for that).
39
+ *
40
+ * @throws `TOKEN_INVALID` when the string is not a JWT or the payload is malformed.
41
+ */
42
+ declare function decodeClientToken(token: string): ClientTokenClaims;
43
+ //#endregion
44
+ export { ClientTokenClaims, decodeClientToken };
@@ -0,0 +1,53 @@
1
+ import { ERROR_CODES, createSDKError } from "../utils/errors.js";
2
+ //#region src/tokens/claims.ts
3
+ /** JOSE header prefix plus three segments; opaque `ek_*` / `dct_*` keys never match. */
4
+ const isClientTokenJwt = (credential) => typeof credential === "string" && credential.startsWith("eyJ") && credential.split(".").length === 3;
5
+ const invalid = (message, data) => createSDKError(ERROR_CODES.TOKEN_INVALID, `Invalid client token: ${message}`, data);
6
+ /** An integer (not a boolean) or an integer string, else `null`. */
7
+ const parseServiceTier = (raw) => typeof raw === "number" && Number.isInteger(raw) ? raw : typeof raw === "string" && /^\s*[+-]?\d+\s*$/.test(raw) ? Number.parseInt(raw, 10) : null;
8
+ const str = (value) => typeof value === "string" ? value : null;
9
+ const strArray = (value) => Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
10
+ /** Map a raw JWT payload to {@link ClientTokenClaims}. Requires `sub` and `exp`. */
11
+ function mapClientTokenClaims(raw) {
12
+ if (typeof raw.sub !== "string" || !raw.sub) throw invalid("missing `sub` claim", { claim: "sub" });
13
+ if (typeof raw.exp !== "number") throw invalid("missing `exp` claim", { claim: "exp" });
14
+ const serviceTier = parseServiceTier(raw.service_tier) ?? (raw.priority ? 3 : null);
15
+ const attribution = raw.attribution;
16
+ return {
17
+ serviceTier,
18
+ pool: serviceTier === 0 ? "free" : "paid",
19
+ userId: raw.sub,
20
+ organizationId: str(raw.organizationId),
21
+ apiKeyName: str(raw.api_key_name),
22
+ apiKeyId: str(raw.parent_api_key_id) ?? str(raw.jti),
23
+ allowedModels: strArray(raw.models),
24
+ allowedOrigins: strArray(raw.origins),
25
+ expiresAt: (/* @__PURE__ */ new Date(raw.exp * 1e3)).toISOString(),
26
+ realtimeConcurrentSessionLimit: typeof raw.realtimeConcurrentSessionLimit === "number" ? raw.realtimeConcurrentSessionLimit : null,
27
+ zeroDataRetention: raw.zeroDataRetention === true,
28
+ attribution: typeof attribution === "object" && attribution !== null && !Array.isArray(attribution) ? attribution : null,
29
+ raw
30
+ };
31
+ }
32
+ /**
33
+ * Read a client token's claims WITHOUT verifying it — no network, no crypto, so it works everywhere
34
+ * including React Native. The result is UNTRUSTED: use it for display or logging, never for
35
+ * authorization (use `verifyClientToken` / `client.tokens.verify` for that).
36
+ *
37
+ * @throws `TOKEN_INVALID` when the string is not a JWT or the payload is malformed.
38
+ */
39
+ function decodeClientToken(token) {
40
+ if (!isClientTokenJwt(token)) throw invalid("not a JWT", { reason: "malformed" });
41
+ let payload;
42
+ try {
43
+ const base64 = token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/");
44
+ const utf8 = atob(base64).replace(/[\s\S]/g, (c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`);
45
+ payload = JSON.parse(decodeURIComponent(utf8));
46
+ } catch {
47
+ throw invalid("payload is not base64url JSON", { reason: "malformed" });
48
+ }
49
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) throw invalid("payload is not a JSON object", { reason: "malformed" });
50
+ return mapClientTokenClaims(payload);
51
+ }
52
+ //#endregion
53
+ export { decodeClientToken, isClientTokenJwt, mapClientTokenClaims };
@@ -1,4 +1,6 @@
1
1
  import { Model } from "../shared/model.js";
2
+ import { ClientTokenClaims } from "./claims.js";
3
+ import { VerifyClientTokenOptions } from "./verify.js";
2
4
 
3
5
  //#region src/tokens/client.d.ts
4
6
 
@@ -25,7 +27,7 @@ type CreateTokenOptions = {
25
27
  };
26
28
  type CreateTokenResponse = {
27
29
  apiKey: string;
28
- /** Signed JWT mirroring `apiKey`, verifiable offline via the public JWKS. */
30
+ /** Signed JWT mirroring `apiKey`, verifiable offline via `client.tokens.verify` / `verifyClientToken`. */
29
31
  token?: string;
30
32
  expiresAt: string;
31
33
  /** Present when `allowedModels` and/or `allowedOrigins` were set on the request. */
@@ -65,6 +67,17 @@ type TokensClient = {
65
67
  * ```
66
68
  */
67
69
  create: (options?: CreateTokenOptions) => Promise<CreateTokenResponse>;
70
+ /**
71
+ * Verify a client token's JWT OFFLINE against the platform's public JWKS (cached) and return its
72
+ * claims — signature (EdDSA), `exp`, `iss`, `aud`. Same as the standalone `verifyClientToken`;
73
+ * does not use the client's API key or `baseUrl`.
74
+ */
75
+ verify: (token: string, options?: VerifyClientTokenOptions) => Promise<ClientTokenClaims>;
76
+ /**
77
+ * Read a client token's claims WITHOUT verifying — no network, no crypto. UNTRUSTED: display or
78
+ * logging only. Same as the standalone `decodeClientToken`.
79
+ */
80
+ decode: (token: string) => ClientTokenClaims;
68
81
  };
69
82
  //#endregion
70
83
  export { CreateTokenOptions, CreateTokenResponse, TokensClient };
@@ -1,5 +1,7 @@
1
1
  import { createSDKError } from "../utils/errors.js";
2
2
  import { buildAuthHeaders } from "../shared/request.js";
3
+ import { decodeClientToken } from "./claims.js";
4
+ import { verifyClientToken } from "./verify.js";
3
5
  //#region src/tokens/client.ts
4
6
  const createTokensClient = (opts) => {
5
7
  const { baseUrl, apiKey, integration } = opts;
@@ -22,7 +24,11 @@ const createTokensClient = (opts) => {
22
24
  }
23
25
  return response.json();
24
26
  };
25
- return { create };
27
+ return {
28
+ create,
29
+ verify: verifyClientToken,
30
+ decode: decodeClientToken
31
+ };
26
32
  };
27
33
  //#endregion
28
34
  export { createTokensClient };
@@ -0,0 +1,26 @@
1
+ import { ClientTokenClaims } from "./claims.js";
2
+
3
+ //#region src/tokens/verify.d.ts
4
+ type VerifyClientTokenOptions = {
5
+ /** Default `https://platform.decart.ai/api/auth/jwks`. */
6
+ jwksUrl?: string;
7
+ /** Expected `iss`. Default `https://platform.decart.ai`. */
8
+ issuer?: string;
9
+ /** Expected `aud`. Default `https://platform.decart.ai`. */
10
+ audience?: string;
11
+ /** Allowed clock skew in seconds for `exp`. Default 60. */
12
+ clockTolerance?: number;
13
+ };
14
+ /**
15
+ * Verify a Decart client token OFFLINE and return its claims: EdDSA signature against the
16
+ * platform's public JWKS (fetched once and cached), plus `exp` (with clock tolerance), `iss` and
17
+ * `aud`. This is a local JWKS check, not an API call, and it needs no API key. Requires WebCrypto and `fetch` (Node 20+, Bun, Deno, browsers, Workers, Edge).
18
+ *
19
+ * @throws `TOKEN_INVALID` bad signature, wrong `iss`/`aud`, malformed token or unknown `kid`
20
+ * @throws `TOKEN_EXPIRED` `exp` is in the past
21
+ * @throws `TOKEN_VERIFY_ERROR` the JWKS could not be fetched — the token itself was not judged
22
+ * @throws `UNSUPPORTED_PLATFORM_FEATURE` the runtime has no WebCrypto (e.g. React Native)
23
+ */
24
+ declare function verifyClientToken(token: string, options?: VerifyClientTokenOptions): Promise<ClientTokenClaims>;
25
+ //#endregion
26
+ export { VerifyClientTokenOptions, verifyClientToken };
@@ -0,0 +1,53 @@
1
+ import { ERROR_CODES, createSDKError } from "../utils/errors.js";
2
+ import { isClientTokenJwt, mapClientTokenClaims } from "./claims.js";
3
+ //#region src/tokens/verify.ts
4
+ const PLATFORM_URL = "https://platform.decart.ai";
5
+ let jose;
6
+ const jwkSets = /* @__PURE__ */ new Map();
7
+ /**
8
+ * Verify a Decart client token OFFLINE and return its claims: EdDSA signature against the
9
+ * platform's public JWKS (fetched once and cached), plus `exp` (with clock tolerance), `iss` and
10
+ * `aud`. This is a local JWKS check, not an API call, and it needs no API key. Requires WebCrypto and `fetch` (Node 20+, Bun, Deno, browsers, Workers, Edge).
11
+ *
12
+ * @throws `TOKEN_INVALID` bad signature, wrong `iss`/`aud`, malformed token or unknown `kid`
13
+ * @throws `TOKEN_EXPIRED` `exp` is in the past
14
+ * @throws `TOKEN_VERIFY_ERROR` the JWKS could not be fetched — the token itself was not judged
15
+ * @throws `UNSUPPORTED_PLATFORM_FEATURE` the runtime has no WebCrypto (e.g. React Native)
16
+ */
17
+ async function verifyClientToken(token, options = {}) {
18
+ if (!isClientTokenJwt(token)) throw createSDKError(ERROR_CODES.TOKEN_INVALID, "Invalid client token: not a JWT", { reason: "malformed" });
19
+ if (!globalThis.crypto?.subtle) throw createSDKError(ERROR_CODES.UNSUPPORTED_PLATFORM_FEATURE, "verifyClientToken needs WebCrypto (crypto.subtle), which this runtime does not provide. Verify client tokens on your server; decodeClientToken works everywhere.", { feature: "verifyClientToken" });
20
+ const jwksUrl = options.jwksUrl ?? `${PLATFORM_URL}/api/auth/jwks`;
21
+ if (!jose) jose = import("jose");
22
+ const { jwtVerify, createRemoteJWKSet, errors } = await jose;
23
+ let jwkSet = jwkSets.get(jwksUrl);
24
+ if (!jwkSet) {
25
+ jwkSet = createRemoteJWKSet(new URL(jwksUrl), { cooldownDuration: 6e4 });
26
+ jwkSets.set(jwksUrl, jwkSet);
27
+ }
28
+ try {
29
+ const { payload } = await jwtVerify(token, jwkSet, {
30
+ algorithms: ["EdDSA"],
31
+ issuer: options.issuer ?? PLATFORM_URL,
32
+ audience: options.audience ?? PLATFORM_URL,
33
+ clockTolerance: options.clockTolerance ?? 60,
34
+ requiredClaims: ["exp", "sub"]
35
+ });
36
+ return mapClientTokenClaims(payload);
37
+ } catch (error) {
38
+ const cause = error instanceof Error ? error : void 0;
39
+ const { code = "unknown", claim } = cause ?? {};
40
+ const data = {
41
+ reason: code,
42
+ ...claim ? { claim } : {}
43
+ };
44
+ if (error instanceof errors.JWTExpired) throw createSDKError(ERROR_CODES.TOKEN_EXPIRED, `Client token expired: ${cause?.message}`, data, cause);
45
+ if (!(!(error instanceof errors.JOSEError) || /^ERR_(JOSE_GENERIC|JWKS_INVALID|JWKS_TIMEOUT)$/.test(code))) throw createSDKError(ERROR_CODES.TOKEN_INVALID, `Invalid client token: ${cause?.message}`, data, cause);
46
+ throw createSDKError(ERROR_CODES.TOKEN_VERIFY_ERROR, `Could not load the client-token JWKS from ${jwksUrl}: ${cause?.message ?? String(error)}`, {
47
+ ...data,
48
+ jwksUrl
49
+ }, cause);
50
+ }
51
+ }
52
+ //#endregion
53
+ export { verifyClientToken };
@@ -17,6 +17,12 @@ declare const ERROR_CODES: {
17
17
  readonly QUEUE_RESULT_ERROR: "QUEUE_RESULT_ERROR";
18
18
  readonly JOB_NOT_COMPLETED: "JOB_NOT_COMPLETED";
19
19
  readonly TOKEN_CREATE_ERROR: "TOKEN_CREATE_ERROR";
20
+ /** Client-token verification: the token is not a valid, correctly signed Decart client token. */
21
+ readonly TOKEN_INVALID: "TOKEN_INVALID";
22
+ /** Client-token verification: signature is fine but `exp` is in the past (beyond clock tolerance). */
23
+ readonly TOKEN_EXPIRED: "TOKEN_EXPIRED";
24
+ /** Client-token verification could not run, e.g. the JWKS could not be fetched. Says nothing about the token. */
25
+ readonly TOKEN_VERIFY_ERROR: "TOKEN_VERIFY_ERROR";
20
26
  readonly FILES_UPLOAD_ERROR: "FILES_UPLOAD_ERROR";
21
27
  readonly FILES_GET_ERROR: "FILES_GET_ERROR";
22
28
  readonly FILES_DELETE_ERROR: "FILES_DELETE_ERROR";
@@ -11,6 +11,12 @@ const ERROR_CODES = {
11
11
  QUEUE_RESULT_ERROR: "QUEUE_RESULT_ERROR",
12
12
  JOB_NOT_COMPLETED: "JOB_NOT_COMPLETED",
13
13
  TOKEN_CREATE_ERROR: "TOKEN_CREATE_ERROR",
14
+ /** Client-token verification: the token is not a valid, correctly signed Decart client token. */
15
+ TOKEN_INVALID: "TOKEN_INVALID",
16
+ /** Client-token verification: signature is fine but `exp` is in the past (beyond clock tolerance). */
17
+ TOKEN_EXPIRED: "TOKEN_EXPIRED",
18
+ /** Client-token verification could not run, e.g. the JWKS could not be fetched. Says nothing about the token. */
19
+ TOKEN_VERIFY_ERROR: "TOKEN_VERIFY_ERROR",
14
20
  FILES_UPLOAD_ERROR: "FILES_UPLOAD_ERROR",
15
21
  FILES_GET_ERROR: "FILES_GET_ERROR",
16
22
  FILES_DELETE_ERROR: "FILES_DELETE_ERROR",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decartai/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Decart's JavaScript SDK",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,6 +49,7 @@
49
49
  "vitest": "^4.0.18"
50
50
  },
51
51
  "dependencies": {
52
+ "jose": "^6.2.2",
52
53
  "livekit-client": "~2.20.1",
53
54
  "mitt": "^3.0.1",
54
55
  "p-retry": "^6.2.1",