rivetkit 2.3.20 → 2.3.22
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 +315 -63
- 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 +2 -2
- package/dist/tsup/actor/errors.cjs.map +1 -1
- package/dist/tsup/actor/errors.d.cts +1 -1
- package/dist/tsup/actor/errors.d.ts +1 -1
- package/dist/tsup/actor/errors.js +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-47FSGXYN.js} +2 -2
- package/dist/tsup/{chunk-2NXLTFWI.js → chunk-4DTWJFIZ.js} +3 -3
- package/dist/tsup/chunk-4DTWJFIZ.js.map +1 -0
- package/dist/tsup/{chunk-2CE5UNWK.cjs → chunk-BDKVTNJF.cjs} +11 -10
- package/dist/tsup/chunk-BDKVTNJF.cjs.map +1 -0
- package/dist/tsup/{chunk-DHWZTUKA.js → chunk-DE2IFYJK.js} +4 -3
- package/dist/tsup/chunk-DE2IFYJK.js.map +1 -0
- package/dist/tsup/{chunk-DMQUERRN.js → chunk-GINS4O73.js} +2 -2
- package/dist/tsup/{chunk-3WS7Z64U.cjs → chunk-GZZNJGXY.cjs} +3 -3
- package/dist/tsup/chunk-GZZNJGXY.cjs.map +1 -0
- package/dist/tsup/{chunk-A36FMKOV.cjs → chunk-HEY27WZX.cjs} +479 -230
- package/dist/tsup/chunk-HEY27WZX.cjs.map +1 -0
- package/dist/tsup/{chunk-WAISW7KR.cjs → chunk-KEAUN5F5.cjs} +10 -10
- package/dist/tsup/chunk-KEAUN5F5.cjs.map +1 -0
- package/dist/tsup/chunk-KVFIONKQ.cjs.map +1 -1
- package/dist/tsup/{chunk-CVLNRT7O.cjs → chunk-LUMROGNM.cjs} +3 -3
- package/dist/tsup/chunk-LUMROGNM.cjs.map +1 -0
- package/dist/tsup/{chunk-JX2ITWAV.cjs → chunk-MGI2VVF6.cjs} +14 -4
- package/dist/tsup/chunk-MGI2VVF6.cjs.map +1 -0
- package/dist/tsup/chunk-MYWZCDML.cjs.map +1 -1
- package/dist/tsup/chunk-NWCAIWHL.cjs.map +1 -1
- package/dist/tsup/{chunk-RSIOGT2U.cjs → chunk-QOQA4JPN.cjs} +7 -7
- package/dist/tsup/chunk-QOQA4JPN.cjs.map +1 -0
- package/dist/tsup/{chunk-TZVAVPGJ.js → chunk-QXBNHJH5.js} +4 -4
- package/dist/tsup/{chunk-W2PXGWMR.js → chunk-RIPVNPR6.js} +2 -2
- package/dist/tsup/{chunk-5G2J26VM.js → chunk-VD47X7KQ.js} +318 -69
- package/dist/tsup/chunk-VD47X7KQ.js.map +1 -0
- package/dist/tsup/{chunk-76YTS32R.cjs → chunk-WLRGUAMW.cjs} +4 -4
- package/dist/tsup/chunk-WLRGUAMW.cjs.map +1 -0
- package/dist/tsup/{chunk-IHKDFKM4.cjs → chunk-WMNXGH25.cjs} +3 -3
- package/dist/tsup/chunk-WMNXGH25.cjs.map +1 -0
- package/dist/tsup/{chunk-SNNVRHLX.js → chunk-XZBDVEQN.js} +2 -2
- package/dist/tsup/{chunk-P2OFXH4G.js → chunk-ZOVJDAOW.js} +14 -4
- package/dist/tsup/chunk-ZOVJDAOW.js.map +1 -0
- package/dist/tsup/client/mod.cjs +6 -6
- package/dist/tsup/client/mod.cjs.map +1 -1
- package/dist/tsup/client/mod.d.cts +15 -6
- package/dist/tsup/client/mod.d.ts +15 -6
- package/dist/tsup/client/mod.js +5 -5
- package/dist/tsup/common/log.cjs +3 -3
- package/dist/tsup/common/log.cjs.map +1 -1
- package/dist/tsup/common/log.js +2 -2
- package/dist/tsup/common/websocket.cjs +4 -4
- package/dist/tsup/common/websocket.cjs.map +1 -1
- package/dist/tsup/common/websocket.js +3 -3
- package/dist/tsup/{config-Be-VOHaX.d.ts → config-Cv6pjCt-.d.ts} +37 -7
- package/dist/tsup/{config-Ct5uXxXk.d.cts → config-osdJqQwX.d.cts} +37 -7
- package/dist/tsup/{context-CXLa0BIM.d.ts → context-CoVbKBuk.d.ts} +1 -1
- package/dist/tsup/{context-Cw-11Q-B.d.cts → context-DpjrM7n7.d.cts} +1 -1
- package/dist/tsup/db/drizzle.cjs +3 -3
- package/dist/tsup/db/drizzle.cjs.map +1 -1
- package/dist/tsup/db/drizzle.js +2 -2
- package/dist/tsup/db/mod.cjs +4 -4
- package/dist/tsup/db/mod.cjs.map +1 -1
- package/dist/tsup/db/mod.js +3 -3
- package/dist/tsup/dynamic/mod.cjs.map +1 -1
- package/dist/tsup/dynamic/mod.d.cts +4 -3
- package/dist/tsup/dynamic/mod.d.ts +4 -3
- package/dist/tsup/inspector/mod.cjs +6 -6
- package/dist/tsup/inspector/mod.cjs.map +1 -1
- package/dist/tsup/inspector/mod.js +5 -5
- package/dist/tsup/inspector/workflow.cjs +5 -5
- package/dist/tsup/inspector/workflow.cjs.map +1 -1
- package/dist/tsup/inspector/workflow.js +4 -4
- package/dist/tsup/inspector-tab/mod.cjs.map +1 -1
- package/dist/tsup/inspector-tab/mod.d.cts +5 -4
- package/dist/tsup/inspector-tab/mod.d.ts +5 -4
- package/dist/tsup/mod.cjs +170 -158
- package/dist/tsup/mod.cjs.map +1 -1
- package/dist/tsup/mod.d.cts +6 -5
- package/dist/tsup/mod.d.ts +6 -5
- package/dist/tsup/mod.js +56 -44
- package/dist/tsup/mod.js.map +1 -1
- package/dist/tsup/test/mod.cjs +7 -7
- package/dist/tsup/test/mod.cjs.map +1 -1
- package/dist/tsup/test/mod.d.cts +4 -3
- package/dist/tsup/test/mod.d.ts +4 -3
- package/dist/tsup/test/mod.js +5 -5
- package/dist/tsup/unstable/migrations.cjs.map +1 -1
- package/dist/tsup/{utils-DsoDinPz.d.ts → utils-3rsE0JvI.d.cts} +12 -2
- package/dist/tsup/{utils-DsoDinPz.d.cts → utils-3rsE0JvI.d.ts} +12 -2
- package/dist/tsup/{utils-OQHBjfcM.d.ts → utils-C1O-SklO.d.ts} +1 -1
- package/dist/tsup/{utils-CNFeS-Dk.d.cts → utils-C6b0aYUM.d.cts} +1 -1
- package/dist/tsup/utils.cjs +3 -3
- package/dist/tsup/utils.cjs.map +1 -1
- package/dist/tsup/utils.d.cts +2 -2
- package/dist/tsup/utils.d.ts +2 -2
- package/dist/tsup/utils.js +2 -2
- package/dist/tsup/workflow/mod.cjs +11 -11
- package/dist/tsup/workflow/mod.cjs.map +1 -1
- package/dist/tsup/workflow/mod.d.cts +6 -5
- package/dist/tsup/workflow/mod.d.ts +6 -5
- package/dist/tsup/workflow/mod.js +5 -5
- package/package.json +10 -9
- package/src/actor/errors.ts +27 -4
- 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/src/registry/native.ts +31 -3
- 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-JX2ITWAV.cjs.map +0 -1
- package/dist/tsup/chunk-P2OFXH4G.js.map +0 -1
- package/dist/tsup/chunk-RSIOGT2U.cjs.map +0 -1
- package/dist/tsup/chunk-WAISW7KR.cjs.map +0 -1
- package/src/registry/process-metrics.ts +0 -250
- /package/dist/tsup/{chunk-553CIQJY.js.map → chunk-47FSGXYN.js.map} +0 -0
- /package/dist/tsup/{chunk-DMQUERRN.js.map → chunk-GINS4O73.js.map} +0 -0
- /package/dist/tsup/{chunk-TZVAVPGJ.js.map → chunk-QXBNHJH5.js.map} +0 -0
- /package/dist/tsup/{chunk-W2PXGWMR.js.map → chunk-RIPVNPR6.js.map} +0 -0
- /package/dist/tsup/{chunk-SNNVRHLX.js.map → chunk-XZBDVEQN.js.map} +0 -0
|
@@ -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,
|