@decartai/sdk 0.2.2 → 0.2.4
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 +57 -0
- package/dist/create-client.js +6 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/index.react-native.d.ts +4 -2
- package/dist/index.react-native.js +3 -1
- package/dist/package.js +1 -1
- package/dist/realtime/client.d.ts +1 -0
- package/dist/realtime/client.js +11 -1
- package/dist/realtime/config-realtime.js +12 -0
- package/dist/realtime/stream-session.js +30 -11
- package/dist/realtime/types.d.ts +11 -2
- package/dist/tokens/claims.d.ts +44 -0
- package/dist/tokens/claims.js +53 -0
- package/dist/tokens/client.d.ts +14 -1
- package/dist/tokens/client.js +7 -1
- package/dist/tokens/verify.d.ts +26 -0
- package/dist/tokens/verify.js +53 -0
- package/dist/utils/errors.d.ts +6 -0
- package/dist/utils/errors.js +6 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -103,6 +103,31 @@ models.realtime("lucy-2.1").supportedSpeeds; // undefined
|
|
|
103
103
|
|
|
104
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
105
|
|
|
106
|
+
#### Connect retries and refused sessions
|
|
107
|
+
|
|
108
|
+
A failed connect is re-dialled up to 5 times with exponential backoff (1 s to 10 s) before
|
|
109
|
+
`connect()` rejects. Two refusals are never retried, because the server would give the same answer:
|
|
110
|
+
a policy close (code 1008) and the API key's concurrent-session limit (close 1013 "Session Limit
|
|
111
|
+
Reached"). The session limit rejects `connect()` on the first attempt with the server's message
|
|
112
|
+
(`Concurrent session limit reached.`); if it lands after the session is established, the session ends
|
|
113
|
+
with a `sessionEnded` event whose `reason` is `"session_limit"`. A capacity refusal (1013 "Try Again
|
|
114
|
+
Later") is transient and still retries.
|
|
115
|
+
|
|
116
|
+
Apps that hand out connects from their own session queue should turn retries off: every retry is a
|
|
117
|
+
fresh dial that takes a freed slot ahead of the people waiting in line.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const realtimeClient = await client.realtime.connect(stream, {
|
|
121
|
+
model,
|
|
122
|
+
retries: 0, // dial once; on failure, rejoin your own queue
|
|
123
|
+
// ...
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
realtimeClient.on("sessionEnded", ({ reason }) => {
|
|
127
|
+
// "session_limit" | "policy_violation" | "moderation_violation" | "insufficient_credits" | ...
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
106
131
|
### Watch a Stream
|
|
107
132
|
|
|
108
133
|
A connected realtime session exposes an SDK `subscribeToken` once it reaches a
|
|
@@ -290,6 +315,38 @@ if (status.status === "completed") {
|
|
|
290
315
|
}
|
|
291
316
|
```
|
|
292
317
|
|
|
318
|
+
### Client Tokens
|
|
319
|
+
|
|
320
|
+
Mint short-lived client tokens on your server instead of shipping your API key.
|
|
321
|
+
The response also carries a signed JWT (`token`) that any server or edge runtime
|
|
322
|
+
can verify **offline** against the platform's public JWKS — no API key, no
|
|
323
|
+
round-trip to Decart:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { createDecartClient, verifyClientToken, decodeClientToken } from "@decartai/sdk";
|
|
327
|
+
|
|
328
|
+
const client = createDecartClient({ apiKey: process.env.DECART_API_KEY });
|
|
329
|
+
const { apiKey, token } = await client.tokens.create({ expiresIn: 300, metadata: { service_tier: 0 } });
|
|
330
|
+
|
|
331
|
+
// Verify offline (EdDSA signature, exp, iss, aud) and read the claims
|
|
332
|
+
const claims = await verifyClientToken(token); // or client.tokens.verify(token)
|
|
333
|
+
claims.serviceTier; // 0
|
|
334
|
+
claims.pool; // "free" for tier 0, otherwise "paid"
|
|
335
|
+
// also: userId, organizationId, apiKeyName, apiKeyId, allowedModels, allowedOrigins,
|
|
336
|
+
// expiresAt, realtimeConcurrentSessionLimit, zeroDataRetention, attribution, raw
|
|
337
|
+
|
|
338
|
+
// Read claims WITHOUT verifying — no network, no crypto. Untrusted: display/logging only.
|
|
339
|
+
const untrusted = decodeClientToken(token);
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The JWKS comes from `https://platform.decart.ai/api/auth/jwks` (the platform
|
|
343
|
+
host, not `baseUrl`) and is cached; the JWT library is loaded lazily so bundles
|
|
344
|
+
that never verify do not ship it. Errors: `TOKEN_INVALID`, `TOKEN_EXPIRED`,
|
|
345
|
+
`TOKEN_VERIFY_ERROR` (JWKS unreachable). Options: `{ jwksUrl, issuer, audience,
|
|
346
|
+
clockTolerance }`. This is an offline check, not an API call. It needs WebCrypto: on
|
|
347
|
+
runtimes without it (React Native) `verify` rejects with `UNSUPPORTED_PLATFORM_FEATURE`;
|
|
348
|
+
`decodeClientToken` works everywhere.
|
|
349
|
+
|
|
293
350
|
### React Native / Expo
|
|
294
351
|
|
|
295
352
|
React Native realtime requires [LiveKit's React Native packages](https://github.com/livekit/client-sdk-react-native)
|
package/dist/create-client.js
CHANGED
|
@@ -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 });
|
package/dist/index.d.ts
CHANGED
|
@@ -11,12 +11,14 @@ import { G2GMetrics } from "./realtime/observability/g2g.js";
|
|
|
11
11
|
import { WebRTCStats } from "./realtime/observability/webrtc-stats.js";
|
|
12
12
|
import { ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport } from "./realtime/observability/connection-quality.js";
|
|
13
13
|
import { ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ReconnectEvent, VideoStallEvent } from "./realtime/observability/diagnostics.js";
|
|
14
|
-
import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEnded } from "./realtime/types.js";
|
|
14
|
+
import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEndReason, SessionEnded } from "./realtime/types.js";
|
|
15
15
|
import { SetInput } from "./realtime/methods.js";
|
|
16
16
|
import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, RealtimeMediaStream } from "./realtime/client.js";
|
|
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, RealtimeSpeed, 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, SessionEndReason, 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 };
|
|
@@ -11,12 +11,14 @@ import { G2GMetrics } from "./realtime/observability/g2g.js";
|
|
|
11
11
|
import { WebRTCStats } from "./realtime/observability/webrtc-stats.js";
|
|
12
12
|
import { ConnectionQuality, ConnectionQualityLimitingFactor, ConnectionQualityMetrics, ConnectionQualityReport } from "./realtime/observability/connection-quality.js";
|
|
13
13
|
import { ClientSessionConnectionBreakdownEvent, ClientSessionConnectionBreakdownPhase, DiagnosticEvent, DiagnosticEventName, DiagnosticEvents, ReconnectEvent, VideoStallEvent } from "./realtime/observability/diagnostics.js";
|
|
14
|
-
import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEnded } from "./realtime/types.js";
|
|
14
|
+
import { ConnectionState, GenerationEndedMessage, QueuePosition, QueuePositionMessage, SessionEndReason, SessionEnded } from "./realtime/types.js";
|
|
15
15
|
import { SetInput } from "./realtime/methods.js";
|
|
16
16
|
import { Events, RealTimeClient, RealTimeClientConnectOptions, RealTimeClientInitialState, RealtimeMediaStream } from "./realtime/client.js";
|
|
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, RealtimeSpeed, 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, SessionEndReason, 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
package/dist/realtime/client.js
CHANGED
|
@@ -28,6 +28,15 @@ const realTimeClientConnectOptionsSchema = z.object({
|
|
|
28
28
|
* model does not list it in `supportedSpeeds`). Omit it (the default) for standard mode.
|
|
29
29
|
*/
|
|
30
30
|
speed: realtimeSpeedSchema.optional(),
|
|
31
|
+
/**
|
|
32
|
+
* How many times a failed connect is re-dialled before `connect()` rejects (default 5, exponential
|
|
33
|
+
* backoff from 1 s to 10 s). `0` dials exactly once. Apps that hand out connects from their own session
|
|
34
|
+
* queue should pass `0`: every retry is a fresh dial that competes with the people waiting in line.
|
|
35
|
+
* Refusals the server will not change its mind about (policy close 1008, concurrent-session limit
|
|
36
|
+
* 1013 "Session Limit Reached") are never retried regardless. Does not affect the automatic
|
|
37
|
+
* reconnect after an established session drops.
|
|
38
|
+
*/
|
|
39
|
+
retries: z.number().int().min(0).optional(),
|
|
31
40
|
/** Local track publish codec. Desktop Safari is always pinned to vp8 and ignores this value. */
|
|
32
41
|
preferredVideoCodec: z.enum([
|
|
33
42
|
"h264",
|
|
@@ -47,7 +56,7 @@ const createRealTimeClient = (opts) => {
|
|
|
47
56
|
const connect = async (stream, options) => {
|
|
48
57
|
const parsedOptions = realTimeClientConnectOptionsSchema.safeParse(options);
|
|
49
58
|
if (!parsedOptions.success) throw parsedOptions.error;
|
|
50
|
-
const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, speed, preferredVideoCodec } = parsedOptions.data;
|
|
59
|
+
const { onRemoteStream, onConnectionChange, onConnectionQuality, onQueuePosition, initialState, resolution, speed, retries, preferredVideoCodec } = parsedOptions.data;
|
|
51
60
|
const mirror = parsedOptions.data.mirror ?? false;
|
|
52
61
|
let session;
|
|
53
62
|
let observability;
|
|
@@ -104,6 +113,7 @@ const createRealTimeClient = (opts) => {
|
|
|
104
113
|
initialImageRef,
|
|
105
114
|
initialPrompt,
|
|
106
115
|
initialPassthrough: initialState?.passthrough,
|
|
116
|
+
connectRetries: retries,
|
|
107
117
|
logger,
|
|
108
118
|
videoCodec: preparedConnection.videoCodec,
|
|
109
119
|
createMediaChannel: preparedConnection.createMediaChannel
|
|
@@ -33,6 +33,18 @@ const REALTIME_CONFIG = {
|
|
|
33
33
|
terminalEndReasons: ["moderation_violation", "insufficient_credits"],
|
|
34
34
|
/** Close code for a policy termination. */
|
|
35
35
|
terminalCloseCode: 1008,
|
|
36
|
+
/**
|
|
37
|
+
* The bouncer's concurrent-session refusal: an `error` message carrying
|
|
38
|
+
* `errorText`, then close `closeCode` with `closeReason`. Retrying it only
|
|
39
|
+
* takes a slot from whoever is next in line. 1013 with any other reason
|
|
40
|
+
* ("Try Again Later": upstream at capacity) is transient and still retries.
|
|
41
|
+
* Matched case-insensitively.
|
|
42
|
+
*/
|
|
43
|
+
sessionLimit: {
|
|
44
|
+
closeCode: 1013,
|
|
45
|
+
closeReason: "session limit reached",
|
|
46
|
+
errorText: "concurrent session limit reached"
|
|
47
|
+
},
|
|
36
48
|
permanentErrorSubstrings: [
|
|
37
49
|
"permission denied",
|
|
38
50
|
"not allowed",
|
|
@@ -7,18 +7,36 @@ import pRetry, { AbortError } from "p-retry";
|
|
|
7
7
|
function isTerminalEndReason(reason) {
|
|
8
8
|
return reason !== void 0 && REALTIME_CONFIG.session.terminalEndReasons.includes(reason);
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
const POLICY_VIOLATION = "policy_violation";
|
|
11
|
+
const SESSION_LIMIT = "session_limit";
|
|
12
|
+
function isSessionLimitClose(code, reason) {
|
|
13
|
+
const { closeCode, closeReason } = REALTIME_CONFIG.session.sessionLimit;
|
|
14
|
+
return code === closeCode && typeof reason === "string" && reason.toLowerCase().includes(closeReason);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The 1008 reason is only sent once generation has started; earlier kills carry just
|
|
18
|
+
* the code. 1013 is terminal only with the session-limit reason: its other reason,
|
|
19
|
+
* "Try Again Later", is a transient capacity refusal.
|
|
20
|
+
*/
|
|
11
21
|
function terminalReasonFromClose(cause) {
|
|
12
|
-
|
|
22
|
+
if (cause.code === REALTIME_CONFIG.session.terminalCloseCode) return POLICY_VIOLATION;
|
|
23
|
+
if (isSessionLimitClose(cause.code, cause.reason)) return SESSION_LIMIT;
|
|
24
|
+
return null;
|
|
13
25
|
}
|
|
14
26
|
/**
|
|
15
27
|
* SignalingChannel only emits `closed` after the handshake, so a close during the
|
|
16
|
-
* join reaches us as connect-failure text rather than an event.
|
|
28
|
+
* join reaches us as connect-failure text rather than an event. The session-limit
|
|
29
|
+
* refusal is preceded by an `error` message, which is usually what rejects the join;
|
|
30
|
+
* the 1013 close lands a tick later, so both texts are recognised.
|
|
17
31
|
*/
|
|
18
32
|
function terminalReasonFromError(error) {
|
|
19
33
|
if (!(error instanceof Error)) return null;
|
|
20
|
-
const
|
|
21
|
-
|
|
34
|
+
const message = error.message.toLowerCase();
|
|
35
|
+
const { terminalCloseCode, sessionLimit } = REALTIME_CONFIG.session;
|
|
36
|
+
if (message.includes(`websocket closed: ${terminalCloseCode}`)) return POLICY_VIOLATION;
|
|
37
|
+
if (message.includes(`websocket closed: ${sessionLimit.closeCode} ${sessionLimit.closeReason}`)) return SESSION_LIMIT;
|
|
38
|
+
if (message.includes(sessionLimit.errorText)) return SESSION_LIMIT;
|
|
39
|
+
return null;
|
|
22
40
|
}
|
|
23
41
|
function encodeSubscribeToken(roomName, options = {}) {
|
|
24
42
|
return btoa(JSON.stringify({
|
|
@@ -82,12 +100,12 @@ var StreamSession = class {
|
|
|
82
100
|
this.setState("connecting");
|
|
83
101
|
this.logger.info("realtime connect: starting", { attemptCycle: attempt });
|
|
84
102
|
try {
|
|
85
|
-
await pRetry(() => this.runOneConnect(attempt), this.retryOptionsFor(attempt));
|
|
103
|
+
await pRetry(() => this.runOneConnect(attempt), this.retryOptionsFor(attempt, this.config.connectRetries));
|
|
86
104
|
} catch (error) {
|
|
87
105
|
const message = error instanceof Error ? error.message : String(error);
|
|
88
106
|
const terminal = this.terminalEndReason ?? terminalReasonFromError(error);
|
|
89
|
-
if (terminal
|
|
90
|
-
this.finishTerminally(terminal, {
|
|
107
|
+
if (terminal) {
|
|
108
|
+
if (!this.disposed) this.finishTerminally(terminal, {
|
|
91
109
|
source: "connect",
|
|
92
110
|
error: message
|
|
93
111
|
});
|
|
@@ -134,9 +152,10 @@ var StreamSession = class {
|
|
|
134
152
|
assertConnected() {
|
|
135
153
|
if (!this.isConnected()) throw new Error(`Cannot send message: connection is ${this.state}`);
|
|
136
154
|
}
|
|
137
|
-
retryOptionsFor(attempt) {
|
|
155
|
+
retryOptionsFor(attempt, retries = REALTIME_CONFIG.session.retry.retries) {
|
|
138
156
|
return {
|
|
139
157
|
...REALTIME_CONFIG.session.retry,
|
|
158
|
+
retries,
|
|
140
159
|
onFailedAttempt: (_error) => {
|
|
141
160
|
this.tearDown();
|
|
142
161
|
},
|
|
@@ -287,9 +306,9 @@ var StreamSession = class {
|
|
|
287
306
|
*/
|
|
288
307
|
finishTerminally(reason, cause) {
|
|
289
308
|
this.logger.warn("realtime session ended by the server; not reconnecting", {
|
|
309
|
+
...cause,
|
|
290
310
|
reason,
|
|
291
|
-
state: this.state
|
|
292
|
-
...cause
|
|
311
|
+
state: this.state
|
|
293
312
|
});
|
|
294
313
|
this.terminalEndReason = reason;
|
|
295
314
|
this.disposed = true;
|
package/dist/realtime/types.d.ts
CHANGED
|
@@ -20,9 +20,18 @@ type GenerationEnded = {
|
|
|
20
20
|
seconds: number;
|
|
21
21
|
reason: string;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Why the server ended a session on purpose. Terminal `generation_ended` reasons
|
|
25
|
+
* (`moderation_violation`, `insufficient_credits`) pass through as sent; the SDK
|
|
26
|
+
* derives the rest from the close:
|
|
27
|
+
* - `policy_violation`: close 1008 before a reason was sent.
|
|
28
|
+
* - `session_limit`: the API key's concurrent-session limit was reached (close 1013
|
|
29
|
+
* "Session Limit Reached"). Rejoin your own queue instead of dialling again.
|
|
30
|
+
*/
|
|
31
|
+
type SessionEndReason = "moderation_violation" | "insufficient_credits" | "policy_violation" | "session_limit" | (string & {});
|
|
23
32
|
/** The server ended the session on purpose; the SDK will not reconnect. */
|
|
24
33
|
type SessionEnded = {
|
|
25
|
-
reason:
|
|
34
|
+
reason: SessionEndReason;
|
|
26
35
|
};
|
|
27
36
|
type ImageSetOptions = {
|
|
28
37
|
prompt?: string | null;
|
|
@@ -30,4 +39,4 @@ type ImageSetOptions = {
|
|
|
30
39
|
timeout?: number;
|
|
31
40
|
};
|
|
32
41
|
//#endregion
|
|
33
|
-
export { ConnectionState, GenerationEnded, GenerationEndedMessage, GenerationTick, ImageSetOptions, QueuePosition, QueuePositionMessage, SessionEnded };
|
|
42
|
+
export { ConnectionState, GenerationEnded, GenerationEndedMessage, GenerationTick, ImageSetOptions, QueuePosition, QueuePositionMessage, SessionEndReason, SessionEnded };
|
|
@@ -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 };
|
package/dist/tokens/client.d.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/dist/tokens/client.js
CHANGED
|
@@ -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 {
|
|
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 };
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -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";
|
package/dist/utils/errors.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|