rivetkit 2.3.20 → 2.3.21
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/browser/client.d.ts +43 -5
- package/dist/browser/client.js +302 -60
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/inspector/client.js +2 -1
- package/dist/browser/inspector/client.js.map +1 -1
- package/dist/tsup/actor/errors.cjs.map +1 -1
- package/dist/tsup/agent-os/index.cjs +2 -1
- package/dist/tsup/agent-os/index.cjs.map +1 -1
- package/dist/tsup/agent-os/index.d.cts +32 -4
- package/dist/tsup/agent-os/index.d.ts +32 -4
- package/dist/tsup/agent-os/index.js +2 -1
- package/dist/tsup/agent-os/index.js.map +1 -1
- package/dist/tsup/{chunk-553CIQJY.js → chunk-2CI2IEVN.js} +2 -2
- package/dist/tsup/{chunk-W2PXGWMR.js → chunk-5EGLHQJJ.js} +2 -2
- package/dist/tsup/{chunk-DMQUERRN.js → chunk-5IBUZFLP.js} +2 -2
- package/dist/tsup/{chunk-76YTS32R.cjs → chunk-6LRPYB4Q.cjs} +4 -4
- package/dist/tsup/chunk-6LRPYB4Q.cjs.map +1 -0
- package/dist/tsup/{chunk-WAISW7KR.cjs → chunk-APOANSDG.cjs} +10 -10
- package/dist/tsup/chunk-APOANSDG.cjs.map +1 -0
- package/dist/tsup/{chunk-IHKDFKM4.cjs → chunk-ARGNQGYG.cjs} +3 -3
- package/dist/tsup/chunk-ARGNQGYG.cjs.map +1 -0
- package/dist/tsup/{chunk-A36FMKOV.cjs → chunk-B5SNXPSS.cjs} +448 -199
- package/dist/tsup/chunk-B5SNXPSS.cjs.map +1 -0
- package/dist/tsup/{chunk-SNNVRHLX.js → chunk-CJXBGTVT.js} +2 -2
- package/dist/tsup/{chunk-CVLNRT7O.cjs → chunk-GHKQC4PD.cjs} +3 -3
- package/dist/tsup/chunk-GHKQC4PD.cjs.map +1 -0
- package/dist/tsup/chunk-JX2ITWAV.cjs.map +1 -1
- package/dist/tsup/chunk-KVFIONKQ.cjs.map +1 -1
- package/dist/tsup/{chunk-2CE5UNWK.cjs → chunk-KWGN2ZBJ.cjs} +3 -2
- package/dist/tsup/chunk-KWGN2ZBJ.cjs.map +1 -0
- package/dist/tsup/{chunk-3WS7Z64U.cjs → chunk-LWSLKKZ5.cjs} +3 -3
- package/dist/tsup/chunk-LWSLKKZ5.cjs.map +1 -0
- package/dist/tsup/chunk-MYWZCDML.cjs.map +1 -1
- package/dist/tsup/{chunk-RSIOGT2U.cjs → chunk-NL2EIKK6.cjs} +7 -7
- package/dist/tsup/chunk-NL2EIKK6.cjs.map +1 -0
- package/dist/tsup/chunk-NWCAIWHL.cjs.map +1 -1
- package/dist/tsup/{chunk-5G2J26VM.js → chunk-PYRI75DL.js} +314 -65
- package/dist/tsup/chunk-PYRI75DL.js.map +1 -0
- package/dist/tsup/{chunk-2NXLTFWI.js → chunk-RFN3BG6D.js} +3 -3
- package/dist/tsup/chunk-RFN3BG6D.js.map +1 -0
- package/dist/tsup/{chunk-DHWZTUKA.js → chunk-RGIZNZYO.js} +3 -2
- package/dist/tsup/chunk-RGIZNZYO.js.map +1 -0
- package/dist/tsup/{chunk-TZVAVPGJ.js → chunk-VMMXXVDO.js} +4 -4
- package/dist/tsup/client/mod.cjs +5 -5
- package/dist/tsup/client/mod.cjs.map +1 -1
- package/dist/tsup/client/mod.d.cts +12 -3
- package/dist/tsup/client/mod.d.ts +12 -3
- package/dist/tsup/client/mod.js +4 -4
- package/dist/tsup/common/log.cjs +2 -2
- package/dist/tsup/common/log.cjs.map +1 -1
- package/dist/tsup/common/log.js +1 -1
- package/dist/tsup/common/websocket.cjs +3 -3
- package/dist/tsup/common/websocket.cjs.map +1 -1
- package/dist/tsup/common/websocket.js +2 -2
- package/dist/tsup/{config-Be-VOHaX.d.ts → config-Bc1nnvUl.d.ts} +35 -5
- package/dist/tsup/{config-Ct5uXxXk.d.cts → config-Do_0guSS.d.cts} +35 -5
- package/dist/tsup/{context-Cw-11Q-B.d.cts → context-CgNkhjdY.d.cts} +1 -1
- package/dist/tsup/{context-CXLa0BIM.d.ts → context-oGQxZDPz.d.ts} +1 -1
- package/dist/tsup/db/drizzle.cjs +2 -2
- package/dist/tsup/db/drizzle.cjs.map +1 -1
- package/dist/tsup/db/drizzle.js +1 -1
- package/dist/tsup/db/mod.cjs +3 -3
- package/dist/tsup/db/mod.cjs.map +1 -1
- package/dist/tsup/db/mod.js +2 -2
- package/dist/tsup/dynamic/mod.cjs.map +1 -1
- package/dist/tsup/dynamic/mod.d.cts +2 -1
- package/dist/tsup/dynamic/mod.d.ts +2 -1
- package/dist/tsup/inspector/mod.cjs +5 -5
- package/dist/tsup/inspector/mod.cjs.map +1 -1
- package/dist/tsup/inspector/mod.js +4 -4
- package/dist/tsup/inspector/workflow.cjs +4 -4
- package/dist/tsup/inspector/workflow.cjs.map +1 -1
- package/dist/tsup/inspector/workflow.js +3 -3
- package/dist/tsup/inspector-tab/mod.cjs.map +1 -1
- package/dist/tsup/inspector-tab/mod.d.cts +3 -2
- package/dist/tsup/inspector-tab/mod.d.ts +3 -2
- package/dist/tsup/mod.cjs +105 -115
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +4 -3
- package/dist/tsup/mod.d.ts +4 -3
- package/dist/tsup/mod.js +29 -39
- package/dist/tsup/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +6 -6
- package/dist/tsup/test/mod.cjs.map +1 -1
- package/dist/tsup/test/mod.d.cts +2 -1
- package/dist/tsup/test/mod.d.ts +2 -1
- package/dist/tsup/test/mod.js +4 -4
- package/dist/tsup/unstable/migrations.cjs.map +1 -1
- package/dist/tsup/utils.cjs +2 -2
- package/dist/tsup/utils.cjs.map +1 -1
- package/dist/tsup/utils.js +1 -1
- package/dist/tsup/workflow/mod.cjs +7 -7
- package/dist/tsup/workflow/mod.cjs.map +1 -1
- package/dist/tsup/workflow/mod.d.cts +4 -3
- package/dist/tsup/workflow/mod.d.ts +4 -3
- package/dist/tsup/workflow/mod.js +4 -4
- package/package.json +10 -9
- package/src/client/actor-conn.ts +34 -1
- package/src/client/actor-handle.ts +29 -1
- package/src/client/auth.ts +37 -0
- package/src/client/client.ts +10 -2
- package/src/client/config.ts +12 -1
- package/src/client/mod.ts +9 -0
- package/src/client/token-provider.test.ts +86 -0
- package/src/client/token-provider.ts +93 -0
- package/src/client/utils.ts +1 -0
- package/src/engine-client/actor-websocket-client.ts +2 -1
- package/src/engine-client/api-endpoints.ts +28 -0
- package/src/engine-client/driver.ts +4 -0
- package/src/engine-client/mod.ts +201 -64
- package/src/registry/config/envoy.ts +1 -17
- package/src/registry/config/index.test.ts +2 -2
- package/src/registry/config/index.ts +7 -2
- package/src/registry/index.ts +28 -0
- package/dist/tsup/chunk-2CE5UNWK.cjs.map +0 -1
- package/dist/tsup/chunk-2NXLTFWI.js.map +0 -1
- package/dist/tsup/chunk-3WS7Z64U.cjs.map +0 -1
- package/dist/tsup/chunk-5G2J26VM.js.map +0 -1
- package/dist/tsup/chunk-76YTS32R.cjs.map +0 -1
- package/dist/tsup/chunk-A36FMKOV.cjs.map +0 -1
- package/dist/tsup/chunk-CVLNRT7O.cjs.map +0 -1
- package/dist/tsup/chunk-DHWZTUKA.js.map +0 -1
- package/dist/tsup/chunk-IHKDFKM4.cjs.map +0 -1
- package/dist/tsup/chunk-RSIOGT2U.cjs.map +0 -1
- package/dist/tsup/chunk-WAISW7KR.cjs.map +0 -1
- /package/dist/tsup/{chunk-553CIQJY.js.map → chunk-2CI2IEVN.js.map} +0 -0
- /package/dist/tsup/{chunk-W2PXGWMR.js.map → chunk-5EGLHQJJ.js.map} +0 -0
- /package/dist/tsup/{chunk-DMQUERRN.js.map → chunk-5IBUZFLP.js.map} +0 -0
- /package/dist/tsup/{chunk-SNNVRHLX.js.map → chunk-CJXBGTVT.js.map} +0 -0
- /package/dist/tsup/{chunk-TZVAVPGJ.js.map → chunk-VMMXXVDO.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rivetkit",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.21",
|
|
4
4
|
"description": "Lightweight libraries for building stateful actors on edge platforms",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -213,14 +213,15 @@
|
|
|
213
213
|
"@rivet-dev/agent-os-core": "^0.1.1",
|
|
214
214
|
"@rivet-dev/services": "^0.1.5",
|
|
215
215
|
"@rivetkit/bare-ts": "^0.6.2",
|
|
216
|
-
"@rivetkit/engine-
|
|
217
|
-
"@rivetkit/engine-
|
|
216
|
+
"@rivetkit/engine-api-full": "2.3.21",
|
|
217
|
+
"@rivetkit/engine-cli": "2.3.21",
|
|
218
|
+
"@rivetkit/engine-envoy-protocol": "2.3.21",
|
|
218
219
|
"@rivetkit/on-change": "6.0.1",
|
|
219
|
-
"@rivetkit/rivetkit-napi": "2.3.
|
|
220
|
-
"@rivetkit/rivetkit-wasm": "2.3.
|
|
221
|
-
"@rivetkit/traces": "2.3.
|
|
222
|
-
"@rivetkit/virtual-websocket": "2.3.
|
|
223
|
-
"@rivetkit/workflow-engine": "2.3.
|
|
220
|
+
"@rivetkit/rivetkit-napi": "2.3.21",
|
|
221
|
+
"@rivetkit/rivetkit-wasm": "2.3.21",
|
|
222
|
+
"@rivetkit/traces": "2.3.21",
|
|
223
|
+
"@rivetkit/virtual-websocket": "2.3.21",
|
|
224
|
+
"@rivetkit/workflow-engine": "2.3.21",
|
|
224
225
|
"cbor-x": "^1.6.0",
|
|
225
226
|
"drizzle-orm": "0.45.2",
|
|
226
227
|
"hono": "^4.7.0",
|
|
@@ -270,7 +271,7 @@
|
|
|
270
271
|
"stableVersion": "0.8.0",
|
|
271
272
|
"repository": {
|
|
272
273
|
"type": "git",
|
|
273
|
-
"url": "https://github.com/rivet-dev/
|
|
274
|
+
"url": "https://github.com/rivet-dev/rivet.git",
|
|
274
275
|
"directory": "rivetkit-typescript/packages/rivetkit"
|
|
275
276
|
}
|
|
276
277
|
}
|
package/src/client/actor-conn.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { ACTOR_CONNS_SYMBOL, type ClientRaw } from "./client";
|
|
|
51
51
|
import * as errors from "./errors";
|
|
52
52
|
import { isRetryableLifecycleReconnectSignal } from "./lifecycle-errors";
|
|
53
53
|
import { logger } from "./log";
|
|
54
|
+
import { isInvalidToken } from "./token-provider";
|
|
54
55
|
import { outboundTelemetryHeaders } from "./outbound-telemetry";
|
|
55
56
|
import {
|
|
56
57
|
createQueueSender,
|
|
@@ -183,6 +184,7 @@ export class ActorConnRaw {
|
|
|
183
184
|
#onOpenPromise?: ReturnType<typeof promiseWithResolvers<undefined>>;
|
|
184
185
|
|
|
185
186
|
#websocket?: UniversalWebSocket;
|
|
187
|
+
#authRetryAvailable = true;
|
|
186
188
|
|
|
187
189
|
#client: ClientRaw;
|
|
188
190
|
#driver: EngineControlClient;
|
|
@@ -531,6 +533,15 @@ export class ActorConnRaw {
|
|
|
531
533
|
if (error instanceof errors.ActorConnDisposed) {
|
|
532
534
|
return false;
|
|
533
535
|
}
|
|
536
|
+
if (
|
|
537
|
+
error instanceof errors.ActorError &&
|
|
538
|
+
isInvalidToken(error.group, error.code) &&
|
|
539
|
+
this.#driver.refreshAuthToken &&
|
|
540
|
+
this.#authRetryAvailable
|
|
541
|
+
) {
|
|
542
|
+
this.#authRetryAvailable = false;
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
534
545
|
|
|
535
546
|
if (
|
|
536
547
|
error instanceof errors.ActorError &&
|
|
@@ -679,6 +690,7 @@ export class ActorConnRaw {
|
|
|
679
690
|
});
|
|
680
691
|
|
|
681
692
|
// Update connection state (this also notifies handlers)
|
|
693
|
+
this.#authRetryAvailable = true;
|
|
682
694
|
this.#setConnStatus("connected");
|
|
683
695
|
|
|
684
696
|
// Resolve open promise
|
|
@@ -853,6 +865,7 @@ export class ActorConnRaw {
|
|
|
853
865
|
|
|
854
866
|
/** Called by the onclose event from drivers. */
|
|
855
867
|
async #handleOnClose(event: Event | CloseEventLike) {
|
|
868
|
+
const failedSocket = this.#websocket;
|
|
856
869
|
// We can't use `event instanceof CloseEvent` because it's not defined in NodeJS
|
|
857
870
|
const closeEvent = event as CloseEventLike;
|
|
858
871
|
const wasClean = closeEvent.wasClean;
|
|
@@ -882,6 +895,26 @@ export class ActorConnRaw {
|
|
|
882
895
|
|
|
883
896
|
if (parsed) {
|
|
884
897
|
const { group, code, rayId } = parsed;
|
|
898
|
+
if (
|
|
899
|
+
isInvalidToken(group, code) &&
|
|
900
|
+
closeEvent.code === 1008 &&
|
|
901
|
+
failedSocket
|
|
902
|
+
) {
|
|
903
|
+
try {
|
|
904
|
+
if (
|
|
905
|
+
!(await this.#driver.refreshAuthToken?.(
|
|
906
|
+
failedSocket,
|
|
907
|
+
))
|
|
908
|
+
) {
|
|
909
|
+
this.#authRetryAvailable = false;
|
|
910
|
+
}
|
|
911
|
+
} catch {
|
|
912
|
+
logger().warn("token renewal failed");
|
|
913
|
+
this.#authRetryAvailable = false;
|
|
914
|
+
}
|
|
915
|
+
} else if (isInvalidToken(group, code)) {
|
|
916
|
+
this.#authRetryAvailable = false;
|
|
917
|
+
}
|
|
885
918
|
|
|
886
919
|
if (this.#shouldReconnectForStaleActor(group, code)) {
|
|
887
920
|
this.#clearResolvedActorIdentity();
|
|
@@ -941,7 +974,7 @@ export class ActorConnRaw {
|
|
|
941
974
|
}
|
|
942
975
|
|
|
943
976
|
// Automatically reconnect if we were connected
|
|
944
|
-
if (wasConnected) {
|
|
977
|
+
if (wasConnected && this.#authRetryAvailable) {
|
|
945
978
|
logger().debug({
|
|
946
979
|
msg: "triggering reconnect",
|
|
947
980
|
connId: this.#connId,
|
|
@@ -25,6 +25,13 @@ import { AsyncMutex } from "@/common/database/shared";
|
|
|
25
25
|
import type { Encoding, JsonCompatValue } from "@/common/encoding";
|
|
26
26
|
import { deconstructError } from "@/common/utils";
|
|
27
27
|
import type { EngineControlClient } from "@/engine-client/driver";
|
|
28
|
+
import type {
|
|
29
|
+
ActorIssueTokenOptions,
|
|
30
|
+
ActorTokenPermissions,
|
|
31
|
+
ActorTokenResource,
|
|
32
|
+
IssuedToken,
|
|
33
|
+
TokenGrant,
|
|
34
|
+
} from "@/client/auth";
|
|
28
35
|
import type { StartCallSpan, CurrentActorInvocation } from "@/registry/runtime";
|
|
29
36
|
import {
|
|
30
37
|
decodeCborCompat,
|
|
@@ -993,6 +1000,27 @@ export class ActorHandleRaw {
|
|
|
993
1000
|
);
|
|
994
1001
|
}
|
|
995
1002
|
|
|
1003
|
+
/** Issue a token scoped to this actor. Defaults to actor gateway read access. */
|
|
1004
|
+
async issueToken(options: ActorIssueTokenOptions = {}): Promise<IssuedToken> {
|
|
1005
|
+
const actorId = await this.resolve();
|
|
1006
|
+
const permissions: ActorTokenPermissions = options.permissions ?? {
|
|
1007
|
+
actor_gateway: ["read"],
|
|
1008
|
+
};
|
|
1009
|
+
const grants: TokenGrant[] = (
|
|
1010
|
+
Object.keys(permissions) as ActorTokenResource[]
|
|
1011
|
+
).flatMap((resource) => {
|
|
1012
|
+
const operations = permissions[resource];
|
|
1013
|
+
return operations
|
|
1014
|
+
? [{ resource, target: { id: actorId }, operations }]
|
|
1015
|
+
: [];
|
|
1016
|
+
});
|
|
1017
|
+
return this.#driver.issueToken({
|
|
1018
|
+
subject: options.subject,
|
|
1019
|
+
expiresIn: options.expiresIn,
|
|
1020
|
+
grants,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
|
|
996
1024
|
/**
|
|
997
1025
|
* Returns the raw URL for routing traffic to the actor.
|
|
998
1026
|
*/
|
|
@@ -1048,4 +1076,4 @@ export type ActorHandle<AD extends AnyActorDefinition> = Omit<
|
|
|
1048
1076
|
// Resolve method returns the actor ID
|
|
1049
1077
|
resolve(): Promise<string>;
|
|
1050
1078
|
} & ActorDefinitionQueueSend<AD> &
|
|
1051
|
-
ActorDefinitionActions<AD>;
|
|
1079
|
+
Omit<ActorDefinitionActions<AD>, keyof ActorHandleRaw>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Rivet } from "@rivetkit/engine-api-full";
|
|
2
|
+
|
|
3
|
+
/** A namespace-scoped Engine grant. The caller must provide every grant explicitly. */
|
|
4
|
+
export type TokenGrant = Rivet.AuthTokenGrant;
|
|
5
|
+
|
|
6
|
+
/** Options for `client.auth.issueToken()`. `expiresIn` is in seconds. */
|
|
7
|
+
export type IssueTokenOptions = Pick<
|
|
8
|
+
Rivet.AuthTokenCreateRequest,
|
|
9
|
+
"subject" | "grants"
|
|
10
|
+
> & {
|
|
11
|
+
expiresIn?: Rivet.AuthTokenCreateRequest["duration"];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Token timestamps are Unix milliseconds. */
|
|
15
|
+
export interface IssuedToken {
|
|
16
|
+
token: Rivet.AuthTokenCreateResponse["token"];
|
|
17
|
+
issuedAt: Rivet.AuthTokenCreateResponse["issuedTs"];
|
|
18
|
+
expiresAt: Rivet.AuthTokenCreateResponse["expiresTs"];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resources that can be scoped to a single actor ID. */
|
|
22
|
+
export type ActorTokenResource = Extract<
|
|
23
|
+
Rivet.AuthTokenGrant["resource"],
|
|
24
|
+
"actor_gateway" | "actor" | "actor_kv"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export type ActorTokenPermissions = Partial<
|
|
28
|
+
Record<ActorTokenResource, Rivet.AuthTokenGrant["operations"]>
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
/** Options for `actor.issueToken()`. Omit permissions for gateway read access only. */
|
|
32
|
+
export type ActorIssueTokenOptions = Pick<
|
|
33
|
+
IssueTokenOptions,
|
|
34
|
+
"subject" | "expiresIn"
|
|
35
|
+
> & {
|
|
36
|
+
permissions?: ActorTokenPermissions;
|
|
37
|
+
};
|
package/src/client/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnyActorDefinition } from "@/actor/definition";
|
|
2
|
+
import type { IssuedToken, IssueTokenOptions } from "@/client/auth";
|
|
2
3
|
import type { ActorQuery } from "@/client/query";
|
|
3
4
|
import type { Encoding } from "@/common/encoding";
|
|
4
5
|
import type { EngineControlClient } from "@/engine-client/driver";
|
|
@@ -198,6 +199,10 @@ export interface ActorClientRuntimeOptions {
|
|
|
198
199
|
*/
|
|
199
200
|
export class ClientRaw {
|
|
200
201
|
#disposed = false;
|
|
202
|
+
/** Issue namespace-scoped tokens with an explicit grant list. */
|
|
203
|
+
readonly auth: {
|
|
204
|
+
issueToken(options: IssueTokenOptions): Promise<IssuedToken>;
|
|
205
|
+
};
|
|
201
206
|
|
|
202
207
|
[ACTOR_CONNS_SYMBOL] = new Set<ActorConnRaw>();
|
|
203
208
|
|
|
@@ -215,6 +220,9 @@ export class ClientRaw {
|
|
|
215
220
|
options: ClientRawOptions & ActorClientRuntimeOptions = {},
|
|
216
221
|
) {
|
|
217
222
|
this.#driver = driver;
|
|
223
|
+
this.auth = {
|
|
224
|
+
issueToken: (issueOptions) => this.#driver.issueToken(issueOptions),
|
|
225
|
+
};
|
|
218
226
|
|
|
219
227
|
this.#encodingKind = options.encoding ?? "bare";
|
|
220
228
|
this.#gatewayOptions = options.gateway ?? {};
|
|
@@ -469,11 +477,11 @@ export class ClientRaw {
|
|
|
469
477
|
*
|
|
470
478
|
* @template A The actor registry type.
|
|
471
479
|
*/
|
|
472
|
-
export type Client<A extends Registry<any>> = ClientRaw & {
|
|
480
|
+
export type Client<A extends Registry<any>> = ClientRaw & Omit<{
|
|
473
481
|
[K in keyof ExtractActorsFromRegistry<A>]: ActorAccessor<
|
|
474
482
|
ExtractActorsFromRegistry<A>[K]
|
|
475
483
|
>;
|
|
476
|
-
}
|
|
484
|
+
}, keyof ClientRaw>;
|
|
477
485
|
|
|
478
486
|
export type AnyClient = Client<Registry<any>>;
|
|
479
487
|
|
package/src/client/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from "zod/v4";
|
|
2
2
|
import { EncodingSchema } from "@/common/encoding";
|
|
3
|
+
import { isLocalEngineEndpoint } from "@/common/engine";
|
|
3
4
|
import type { RegistryConfig } from "@/registry/config";
|
|
4
5
|
import type { GetUpgradeWebSocket } from "@/utils";
|
|
5
6
|
import { tryParseEndpoint } from "@/utils/endpoint-parser";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
getRivetPool,
|
|
11
12
|
getRivetToken,
|
|
12
13
|
} from "@/utils/env-vars";
|
|
14
|
+
import type { GetToken } from "./token-provider";
|
|
13
15
|
|
|
14
16
|
const DEFAULT_ENDPOINT = "http://localhost:6420";
|
|
15
17
|
export const DEFAULT_MAX_QUERY_INPUT_SIZE = 4 * 1024;
|
|
@@ -54,6 +56,9 @@ export const ClientConfigSchemaBase = z.object({
|
|
|
54
56
|
.optional()
|
|
55
57
|
.transform((val) => val ?? getRivetToken()),
|
|
56
58
|
|
|
59
|
+
/** Issue and renew a client token on demand. Overrides the static token when provided. */
|
|
60
|
+
getToken: z.custom<GetToken>().optional(),
|
|
61
|
+
|
|
57
62
|
/** Namespace to connect to. */
|
|
58
63
|
namespace: z
|
|
59
64
|
.string()
|
|
@@ -131,7 +136,12 @@ export function transformClientConfig(
|
|
|
131
136
|
...config,
|
|
132
137
|
endpoint: parsedEndpoint?.endpoint,
|
|
133
138
|
namespace: parsedEndpoint?.namespace ?? config.namespace ?? "default",
|
|
134
|
-
token:
|
|
139
|
+
token:
|
|
140
|
+
parsedEndpoint?.token ??
|
|
141
|
+
config.token ??
|
|
142
|
+
(config.endpoint && isLocalEngineEndpoint(config.endpoint)
|
|
143
|
+
? "default"
|
|
144
|
+
: undefined),
|
|
135
145
|
};
|
|
136
146
|
}
|
|
137
147
|
|
|
@@ -151,6 +161,7 @@ export function convertRegistryConfigToClientConfig(
|
|
|
151
161
|
return {
|
|
152
162
|
endpoint: config.endpoint,
|
|
153
163
|
token: config.token,
|
|
164
|
+
getToken: undefined,
|
|
154
165
|
namespace: config.namespace,
|
|
155
166
|
poolName: config.envoy.poolName,
|
|
156
167
|
headers: config.headers,
|
package/src/client/mod.ts
CHANGED
|
@@ -19,6 +19,14 @@ export {
|
|
|
19
19
|
UserError,
|
|
20
20
|
} from "@/client/errors";
|
|
21
21
|
export type { CreateRequest } from "@/client/query";
|
|
22
|
+
export type {
|
|
23
|
+
ActorIssueTokenOptions,
|
|
24
|
+
ActorTokenPermissions,
|
|
25
|
+
ActorTokenResource,
|
|
26
|
+
IssuedToken,
|
|
27
|
+
IssueTokenOptions,
|
|
28
|
+
TokenGrant,
|
|
29
|
+
} from "./auth";
|
|
22
30
|
export type { Encoding } from "@/common/encoding";
|
|
23
31
|
export type {
|
|
24
32
|
ActorActionFunction,
|
|
@@ -49,6 +57,7 @@ export type {
|
|
|
49
57
|
QueryOptions,
|
|
50
58
|
Region,
|
|
51
59
|
} from "./client";
|
|
60
|
+
export type { GetToken } from "./token-provider";
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
63
|
* Creates a client with the actor accessor proxy.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isInvalidToken,
|
|
4
|
+
isInvalidTokenResponse,
|
|
5
|
+
TokenProvider,
|
|
6
|
+
} from "./token-provider";
|
|
7
|
+
|
|
8
|
+
describe("renewable client tokens", () => {
|
|
9
|
+
test("single-flights initial issuance and concurrent rejection refreshes", async () => {
|
|
10
|
+
const getToken = vi.fn(
|
|
11
|
+
async ({ forceRefresh }: { forceRefresh: boolean }) =>
|
|
12
|
+
forceRefresh ? "renewed" : "initial",
|
|
13
|
+
);
|
|
14
|
+
const provider = new TokenProvider(getToken);
|
|
15
|
+
expect(
|
|
16
|
+
await Promise.all([provider.current(), provider.current()]),
|
|
17
|
+
).toEqual(["initial", "initial"]);
|
|
18
|
+
expect(getToken).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(
|
|
20
|
+
await Promise.all([
|
|
21
|
+
provider.refreshIfCurrent("initial"),
|
|
22
|
+
provider.refreshIfCurrent("initial"),
|
|
23
|
+
]),
|
|
24
|
+
).toEqual(["renewed", "renewed"]);
|
|
25
|
+
expect(getToken).toHaveBeenCalledTimes(2);
|
|
26
|
+
expect(await provider.refreshIfCurrent("initial")).toBe("renewed");
|
|
27
|
+
expect(getToken).toHaveBeenCalledTimes(2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("retries failed issuance and refuses empty credentials", async () => {
|
|
31
|
+
const getToken = vi
|
|
32
|
+
.fn()
|
|
33
|
+
.mockRejectedValueOnce(new Error("issuer down"))
|
|
34
|
+
.mockResolvedValueOnce("ok");
|
|
35
|
+
const provider = new TokenProvider(getToken);
|
|
36
|
+
await expect(provider.current()).rejects.toThrow("issuer down");
|
|
37
|
+
expect(await provider.current()).toBe("ok");
|
|
38
|
+
await expect(
|
|
39
|
+
new TokenProvider(async () => "").current(),
|
|
40
|
+
).rejects.toThrow("nonempty");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("refreshes before a known JWT expiry and treats opaque tokens as unexpiring hints", async () => {
|
|
44
|
+
const payload = btoa(
|
|
45
|
+
JSON.stringify({ exp: Math.floor(Date.now() / 1_000) + 1 }),
|
|
46
|
+
).replaceAll("=", "");
|
|
47
|
+
const getToken = vi.fn(async () => `header.${payload}.signature`);
|
|
48
|
+
const provider = new TokenProvider(getToken);
|
|
49
|
+
await provider.current();
|
|
50
|
+
await provider.current();
|
|
51
|
+
expect(getToken).toHaveBeenCalledTimes(2);
|
|
52
|
+
const opaque = vi.fn(async () => "opaque");
|
|
53
|
+
const opaqueProvider = new TokenProvider(opaque);
|
|
54
|
+
await opaqueProvider.current();
|
|
55
|
+
await opaqueProvider.current();
|
|
56
|
+
expect(opaque).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("only confirmed 401 invalid/expired errors permit renewal", () => {
|
|
60
|
+
expect(isInvalidToken("auth", "invalid_token")).toBe(true);
|
|
61
|
+
expect(isInvalidToken("auth", "token_expired")).toBe(true);
|
|
62
|
+
for (const code of [
|
|
63
|
+
"insufficient_permissions",
|
|
64
|
+
"verification_unavailable",
|
|
65
|
+
"issuance_unavailable",
|
|
66
|
+
]) {
|
|
67
|
+
expect(isInvalidToken("auth", code)).toBe(false);
|
|
68
|
+
}
|
|
69
|
+
expect(
|
|
70
|
+
isInvalidTokenResponse(
|
|
71
|
+
new Response(null, {
|
|
72
|
+
status: 401,
|
|
73
|
+
headers: { "x-rivet-error": "auth.token_expired" },
|
|
74
|
+
}),
|
|
75
|
+
),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
expect(
|
|
78
|
+
isInvalidTokenResponse(
|
|
79
|
+
new Response(null, {
|
|
80
|
+
status: 503,
|
|
81
|
+
headers: { "x-rivet-error": "auth.token_expired" },
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Called when RivetKit needs a credential. Keep issuer secrets on your backend. */
|
|
2
|
+
export type GetToken = (options: { forceRefresh: boolean }) => Promise<string>;
|
|
3
|
+
|
|
4
|
+
/** Only a confirmed invalid/expired credential can trigger an automatic renewal. */
|
|
5
|
+
export function isInvalidToken(group: string, code: string): boolean {
|
|
6
|
+
return (
|
|
7
|
+
group === "auth" &&
|
|
8
|
+
(code === "invalid_token" || code === "token_expired")
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isInvalidTokenResponse(response: Response): boolean {
|
|
13
|
+
if (response.status !== 401) return false;
|
|
14
|
+
const error = response.headers.get("x-rivet-error");
|
|
15
|
+
if (!error) return false;
|
|
16
|
+
const separator = error.indexOf(".");
|
|
17
|
+
return (
|
|
18
|
+
separator > 0 &&
|
|
19
|
+
isInvalidToken(error.slice(0, separator), error.slice(separator + 1))
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One token cache per client; concurrent requests and refreshes share the same issuance call. */
|
|
24
|
+
export class TokenProvider {
|
|
25
|
+
#cached?: { token: string; expiresAt?: number };
|
|
26
|
+
#pending?: Promise<string>;
|
|
27
|
+
#pendingForce = false;
|
|
28
|
+
|
|
29
|
+
constructor(private readonly getToken: GetToken) {}
|
|
30
|
+
|
|
31
|
+
async current(): Promise<string> {
|
|
32
|
+
if (
|
|
33
|
+
this.#cached &&
|
|
34
|
+
(this.#cached.expiresAt === undefined ||
|
|
35
|
+
Date.now() < this.#cached.expiresAt - 5_000)
|
|
36
|
+
) {
|
|
37
|
+
return this.#cached.token;
|
|
38
|
+
}
|
|
39
|
+
return this.#load(false);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A late rejection for an older token must never erase a newer credential. */
|
|
43
|
+
async refreshIfCurrent(failedToken: string): Promise<string> {
|
|
44
|
+
if (this.#pending !== undefined) {
|
|
45
|
+
const wasForced = this.#pendingForce;
|
|
46
|
+
const refreshed = await this.#pending;
|
|
47
|
+
if (wasForced || refreshed !== failedToken) return refreshed;
|
|
48
|
+
}
|
|
49
|
+
if (this.#cached?.token !== failedToken) {
|
|
50
|
+
return this.current();
|
|
51
|
+
}
|
|
52
|
+
return this.#load(true);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#load(forceRefresh: boolean): Promise<string> {
|
|
56
|
+
if (this.#pending !== undefined) return this.#pending;
|
|
57
|
+
|
|
58
|
+
const pending = Promise.resolve()
|
|
59
|
+
.then(() => this.getToken({ forceRefresh }))
|
|
60
|
+
.then((token) => {
|
|
61
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"getToken must resolve to a nonempty string",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
this.#cached = { token, expiresAt: jwtExpiration(token) };
|
|
67
|
+
return token;
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
if (this.#pending === pending) this.#pending = undefined;
|
|
71
|
+
});
|
|
72
|
+
this.#pending = pending;
|
|
73
|
+
this.#pendingForce = forceRefresh;
|
|
74
|
+
return pending;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// This is only a cache hint, never a signature or authorization check. The Engine verifies JWTs.
|
|
79
|
+
function jwtExpiration(token: string): number | undefined {
|
|
80
|
+
try {
|
|
81
|
+
const payload = token.split(".")[1];
|
|
82
|
+
if (!payload) return undefined;
|
|
83
|
+
const claims = JSON.parse(
|
|
84
|
+
atob(payload.replace(/-/g, "+").replace(/_/g, "/")),
|
|
85
|
+
) as { exp?: unknown };
|
|
86
|
+
return typeof claims.exp === "number" &&
|
|
87
|
+
Number.isSafeInteger(claims.exp)
|
|
88
|
+
? claims.exp * 1_000
|
|
89
|
+
: undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/client/utils.ts
CHANGED
|
@@ -336,7 +336,8 @@ export async function openWebSocketToGateway(
|
|
|
336
336
|
|
|
337
337
|
logger().debug({
|
|
338
338
|
msg: "opening websocket to actor via guard",
|
|
339
|
-
|
|
339
|
+
// Gateway URLs can embed credentials; never include the path or query in logs.
|
|
340
|
+
origin: new URL(gatewayUrl).origin,
|
|
340
341
|
});
|
|
341
342
|
|
|
342
343
|
// Create WebSocket connection
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { serializeActorKey } from "@/actor/keys";
|
|
2
2
|
import type { ClientConfig } from "@/client/client";
|
|
3
|
+
import type { IssuedToken, IssueTokenOptions } from "@/client/auth";
|
|
3
4
|
import type { MetadataResponse } from "@/common/router";
|
|
4
5
|
import type {
|
|
5
6
|
ActorsCreateRequest,
|
|
@@ -12,6 +13,33 @@ import type {
|
|
|
12
13
|
import type { RivetId } from "@/engine-api/common";
|
|
13
14
|
import { apiCall } from "./api-utils";
|
|
14
15
|
|
|
16
|
+
/** Engine's JSON response uses snake_case; the public result uses camelCase. */
|
|
17
|
+
export type IssueTokenResponse = {
|
|
18
|
+
token: IssuedToken["token"];
|
|
19
|
+
issued_ts: IssuedToken["issuedAt"];
|
|
20
|
+
expires_ts: IssuedToken["expiresAt"];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function issueToken(
|
|
24
|
+
config: ClientConfig,
|
|
25
|
+
options: IssueTokenOptions,
|
|
26
|
+
): Promise<IssueTokenResponse> {
|
|
27
|
+
return apiCall<
|
|
28
|
+
{
|
|
29
|
+
namespace: string;
|
|
30
|
+
subject?: IssueTokenOptions["subject"];
|
|
31
|
+
duration?: IssueTokenOptions["expiresIn"];
|
|
32
|
+
grants: IssueTokenOptions["grants"];
|
|
33
|
+
},
|
|
34
|
+
IssueTokenResponse
|
|
35
|
+
>(config, "POST", "/auth/tokens", {
|
|
36
|
+
namespace: config.namespace,
|
|
37
|
+
subject: options.subject,
|
|
38
|
+
duration: options.expiresIn,
|
|
39
|
+
grants: options.grants,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
// MARK: Get actor
|
|
16
44
|
export async function getActor(
|
|
17
45
|
config: ClientConfig,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Hono, Context as HonoContext } from "hono";
|
|
2
2
|
import type { ActorKey, Encoding, UniversalWebSocket } from "@/actor/mod";
|
|
3
3
|
import type { ActorQuery, CrashPolicy } from "@/client/query";
|
|
4
|
+
import type { IssuedToken, IssueTokenOptions } from "@/client/auth";
|
|
4
5
|
import type { RegistryConfig } from "@/registry/config";
|
|
5
6
|
import type { GetUpgradeWebSocket } from "@/utils";
|
|
6
7
|
|
|
@@ -17,6 +18,7 @@ export function shouldSkipReadyWait(
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export interface EngineControlClient {
|
|
21
|
+
issueToken(options: IssueTokenOptions): Promise<IssuedToken>;
|
|
20
22
|
getForId(input: GetForIdInput): Promise<ActorOutput | undefined>;
|
|
21
23
|
getWithKey(input: GetWithKeyInput): Promise<ActorOutput | undefined>;
|
|
22
24
|
getOrCreateWithKey(input: GetOrCreateWithKeyInput): Promise<ActorOutput>;
|
|
@@ -35,6 +37,8 @@ export interface EngineControlClient {
|
|
|
35
37
|
params: unknown,
|
|
36
38
|
options?: GatewayRequestOptions,
|
|
37
39
|
): Promise<UniversalWebSocket>;
|
|
40
|
+
/** Refresh a credential rejected during this WebSocket handshake, if supported. */
|
|
41
|
+
refreshAuthToken?(ws: UniversalWebSocket): Promise<boolean>;
|
|
38
42
|
proxyRequest(
|
|
39
43
|
c: HonoContext,
|
|
40
44
|
actorRequest: Request,
|