rivetkit 2.0.37 → 2.0.38
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/tsup/{chunk-XI335ZED.js → chunk-4U45T5KW.js} +6 -4
- package/dist/tsup/chunk-4U45T5KW.js.map +1 -0
- package/dist/tsup/{chunk-G4N7FZMM.cjs → chunk-6WLJW57U.cjs} +149 -93
- package/dist/tsup/chunk-6WLJW57U.cjs.map +1 -0
- package/dist/tsup/{chunk-ZQBSQ6H3.js → chunk-EEL32AJM.js} +140 -89
- package/dist/tsup/chunk-EEL32AJM.js.map +1 -0
- package/dist/tsup/{chunk-J6TX5EFW.js → chunk-FA6FGAEC.js} +62 -6
- package/dist/tsup/chunk-FA6FGAEC.js.map +1 -0
- package/dist/tsup/{chunk-22NKW7F5.cjs → chunk-FZQHTGQX.cjs} +9 -9
- package/dist/tsup/{chunk-22NKW7F5.cjs.map → chunk-FZQHTGQX.cjs.map} +1 -1
- package/dist/tsup/{chunk-4V7MS7SO.cjs → chunk-GFKZZG2A.cjs} +3 -3
- package/dist/tsup/{chunk-4V7MS7SO.cjs.map → chunk-GFKZZG2A.cjs.map} +1 -1
- package/dist/tsup/{chunk-LYYTV7DN.cjs → chunk-IRTVRBJA.cjs} +46 -46
- package/dist/tsup/{chunk-LYYTV7DN.cjs.map → chunk-IRTVRBJA.cjs.map} +1 -1
- package/dist/tsup/{chunk-B6BP74X3.cjs → chunk-IWXMFQDT.cjs} +113 -74
- package/dist/tsup/chunk-IWXMFQDT.cjs.map +1 -0
- package/dist/tsup/{chunk-RBA5AQTB.js → chunk-K2RNF2ZR.js} +5 -5
- package/dist/tsup/{chunk-5VVIFC6M.cjs → chunk-LULP6HM2.cjs} +381 -330
- package/dist/tsup/chunk-LULP6HM2.cjs.map +1 -0
- package/dist/tsup/{chunk-5XGZXH74.js → chunk-MIOU6BF3.js} +57 -18
- package/dist/tsup/chunk-MIOU6BF3.js.map +1 -0
- package/dist/tsup/{chunk-X5IX3YPO.cjs → chunk-O433HWWG.cjs} +6 -4
- package/dist/tsup/chunk-O433HWWG.cjs.map +1 -0
- package/dist/tsup/{chunk-FIUSIG6J.js → chunk-UUEZVDRL.js} +4 -4
- package/dist/tsup/{chunk-RXA3ZMCL.js → chunk-WIZ4JGP6.js} +2 -2
- package/dist/tsup/client/mod.cjs +5 -5
- package/dist/tsup/client/mod.d.cts +2 -2
- package/dist/tsup/client/mod.d.ts +2 -2
- package/dist/tsup/client/mod.js +4 -4
- package/dist/tsup/common/log.cjs +2 -2
- package/dist/tsup/common/log.js +1 -1
- package/dist/tsup/common/websocket.cjs +3 -3
- package/dist/tsup/common/websocket.js +2 -2
- package/dist/tsup/{config-CRuzI6n4.d.ts → config-CbIHPGKl.d.ts} +167 -56
- package/dist/tsup/{config--NjwiYlS.d.cts → config-CwJCQyP1.d.cts} +167 -56
- package/dist/tsup/{driver-BcmckRaF.d.ts → driver-CMN823Lc.d.ts} +1 -1
- package/dist/tsup/{driver-yKjYx9Yy.d.cts → driver-Lw_oORox.d.cts} +1 -1
- package/dist/tsup/driver-helpers/mod.cjs +3 -3
- package/dist/tsup/driver-helpers/mod.d.cts +2 -2
- package/dist/tsup/driver-helpers/mod.d.ts +2 -2
- package/dist/tsup/driver-helpers/mod.js +2 -2
- package/dist/tsup/driver-test-suite/mod.cjs +34 -34
- package/dist/tsup/driver-test-suite/mod.d.cts +2 -2
- package/dist/tsup/driver-test-suite/mod.d.ts +2 -2
- package/dist/tsup/driver-test-suite/mod.js +7 -7
- package/dist/tsup/mod.cjs +17 -7
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +4 -4
- package/dist/tsup/mod.d.ts +4 -4
- package/dist/tsup/mod.js +16 -6
- package/dist/tsup/test/mod.cjs +7 -7
- package/dist/tsup/test/mod.d.cts +1 -1
- package/dist/tsup/test/mod.d.ts +1 -1
- package/dist/tsup/test/mod.js +6 -6
- package/dist/tsup/utils.cjs +2 -2
- package/dist/tsup/utils.js +1 -1
- package/package.json +6 -4
- package/src/actor/config.ts +47 -0
- package/src/client/actor-conn.ts +70 -81
- package/src/client/actor-handle.ts +22 -12
- package/src/client/actor-query.ts +47 -0
- package/src/client/errors.ts +22 -58
- package/src/client/utils.ts +33 -0
- package/src/manager/driver.ts +1 -3
- package/src/manager-api/actors.ts +1 -20
- package/src/registry/config/index.ts +68 -0
- package/src/remote-manager-driver/mod.ts +11 -1
- package/src/serverless/router.test.ts +166 -0
- package/src/serverless/router.ts +58 -5
- package/src/utils/env-vars.ts +4 -1
- package/dist/tsup/chunk-5VVIFC6M.cjs.map +0 -1
- package/dist/tsup/chunk-5XGZXH74.js.map +0 -1
- package/dist/tsup/chunk-B6BP74X3.cjs.map +0 -1
- package/dist/tsup/chunk-G4N7FZMM.cjs.map +0 -1
- package/dist/tsup/chunk-J6TX5EFW.js.map +0 -1
- package/dist/tsup/chunk-X5IX3YPO.cjs.map +0 -1
- package/dist/tsup/chunk-XI335ZED.js.map +0 -1
- package/dist/tsup/chunk-ZQBSQ6H3.js.map +0 -1
- /package/dist/tsup/{chunk-RBA5AQTB.js.map → chunk-K2RNF2ZR.js.map} +0 -0
- /package/dist/tsup/{chunk-FIUSIG6J.js.map → chunk-UUEZVDRL.js.map} +0 -0
- /package/dist/tsup/{chunk-RXA3ZMCL.js.map → chunk-WIZ4JGP6.js.map} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Context as HonoContext } from "hono";
|
|
2
2
|
import * as errors from "@/actor/errors";
|
|
3
|
+
import { stringifyError } from "@/common/utils";
|
|
3
4
|
import type { ManagerDriver } from "@/driver-helpers/mod";
|
|
4
5
|
import type { ActorQuery } from "@/manager/protocol/query";
|
|
6
|
+
import { ActorSchedulingError } from "./errors";
|
|
5
7
|
import { logger } from "./log";
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -63,3 +65,48 @@ export async function queryActor(
|
|
|
63
65
|
logger().debug({ msg: "actor query result", actorId: actorOutput.actorId });
|
|
64
66
|
return { actorId: actorOutput.actorId };
|
|
65
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract the actor name from a query.
|
|
71
|
+
*/
|
|
72
|
+
export function getActorNameFromQuery(query: ActorQuery): string {
|
|
73
|
+
if ("getForId" in query) return query.getForId.name;
|
|
74
|
+
if ("getForKey" in query) return query.getForKey.name;
|
|
75
|
+
if ("getOrCreateForKey" in query) return query.getOrCreateForKey.name;
|
|
76
|
+
if ("create" in query) return query.create.name;
|
|
77
|
+
throw new errors.InvalidRequest("Invalid query format");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch actor details and check for scheduling errors.
|
|
82
|
+
*/
|
|
83
|
+
export async function checkForSchedulingError(
|
|
84
|
+
group: string,
|
|
85
|
+
code: string,
|
|
86
|
+
actorId: string,
|
|
87
|
+
query: ActorQuery,
|
|
88
|
+
driver: ManagerDriver,
|
|
89
|
+
): Promise<ActorSchedulingError | null> {
|
|
90
|
+
const name = getActorNameFromQuery(query);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const actor = await driver.getForId({ name, actorId });
|
|
94
|
+
|
|
95
|
+
if (actor?.error) {
|
|
96
|
+
logger().info({
|
|
97
|
+
msg: "found actor scheduling error",
|
|
98
|
+
actorId,
|
|
99
|
+
error: actor.error,
|
|
100
|
+
});
|
|
101
|
+
return new ActorSchedulingError(group, code, actorId, actor.error);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger().warn({
|
|
105
|
+
msg: "failed to fetch actor details for scheduling error check",
|
|
106
|
+
actorId,
|
|
107
|
+
error: stringifyError(err),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
package/src/client/errors.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { MAX_CONN_PARAMS_SIZE } from "@/common//network";
|
|
2
|
-
|
|
3
1
|
export class ActorClientError extends Error {}
|
|
4
2
|
|
|
5
3
|
export class InternalError extends ActorClientError {}
|
|
@@ -41,72 +39,38 @@ export class ActorConnDisposed extends ActorClientError {
|
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
// === Actor Scheduling Error Types ===
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Errors from serverless connection workflow.
|
|
48
|
-
* Matches ServerlessConnectionError from API.
|
|
49
|
-
*/
|
|
50
|
-
export type ServerlessConnectionError =
|
|
51
|
-
| { http_error: { status_code: number; body: string } }
|
|
52
|
-
| "stream_ended_early"
|
|
53
|
-
| { connection_error: { message: string } }
|
|
54
|
-
| "invalid_base64"
|
|
55
|
-
| { invalid_payload: { message: string } }
|
|
56
|
-
| "runner_config_not_found"
|
|
57
|
-
| "runner_config_not_serverless"
|
|
58
|
-
| "namespace_not_found";
|
|
59
|
-
|
|
60
42
|
/**
|
|
61
|
-
*
|
|
62
|
-
* Matches ActorError from API.
|
|
43
|
+
* Checks if an error code indicates a scheduling error that may have more details.
|
|
63
44
|
*/
|
|
64
|
-
export
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
export function isSchedulingError(group: string, code: string): boolean {
|
|
46
|
+
return (
|
|
47
|
+
group === "guard" &&
|
|
48
|
+
(code === "actor_ready_timeout" || code === "actor_runner_failed")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
68
51
|
|
|
69
52
|
/**
|
|
70
53
|
* Error thrown when actor scheduling fails.
|
|
71
54
|
* Provides detailed information about why the actor failed to start.
|
|
72
55
|
*/
|
|
73
|
-
export class ActorSchedulingError extends
|
|
56
|
+
export class ActorSchedulingError extends ActorError {
|
|
74
57
|
public readonly actorId: string;
|
|
75
|
-
public readonly
|
|
76
|
-
public readonly details: ActorErrorDetails;
|
|
58
|
+
public readonly details: unknown;
|
|
77
59
|
|
|
78
|
-
constructor(
|
|
79
|
-
|
|
80
|
-
|
|
60
|
+
constructor(
|
|
61
|
+
group: string,
|
|
62
|
+
code: string,
|
|
63
|
+
actorId: string,
|
|
64
|
+
details: unknown,
|
|
65
|
+
) {
|
|
66
|
+
super(
|
|
67
|
+
group,
|
|
68
|
+
code,
|
|
69
|
+
`Actor failed to start (${actorId}): ${JSON.stringify(details)}`,
|
|
70
|
+
{ actorId, details },
|
|
71
|
+
);
|
|
81
72
|
this.name = "ActorSchedulingError";
|
|
82
73
|
this.actorId = actorId;
|
|
83
|
-
this.
|
|
84
|
-
this.details = error;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static formatMessage(error: ActorErrorDetails): string {
|
|
88
|
-
if ("serverless_error" in error) {
|
|
89
|
-
const se = error.serverless_error;
|
|
90
|
-
if (typeof se === "string") {
|
|
91
|
-
return `Serverless error: ${se.replace(/_/g, " ")}`;
|
|
92
|
-
}
|
|
93
|
-
if ("http_error" in se) {
|
|
94
|
-
return `Serverless HTTP ${se.http_error.status_code}: ${se.http_error.body}`;
|
|
95
|
-
}
|
|
96
|
-
if ("connection_error" in se) {
|
|
97
|
-
return `Serverless connection error: ${se.connection_error.message}`;
|
|
98
|
-
}
|
|
99
|
-
if ("invalid_payload" in se) {
|
|
100
|
-
return `Invalid serverless payload: ${se.invalid_payload.message}`;
|
|
101
|
-
}
|
|
102
|
-
return "Unknown serverless error";
|
|
103
|
-
}
|
|
104
|
-
if ("no_capacity" in error) {
|
|
105
|
-
return `No capacity available for runner: ${error.no_capacity.runner_name}`;
|
|
106
|
-
}
|
|
107
|
-
if ("runner_no_response" in error) {
|
|
108
|
-
return `Runner ${error.runner_no_response.runner_id} did not respond`;
|
|
109
|
-
}
|
|
110
|
-
return "Unknown scheduling error";
|
|
74
|
+
this.details = details;
|
|
111
75
|
}
|
|
112
76
|
}
|
package/src/client/utils.ts
CHANGED
|
@@ -20,6 +20,39 @@ import { httpUserAgent } from "@/utils";
|
|
|
20
20
|
import { ActorError, HttpRequestError } from "./errors";
|
|
21
21
|
import { logger } from "./log";
|
|
22
22
|
|
|
23
|
+
export interface ParsedCloseReason {
|
|
24
|
+
group: string;
|
|
25
|
+
code: string;
|
|
26
|
+
rayId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses WebSocket close reason string into structured data.
|
|
31
|
+
*
|
|
32
|
+
* Expected format examples:
|
|
33
|
+
* - "guard.actor_runner_failed#t1s80so6h3irenp8ymzltfoittcl00"
|
|
34
|
+
* - "ws.client_closed"
|
|
35
|
+
*
|
|
36
|
+
* Returns undefined if the format is invalid
|
|
37
|
+
*/
|
|
38
|
+
export function parseWebSocketCloseReason(
|
|
39
|
+
reason: string,
|
|
40
|
+
): ParsedCloseReason | undefined {
|
|
41
|
+
const [mainPart, rayId] = reason.split("#");
|
|
42
|
+
const [group, code] = mainPart.split(".");
|
|
43
|
+
|
|
44
|
+
if (!group || !code) {
|
|
45
|
+
logger().warn({ msg: "failed to parse close reason", reason });
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
group,
|
|
51
|
+
code,
|
|
52
|
+
rayId,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
23
56
|
export type WebSocketMessage = string | Blob | ArrayBuffer | Uint8Array;
|
|
24
57
|
|
|
25
58
|
export function messageLength(message: WebSocketMessage): number {
|
package/src/manager/driver.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Env, Hono, Context as HonoContext } from "hono";
|
|
2
2
|
import type { ActorKey, Encoding, UniversalWebSocket } from "@/actor/mod";
|
|
3
|
-
|
|
4
|
-
import type { ActorErrorDetails } from "@/client/errors";
|
|
5
3
|
import type { RegistryConfig } from "@/registry/config";
|
|
6
4
|
import type { GetUpgradeWebSocket } from "@/utils";
|
|
7
5
|
|
|
@@ -97,5 +95,5 @@ export interface ActorOutput {
|
|
|
97
95
|
connectableTs?: number | null;
|
|
98
96
|
sleepTs?: number | null;
|
|
99
97
|
destroyTs?: number | null;
|
|
100
|
-
error?:
|
|
98
|
+
error?: unknown;
|
|
101
99
|
}
|
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { RivetIdSchema } from "./common";
|
|
3
3
|
|
|
4
|
-
// Schema for serverless connection errors
|
|
5
|
-
const ServerlessConnectionErrorSchema = z.union([
|
|
6
|
-
z.object({ http_error: z.object({ status_code: z.number(), body: z.string() }) }),
|
|
7
|
-
z.literal("stream_ended_early"),
|
|
8
|
-
z.object({ connection_error: z.object({ message: z.string() }) }),
|
|
9
|
-
z.literal("invalid_base64"),
|
|
10
|
-
z.object({ invalid_payload: z.object({ message: z.string() }) }),
|
|
11
|
-
z.literal("runner_config_not_found"),
|
|
12
|
-
z.literal("runner_config_not_serverless"),
|
|
13
|
-
z.literal("namespace_not_found"),
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
// Schema for actor error details from API
|
|
17
|
-
const ActorErrorDetailsSchema = z.union([
|
|
18
|
-
z.object({ serverless_error: ServerlessConnectionErrorSchema }),
|
|
19
|
-
z.object({ no_capacity: z.object({ runner_name: z.string() }) }),
|
|
20
|
-
z.object({ runner_no_response: z.object({ runner_id: z.string() }) }),
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
4
|
export const ActorSchema = z.object({
|
|
24
5
|
actor_id: RivetIdSchema,
|
|
25
6
|
name: z.string(),
|
|
@@ -31,7 +12,7 @@ export const ActorSchema = z.object({
|
|
|
31
12
|
destroy_ts: z.number().nullable().optional(),
|
|
32
13
|
sleep_ts: z.number().nullable().optional(),
|
|
33
14
|
start_ts: z.number().nullable().optional(),
|
|
34
|
-
error:
|
|
15
|
+
error: z.unknown().nullable().optional(),
|
|
35
16
|
});
|
|
36
17
|
export type Actor = z.infer<typeof ActorSchema>;
|
|
37
18
|
|
|
@@ -264,3 +264,71 @@ export function buildActorNames(
|
|
|
264
264
|
Object.keys(config.use).map((name) => [name, { metadata: {} }]),
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
|
+
|
|
268
|
+
// MARK: Documentation Schemas
|
|
269
|
+
// These schemas are JSON-serializable versions used for documentation generation.
|
|
270
|
+
// They exclude runtime-only fields (transforms, custom types, Logger instances).
|
|
271
|
+
|
|
272
|
+
export const DocInspectorConfigSchema = z
|
|
273
|
+
.object({
|
|
274
|
+
enabled: z.boolean().optional().describe("Whether to enable the Rivet Inspector. Defaults to true in development mode."),
|
|
275
|
+
token: z.string().optional().describe("Token used to access the Inspector."),
|
|
276
|
+
defaultEndpoint: z.string().optional().describe("Default RivetKit server endpoint for Rivet Inspector to connect to."),
|
|
277
|
+
})
|
|
278
|
+
.optional()
|
|
279
|
+
.describe("Inspector configuration for debugging and development.");
|
|
280
|
+
|
|
281
|
+
export const DocConfigureRunnerPoolSchema = z
|
|
282
|
+
.object({
|
|
283
|
+
name: z.string().optional().describe("Name of the runner pool."),
|
|
284
|
+
url: z.string().describe("URL of the serverless platform to configure runners."),
|
|
285
|
+
headers: z.record(z.string(), z.string()).optional().describe("Headers to include in requests to the serverless platform."),
|
|
286
|
+
maxRunners: z.number().optional().describe("Maximum number of runners in the pool."),
|
|
287
|
+
minRunners: z.number().optional().describe("Minimum number of runners to keep warm."),
|
|
288
|
+
requestLifespan: z.number().optional().describe("Maximum lifespan of a request in milliseconds."),
|
|
289
|
+
runnersMargin: z.number().optional().describe("Buffer margin for scaling runners."),
|
|
290
|
+
slotsPerRunner: z.number().optional().describe("Number of actor slots per runner."),
|
|
291
|
+
metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata to pass to the serverless platform."),
|
|
292
|
+
})
|
|
293
|
+
.optional();
|
|
294
|
+
|
|
295
|
+
export const DocServerlessConfigSchema = z.object({
|
|
296
|
+
spawnEngine: z.boolean().optional().describe("Downloads and starts the full Rust engine process. Auto-enabled in development mode when no endpoint is provided. Default: false"),
|
|
297
|
+
engineVersion: z.string().optional().describe("Version of the engine to download. Defaults to the current RivetKit version."),
|
|
298
|
+
configureRunnerPool: DocConfigureRunnerPoolSchema.describe("Automatically configure serverless runners in the engine."),
|
|
299
|
+
basePath: z.string().optional().describe("Base path for serverless API routes. Default: '/api/rivet'"),
|
|
300
|
+
publicEndpoint: z.string().optional().describe("The endpoint that clients should connect to. Supports URL auth syntax: https://namespace:token@api.rivet.dev"),
|
|
301
|
+
publicToken: z.string().optional().describe("Token that clients should use when connecting via the public endpoint."),
|
|
302
|
+
}).describe("Configuration for serverless deployment mode.");
|
|
303
|
+
|
|
304
|
+
export const DocRunnerConfigSchema = z.object({
|
|
305
|
+
totalSlots: z.number().optional().describe("Total number of actor slots available. Default: 100000"),
|
|
306
|
+
runnerName: z.string().optional().describe("Name of this runner. Default: 'default'"),
|
|
307
|
+
runnerKey: z.string().optional().describe("Authentication key for the runner."),
|
|
308
|
+
version: z.number().optional().describe("Version number of this runner. Default: 1"),
|
|
309
|
+
}).describe("Configuration for runner mode.");
|
|
310
|
+
|
|
311
|
+
export const DocRegistryConfigSchema = z
|
|
312
|
+
.object({
|
|
313
|
+
use: z.record(z.string(), z.unknown()).describe("Actor definitions. Keys are actor names, values are actor definitions."),
|
|
314
|
+
maxIncomingMessageSize: z.number().optional().describe("Maximum size of incoming WebSocket messages in bytes. Default: 65536"),
|
|
315
|
+
maxOutgoingMessageSize: z.number().optional().describe("Maximum size of outgoing WebSocket messages in bytes. Default: 1048576"),
|
|
316
|
+
noWelcome: z.boolean().optional().describe("Disable the welcome message on startup. Default: false"),
|
|
317
|
+
logging: z
|
|
318
|
+
.object({
|
|
319
|
+
level: LogLevelSchema.optional().describe("Log level for RivetKit. Default: 'warn'"),
|
|
320
|
+
})
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("Logging configuration."),
|
|
323
|
+
endpoint: z.string().optional().describe("Endpoint URL to connect to Rivet Engine. Supports URL auth syntax: https://namespace:token@api.rivet.dev. Can also be set via RIVET_ENDPOINT environment variable."),
|
|
324
|
+
token: z.string().optional().describe("Authentication token for Rivet Engine. Can also be set via RIVET_TOKEN environment variable."),
|
|
325
|
+
namespace: z.string().optional().describe("Namespace to use. Default: 'default'. Can also be set via RIVET_NAMESPACE environment variable."),
|
|
326
|
+
headers: z.record(z.string(), z.string()).optional().describe("Additional headers to include in requests to Rivet Engine."),
|
|
327
|
+
serveManager: z.boolean().optional().describe("Whether to start the local manager server. Auto-determined based on endpoint and NODE_ENV if not specified."),
|
|
328
|
+
managerBasePath: z.string().optional().describe("Base path for the manager API. Default: '/'"),
|
|
329
|
+
managerPort: z.number().optional().describe("Port to run the manager on. Default: 6420"),
|
|
330
|
+
inspector: DocInspectorConfigSchema,
|
|
331
|
+
serverless: DocServerlessConfigSchema.optional(),
|
|
332
|
+
runner: DocRunnerConfigSchema.optional(),
|
|
333
|
+
})
|
|
334
|
+
.describe("RivetKit registry configuration.");
|
|
@@ -81,9 +81,19 @@ export class RemoteManagerDriver implements ManagerDriver {
|
|
|
81
81
|
// Override endpoint for all future requests
|
|
82
82
|
if (metadataData.clientEndpoint) {
|
|
83
83
|
this.#config.endpoint = metadataData.clientEndpoint;
|
|
84
|
+
if (metadataData.clientNamespace) {
|
|
85
|
+
this.#config.namespace =
|
|
86
|
+
metadataData.clientNamespace;
|
|
87
|
+
}
|
|
88
|
+
if (metadataData.clientToken) {
|
|
89
|
+
this.#config.token = metadataData.clientToken;
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
logger().info({
|
|
85
|
-
msg: "overriding
|
|
93
|
+
msg: "overriding client endpoint",
|
|
86
94
|
endpoint: metadataData.clientEndpoint,
|
|
95
|
+
namespace: metadataData.clientNamespace,
|
|
96
|
+
token: metadataData.clientToken,
|
|
87
97
|
});
|
|
88
98
|
}
|
|
89
99
|
|
|
@@ -26,6 +26,12 @@ describe("normalizeEndpointUrl", () => {
|
|
|
26
26
|
);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
+
test("normalizes IPv6 loopback [::1] to localhost", () => {
|
|
30
|
+
expect(normalizeEndpointUrl("http://[::1]:6420")).toBe(
|
|
31
|
+
"http://localhost:6420/",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
29
35
|
test("preserves path without trailing slash", () => {
|
|
30
36
|
expect(normalizeEndpointUrl("http://example.com/api/v1")).toBe(
|
|
31
37
|
"http://example.com/api/v1",
|
|
@@ -69,6 +75,50 @@ describe("normalizeEndpointUrl", () => {
|
|
|
69
75
|
test("returns null for empty string", () => {
|
|
70
76
|
expect(normalizeEndpointUrl("")).toBeNull();
|
|
71
77
|
});
|
|
78
|
+
|
|
79
|
+
describe("regional endpoint normalization", () => {
|
|
80
|
+
test("normalizes api-us-west-1.rivet.dev to api.rivet.dev", () => {
|
|
81
|
+
expect(normalizeEndpointUrl("https://api-us-west-1.rivet.dev")).toBe(
|
|
82
|
+
"https://api.rivet.dev/",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("normalizes api-lax.staging.rivet.dev to api.staging.rivet.dev", () => {
|
|
87
|
+
expect(
|
|
88
|
+
normalizeEndpointUrl("https://api-lax.staging.rivet.dev"),
|
|
89
|
+
).toBe("https://api.staging.rivet.dev/");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("preserves api.rivet.dev unchanged", () => {
|
|
93
|
+
expect(normalizeEndpointUrl("https://api.rivet.dev")).toBe(
|
|
94
|
+
"https://api.rivet.dev/",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("does not normalize non-api prefixed hostnames", () => {
|
|
99
|
+
expect(normalizeEndpointUrl("https://foo-bar.rivet.dev")).toBe(
|
|
100
|
+
"https://foo-bar.rivet.dev/",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("does not normalize non-rivet.dev domains", () => {
|
|
105
|
+
expect(normalizeEndpointUrl("https://api-us-west-1.example.com")).toBe(
|
|
106
|
+
"https://api-us-west-1.example.com/",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("preserves path when normalizing regional endpoint", () => {
|
|
111
|
+
expect(
|
|
112
|
+
normalizeEndpointUrl("https://api-us-west-1.rivet.dev/v1/actors"),
|
|
113
|
+
).toBe("https://api.rivet.dev/v1/actors");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("preserves port when normalizing regional endpoint", () => {
|
|
117
|
+
expect(
|
|
118
|
+
normalizeEndpointUrl("https://api-us-west-1.rivet.dev:8080"),
|
|
119
|
+
).toBe("https://api.rivet.dev:8080/");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
72
122
|
});
|
|
73
123
|
|
|
74
124
|
describe("endpointsMatch", () => {
|
|
@@ -102,6 +152,12 @@ describe("endpointsMatch", () => {
|
|
|
102
152
|
).toBe(true);
|
|
103
153
|
});
|
|
104
154
|
|
|
155
|
+
test("matches localhost and IPv6 loopback [::1]", () => {
|
|
156
|
+
expect(
|
|
157
|
+
endpointsMatch("http://localhost:6420", "http://[::1]:6420"),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
105
161
|
test("does not match different hosts", () => {
|
|
106
162
|
expect(
|
|
107
163
|
endpointsMatch("http://localhost:6420", "http://example.com:6420"),
|
|
@@ -130,4 +186,114 @@ describe("endpointsMatch", () => {
|
|
|
130
186
|
expect(endpointsMatch("not-a-url", "not-a-url")).toBe(true);
|
|
131
187
|
expect(endpointsMatch("not-a-url", "different")).toBe(false);
|
|
132
188
|
});
|
|
189
|
+
|
|
190
|
+
describe("regional endpoint matching", () => {
|
|
191
|
+
test("matches api.rivet.dev with api-us-west-1.rivet.dev", () => {
|
|
192
|
+
expect(
|
|
193
|
+
endpointsMatch(
|
|
194
|
+
"https://api.rivet.dev",
|
|
195
|
+
"https://api-us-west-1.rivet.dev",
|
|
196
|
+
),
|
|
197
|
+
).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("matches api-us-west-1.rivet.dev with api.rivet.dev (reverse order)", () => {
|
|
201
|
+
expect(
|
|
202
|
+
endpointsMatch(
|
|
203
|
+
"https://api-us-west-1.rivet.dev",
|
|
204
|
+
"https://api.rivet.dev",
|
|
205
|
+
),
|
|
206
|
+
).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("matches api.staging.rivet.dev with api-lax.staging.rivet.dev", () => {
|
|
210
|
+
expect(
|
|
211
|
+
endpointsMatch(
|
|
212
|
+
"https://api.staging.rivet.dev",
|
|
213
|
+
"https://api-lax.staging.rivet.dev",
|
|
214
|
+
),
|
|
215
|
+
).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("matches api-lax.staging.rivet.dev with api.staging.rivet.dev (reverse order)", () => {
|
|
219
|
+
expect(
|
|
220
|
+
endpointsMatch(
|
|
221
|
+
"https://api-lax.staging.rivet.dev",
|
|
222
|
+
"https://api.staging.rivet.dev",
|
|
223
|
+
),
|
|
224
|
+
).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("matches with paths", () => {
|
|
228
|
+
expect(
|
|
229
|
+
endpointsMatch(
|
|
230
|
+
"https://api.rivet.dev/v1/actors",
|
|
231
|
+
"https://api-us-west-1.rivet.dev/v1/actors",
|
|
232
|
+
),
|
|
233
|
+
).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("does not match different domains", () => {
|
|
237
|
+
expect(
|
|
238
|
+
endpointsMatch(
|
|
239
|
+
"https://api.rivet.dev",
|
|
240
|
+
"https://api-us-west-1.example.com",
|
|
241
|
+
),
|
|
242
|
+
).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("does not match different protocols", () => {
|
|
246
|
+
expect(
|
|
247
|
+
endpointsMatch(
|
|
248
|
+
"http://api.rivet.dev",
|
|
249
|
+
"https://api-us-west-1.rivet.dev",
|
|
250
|
+
),
|
|
251
|
+
).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("does not match different paths", () => {
|
|
255
|
+
expect(
|
|
256
|
+
endpointsMatch(
|
|
257
|
+
"https://api.rivet.dev/v1",
|
|
258
|
+
"https://api-us-west-1.rivet.dev/v2",
|
|
259
|
+
),
|
|
260
|
+
).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("does not match different ports", () => {
|
|
264
|
+
expect(
|
|
265
|
+
endpointsMatch(
|
|
266
|
+
"https://api.rivet.dev:8080",
|
|
267
|
+
"https://api-us-west-1.rivet.dev:9090",
|
|
268
|
+
),
|
|
269
|
+
).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("matches with same port", () => {
|
|
273
|
+
expect(
|
|
274
|
+
endpointsMatch(
|
|
275
|
+
"https://api.rivet.dev:8080",
|
|
276
|
+
"https://api-us-west-1.rivet.dev:8080",
|
|
277
|
+
),
|
|
278
|
+
).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("does not match non-api prefixed hosts", () => {
|
|
282
|
+
expect(
|
|
283
|
+
endpointsMatch(
|
|
284
|
+
"https://foo.rivet.dev",
|
|
285
|
+
"https://foo-us-west-1.rivet.dev",
|
|
286
|
+
),
|
|
287
|
+
).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("does not match api.staging.rivet.dev with api-us-west-1.rivet.dev (different base domains)", () => {
|
|
291
|
+
expect(
|
|
292
|
+
endpointsMatch(
|
|
293
|
+
"https://api.staging.rivet.dev",
|
|
294
|
+
"https://api-us-west-1.rivet.dev",
|
|
295
|
+
),
|
|
296
|
+
).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
133
299
|
});
|
package/src/serverless/router.ts
CHANGED
|
@@ -124,7 +124,8 @@ export function buildServerlessRouter(
|
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Normalizes a URL for comparison by extracting protocol, host, port, and pathname.
|
|
127
|
-
* Normalizes 127.0.0.1
|
|
127
|
+
* Normalizes loopback addresses (127.0.0.1, 0.0.0.0, ::1) to localhost for consistent comparison.
|
|
128
|
+
* Normalizes regional endpoints (api-*.domain) to base endpoints (api.domain).
|
|
128
129
|
* Returns null if the URL is invalid.
|
|
129
130
|
*/
|
|
130
131
|
export function normalizeEndpointUrl(url: string): string | null {
|
|
@@ -133,13 +134,20 @@ export function normalizeEndpointUrl(url: string): string | null {
|
|
|
133
134
|
// Normalize pathname by removing trailing slash (except for root)
|
|
134
135
|
const pathname =
|
|
135
136
|
parsed.pathname === "/" ? "/" : parsed.pathname.replace(/\/+$/, "");
|
|
137
|
+
|
|
136
138
|
// Normalize loopback addresses to localhost
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
let hostname = isLoopbackAddress(parsed.hostname)
|
|
140
|
+
? "localhost"
|
|
141
|
+
: parsed.hostname;
|
|
142
|
+
|
|
143
|
+
// Normalize regional endpoints (api-region.domain) to base endpoints (api.domain)
|
|
144
|
+
// HACK: This is specific to Rivet Cloud and will not work for self-hosted
|
|
145
|
+
// engines with different regional endpoint naming conventions.
|
|
146
|
+
hostname = normalizeRegionalHostname(hostname);
|
|
147
|
+
|
|
141
148
|
// Reconstruct host with normalized hostname and port
|
|
142
149
|
const host = parsed.port ? `${hostname}:${parsed.port}` : hostname;
|
|
150
|
+
|
|
143
151
|
// Reconstruct normalized URL with protocol, host, and pathname
|
|
144
152
|
return `${parsed.protocol}//${host}${pathname}`;
|
|
145
153
|
} catch {
|
|
@@ -147,6 +155,39 @@ export function normalizeEndpointUrl(url: string): string | null {
|
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Normalizes regional hostnames (api-region.domain) to base hostnames (api.domain).
|
|
160
|
+
* Only applies to rivet.dev domains.
|
|
161
|
+
*
|
|
162
|
+
* Examples:
|
|
163
|
+
* - api-us-west-1.rivet.dev -> api.rivet.dev
|
|
164
|
+
* - api-lax.staging.rivet.dev -> api.staging.rivet.dev
|
|
165
|
+
* - api.rivet.dev -> api.rivet.dev (unchanged)
|
|
166
|
+
* - api-us-west-1.example.com -> api-us-west-1.example.com (unchanged, not rivet.dev)
|
|
167
|
+
* - foo-bar.rivet.dev -> foo-bar.rivet.dev (unchanged, not api- prefix)
|
|
168
|
+
*/
|
|
169
|
+
function normalizeRegionalHostname(hostname: string): string {
|
|
170
|
+
// Only apply to rivet.dev domains
|
|
171
|
+
if (!hostname.endsWith(".rivet.dev")) {
|
|
172
|
+
return hostname;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!hostname.startsWith("api-")) {
|
|
176
|
+
return hostname;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Find the first dot after "api-"
|
|
180
|
+
const withoutPrefix = hostname.slice(4); // Remove "api-"
|
|
181
|
+
const firstDotIndex = withoutPrefix.indexOf(".");
|
|
182
|
+
if (firstDotIndex === -1) {
|
|
183
|
+
return hostname;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Extract the domain part and prepend "api."
|
|
187
|
+
const domain = withoutPrefix.slice(firstDotIndex + 1);
|
|
188
|
+
return `api.${domain}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
/**
|
|
151
192
|
* Compares two endpoint URLs after normalization.
|
|
152
193
|
* Returns true if they match (same protocol, host, port, and path).
|
|
@@ -160,3 +201,15 @@ export function endpointsMatch(a: string, b: string): boolean {
|
|
|
160
201
|
}
|
|
161
202
|
return normalizedA === normalizedB;
|
|
162
203
|
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Checks if a hostname is a loopback address that should be normalized to localhost.
|
|
207
|
+
*/
|
|
208
|
+
function isLoopbackAddress(hostname: string): boolean {
|
|
209
|
+
return (
|
|
210
|
+
hostname === "127.0.0.1" ||
|
|
211
|
+
hostname === "0.0.0.0" ||
|
|
212
|
+
hostname === "::1" ||
|
|
213
|
+
hostname === "[::1]"
|
|
214
|
+
);
|
|
215
|
+
}
|
package/src/utils/env-vars.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// This file consolidates all environment variables that affect RivetKit's behavior.
|
|
2
|
+
//
|
|
3
|
+
// IMPORTANT: When adding or modifying environment variables here, also update the
|
|
4
|
+
// documentation at: docs/general/registry-configuration.mdx
|
|
2
5
|
|
|
3
6
|
import { getEnvUniversal } from "@/utils";
|
|
4
7
|
|