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.
Files changed (148) hide show
  1. package/dist/browser/client.d.ts +43 -5
  2. package/dist/browser/client.js +315 -63
  3. package/dist/browser/client.js.map +1 -1
  4. package/dist/browser/inspector/client.js +2 -1
  5. package/dist/browser/inspector/client.js.map +1 -1
  6. package/dist/tsup/actor/errors.cjs +2 -2
  7. package/dist/tsup/actor/errors.cjs.map +1 -1
  8. package/dist/tsup/actor/errors.d.cts +1 -1
  9. package/dist/tsup/actor/errors.d.ts +1 -1
  10. package/dist/tsup/actor/errors.js +1 -1
  11. package/dist/tsup/agent-os/index.cjs +2 -1
  12. package/dist/tsup/agent-os/index.cjs.map +1 -1
  13. package/dist/tsup/agent-os/index.d.cts +32 -4
  14. package/dist/tsup/agent-os/index.d.ts +32 -4
  15. package/dist/tsup/agent-os/index.js +2 -1
  16. package/dist/tsup/agent-os/index.js.map +1 -1
  17. package/dist/tsup/{chunk-553CIQJY.js → chunk-47FSGXYN.js} +2 -2
  18. package/dist/tsup/{chunk-2NXLTFWI.js → chunk-4DTWJFIZ.js} +3 -3
  19. package/dist/tsup/chunk-4DTWJFIZ.js.map +1 -0
  20. package/dist/tsup/{chunk-2CE5UNWK.cjs → chunk-BDKVTNJF.cjs} +11 -10
  21. package/dist/tsup/chunk-BDKVTNJF.cjs.map +1 -0
  22. package/dist/tsup/{chunk-DHWZTUKA.js → chunk-DE2IFYJK.js} +4 -3
  23. package/dist/tsup/chunk-DE2IFYJK.js.map +1 -0
  24. package/dist/tsup/{chunk-DMQUERRN.js → chunk-GINS4O73.js} +2 -2
  25. package/dist/tsup/{chunk-3WS7Z64U.cjs → chunk-GZZNJGXY.cjs} +3 -3
  26. package/dist/tsup/chunk-GZZNJGXY.cjs.map +1 -0
  27. package/dist/tsup/{chunk-A36FMKOV.cjs → chunk-HEY27WZX.cjs} +479 -230
  28. package/dist/tsup/chunk-HEY27WZX.cjs.map +1 -0
  29. package/dist/tsup/{chunk-WAISW7KR.cjs → chunk-KEAUN5F5.cjs} +10 -10
  30. package/dist/tsup/chunk-KEAUN5F5.cjs.map +1 -0
  31. package/dist/tsup/chunk-KVFIONKQ.cjs.map +1 -1
  32. package/dist/tsup/{chunk-CVLNRT7O.cjs → chunk-LUMROGNM.cjs} +3 -3
  33. package/dist/tsup/chunk-LUMROGNM.cjs.map +1 -0
  34. package/dist/tsup/{chunk-JX2ITWAV.cjs → chunk-MGI2VVF6.cjs} +14 -4
  35. package/dist/tsup/chunk-MGI2VVF6.cjs.map +1 -0
  36. package/dist/tsup/chunk-MYWZCDML.cjs.map +1 -1
  37. package/dist/tsup/chunk-NWCAIWHL.cjs.map +1 -1
  38. package/dist/tsup/{chunk-RSIOGT2U.cjs → chunk-QOQA4JPN.cjs} +7 -7
  39. package/dist/tsup/chunk-QOQA4JPN.cjs.map +1 -0
  40. package/dist/tsup/{chunk-TZVAVPGJ.js → chunk-QXBNHJH5.js} +4 -4
  41. package/dist/tsup/{chunk-W2PXGWMR.js → chunk-RIPVNPR6.js} +2 -2
  42. package/dist/tsup/{chunk-5G2J26VM.js → chunk-VD47X7KQ.js} +318 -69
  43. package/dist/tsup/chunk-VD47X7KQ.js.map +1 -0
  44. package/dist/tsup/{chunk-76YTS32R.cjs → chunk-WLRGUAMW.cjs} +4 -4
  45. package/dist/tsup/chunk-WLRGUAMW.cjs.map +1 -0
  46. package/dist/tsup/{chunk-IHKDFKM4.cjs → chunk-WMNXGH25.cjs} +3 -3
  47. package/dist/tsup/chunk-WMNXGH25.cjs.map +1 -0
  48. package/dist/tsup/{chunk-SNNVRHLX.js → chunk-XZBDVEQN.js} +2 -2
  49. package/dist/tsup/{chunk-P2OFXH4G.js → chunk-ZOVJDAOW.js} +14 -4
  50. package/dist/tsup/chunk-ZOVJDAOW.js.map +1 -0
  51. package/dist/tsup/client/mod.cjs +6 -6
  52. package/dist/tsup/client/mod.cjs.map +1 -1
  53. package/dist/tsup/client/mod.d.cts +15 -6
  54. package/dist/tsup/client/mod.d.ts +15 -6
  55. package/dist/tsup/client/mod.js +5 -5
  56. package/dist/tsup/common/log.cjs +3 -3
  57. package/dist/tsup/common/log.cjs.map +1 -1
  58. package/dist/tsup/common/log.js +2 -2
  59. package/dist/tsup/common/websocket.cjs +4 -4
  60. package/dist/tsup/common/websocket.cjs.map +1 -1
  61. package/dist/tsup/common/websocket.js +3 -3
  62. package/dist/tsup/{config-Be-VOHaX.d.ts → config-Cv6pjCt-.d.ts} +37 -7
  63. package/dist/tsup/{config-Ct5uXxXk.d.cts → config-osdJqQwX.d.cts} +37 -7
  64. package/dist/tsup/{context-CXLa0BIM.d.ts → context-CoVbKBuk.d.ts} +1 -1
  65. package/dist/tsup/{context-Cw-11Q-B.d.cts → context-DpjrM7n7.d.cts} +1 -1
  66. package/dist/tsup/db/drizzle.cjs +3 -3
  67. package/dist/tsup/db/drizzle.cjs.map +1 -1
  68. package/dist/tsup/db/drizzle.js +2 -2
  69. package/dist/tsup/db/mod.cjs +4 -4
  70. package/dist/tsup/db/mod.cjs.map +1 -1
  71. package/dist/tsup/db/mod.js +3 -3
  72. package/dist/tsup/dynamic/mod.cjs.map +1 -1
  73. package/dist/tsup/dynamic/mod.d.cts +4 -3
  74. package/dist/tsup/dynamic/mod.d.ts +4 -3
  75. package/dist/tsup/inspector/mod.cjs +6 -6
  76. package/dist/tsup/inspector/mod.cjs.map +1 -1
  77. package/dist/tsup/inspector/mod.js +5 -5
  78. package/dist/tsup/inspector/workflow.cjs +5 -5
  79. package/dist/tsup/inspector/workflow.cjs.map +1 -1
  80. package/dist/tsup/inspector/workflow.js +4 -4
  81. package/dist/tsup/inspector-tab/mod.cjs.map +1 -1
  82. package/dist/tsup/inspector-tab/mod.d.cts +5 -4
  83. package/dist/tsup/inspector-tab/mod.d.ts +5 -4
  84. package/dist/tsup/mod.cjs +170 -158
  85. package/dist/tsup/mod.cjs.map +1 -1
  86. package/dist/tsup/mod.d.cts +6 -5
  87. package/dist/tsup/mod.d.ts +6 -5
  88. package/dist/tsup/mod.js +56 -44
  89. package/dist/tsup/mod.js.map +1 -1
  90. package/dist/tsup/test/mod.cjs +7 -7
  91. package/dist/tsup/test/mod.cjs.map +1 -1
  92. package/dist/tsup/test/mod.d.cts +4 -3
  93. package/dist/tsup/test/mod.d.ts +4 -3
  94. package/dist/tsup/test/mod.js +5 -5
  95. package/dist/tsup/unstable/migrations.cjs.map +1 -1
  96. package/dist/tsup/{utils-DsoDinPz.d.ts → utils-3rsE0JvI.d.cts} +12 -2
  97. package/dist/tsup/{utils-DsoDinPz.d.cts → utils-3rsE0JvI.d.ts} +12 -2
  98. package/dist/tsup/{utils-OQHBjfcM.d.ts → utils-C1O-SklO.d.ts} +1 -1
  99. package/dist/tsup/{utils-CNFeS-Dk.d.cts → utils-C6b0aYUM.d.cts} +1 -1
  100. package/dist/tsup/utils.cjs +3 -3
  101. package/dist/tsup/utils.cjs.map +1 -1
  102. package/dist/tsup/utils.d.cts +2 -2
  103. package/dist/tsup/utils.d.ts +2 -2
  104. package/dist/tsup/utils.js +2 -2
  105. package/dist/tsup/workflow/mod.cjs +11 -11
  106. package/dist/tsup/workflow/mod.cjs.map +1 -1
  107. package/dist/tsup/workflow/mod.d.cts +6 -5
  108. package/dist/tsup/workflow/mod.d.ts +6 -5
  109. package/dist/tsup/workflow/mod.js +5 -5
  110. package/package.json +10 -9
  111. package/src/actor/errors.ts +27 -4
  112. package/src/client/actor-conn.ts +34 -1
  113. package/src/client/actor-handle.ts +29 -1
  114. package/src/client/auth.ts +37 -0
  115. package/src/client/client.ts +10 -2
  116. package/src/client/config.ts +12 -1
  117. package/src/client/mod.ts +9 -0
  118. package/src/client/token-provider.test.ts +86 -0
  119. package/src/client/token-provider.ts +93 -0
  120. package/src/client/utils.ts +1 -0
  121. package/src/engine-client/actor-websocket-client.ts +2 -1
  122. package/src/engine-client/api-endpoints.ts +28 -0
  123. package/src/engine-client/driver.ts +4 -0
  124. package/src/engine-client/mod.ts +201 -64
  125. package/src/registry/config/envoy.ts +1 -17
  126. package/src/registry/config/index.test.ts +2 -2
  127. package/src/registry/config/index.ts +7 -2
  128. package/src/registry/index.ts +28 -0
  129. package/src/registry/native.ts +31 -3
  130. package/dist/tsup/chunk-2CE5UNWK.cjs.map +0 -1
  131. package/dist/tsup/chunk-2NXLTFWI.js.map +0 -1
  132. package/dist/tsup/chunk-3WS7Z64U.cjs.map +0 -1
  133. package/dist/tsup/chunk-5G2J26VM.js.map +0 -1
  134. package/dist/tsup/chunk-76YTS32R.cjs.map +0 -1
  135. package/dist/tsup/chunk-A36FMKOV.cjs.map +0 -1
  136. package/dist/tsup/chunk-CVLNRT7O.cjs.map +0 -1
  137. package/dist/tsup/chunk-DHWZTUKA.js.map +0 -1
  138. package/dist/tsup/chunk-IHKDFKM4.cjs.map +0 -1
  139. package/dist/tsup/chunk-JX2ITWAV.cjs.map +0 -1
  140. package/dist/tsup/chunk-P2OFXH4G.js.map +0 -1
  141. package/dist/tsup/chunk-RSIOGT2U.cjs.map +0 -1
  142. package/dist/tsup/chunk-WAISW7KR.cjs.map +0 -1
  143. package/src/registry/process-metrics.ts +0 -250
  144. /package/dist/tsup/{chunk-553CIQJY.js.map → chunk-47FSGXYN.js.map} +0 -0
  145. /package/dist/tsup/{chunk-DMQUERRN.js.map → chunk-GINS4O73.js.map} +0 -0
  146. /package/dist/tsup/{chunk-TZVAVPGJ.js.map → chunk-QXBNHJH5.js.map} +0 -0
  147. /package/dist/tsup/{chunk-W2PXGWMR.js.map → chunk-RIPVNPR6.js.map} +0 -0
  148. /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
+ };
@@ -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
 
@@ -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: parsedEndpoint?.token ?? config.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
+ }
@@ -229,6 +229,7 @@ export async function sendHttpRequest<
229
229
  metadata: responseData.metadata,
230
230
  rayId: rayId ?? undefined,
231
231
  actor: responseData.actor,
232
+ statusCode: response.status,
232
233
  },
233
234
  );
234
235
  } catch (error) {
@@ -336,7 +336,8 @@ export async function openWebSocketToGateway(
336
336
 
337
337
  logger().debug({
338
338
  msg: "opening websocket to actor via guard",
339
- gatewayUrl,
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,