rivetkit 2.3.19 → 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.
Files changed (146) hide show
  1. package/dist/browser/client.d.ts +82 -5
  2. package/dist/browser/client.js +302 -60
  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.map +1 -1
  7. package/dist/tsup/agent-os/index.cjs +35 -1
  8. package/dist/tsup/agent-os/index.cjs.map +1 -1
  9. package/dist/tsup/agent-os/index.d.cts +71 -4
  10. package/dist/tsup/agent-os/index.d.ts +71 -4
  11. package/dist/tsup/agent-os/index.js +35 -1
  12. package/dist/tsup/agent-os/index.js.map +1 -1
  13. package/dist/tsup/{chunk-X7RMJQGY.js → chunk-2CI2IEVN.js} +2 -2
  14. package/dist/tsup/{chunk-WME6KOXW.js → chunk-5EGLHQJJ.js} +2 -2
  15. package/dist/tsup/{chunk-464WE4RF.js → chunk-5IBUZFLP.js} +2 -2
  16. package/dist/tsup/{chunk-PPEIPCW4.cjs → chunk-6LRPYB4Q.cjs} +4 -4
  17. package/dist/tsup/chunk-6LRPYB4Q.cjs.map +1 -0
  18. package/dist/tsup/{chunk-7BRRGBRY.cjs → chunk-APOANSDG.cjs} +10 -10
  19. package/dist/tsup/chunk-APOANSDG.cjs.map +1 -0
  20. package/dist/tsup/{chunk-G4U5L6AX.cjs → chunk-ARGNQGYG.cjs} +3 -3
  21. package/dist/tsup/chunk-ARGNQGYG.cjs.map +1 -0
  22. package/dist/tsup/{chunk-IJUXBWH4.cjs → chunk-B5SNXPSS.cjs} +448 -199
  23. package/dist/tsup/chunk-B5SNXPSS.cjs.map +1 -0
  24. package/dist/tsup/{chunk-4SC2NSAV.js → chunk-CJXBGTVT.js} +2 -2
  25. package/dist/tsup/{chunk-EB77NZL4.js → chunk-CSOHAAZK.js} +33 -1
  26. package/dist/tsup/chunk-CSOHAAZK.js.map +1 -0
  27. package/dist/tsup/{chunk-Z4RAXFSD.cjs → chunk-GHKQC4PD.cjs} +3 -3
  28. package/dist/tsup/chunk-GHKQC4PD.cjs.map +1 -0
  29. package/dist/tsup/chunk-JX2ITWAV.cjs.map +1 -1
  30. package/dist/tsup/chunk-KVFIONKQ.cjs.map +1 -1
  31. package/dist/tsup/{chunk-RJC5O2SD.cjs → chunk-KWGN2ZBJ.cjs} +3 -2
  32. package/dist/tsup/chunk-KWGN2ZBJ.cjs.map +1 -0
  33. package/dist/tsup/{chunk-OGPFN6BT.cjs → chunk-LWSLKKZ5.cjs} +3 -3
  34. package/dist/tsup/chunk-LWSLKKZ5.cjs.map +1 -0
  35. package/dist/tsup/chunk-MYWZCDML.cjs.map +1 -1
  36. package/dist/tsup/{chunk-7A5EIXJJ.cjs → chunk-NL2EIKK6.cjs} +7 -7
  37. package/dist/tsup/chunk-NL2EIKK6.cjs.map +1 -0
  38. package/dist/tsup/{chunk-EO5UOXXY.cjs → chunk-NWCAIWHL.cjs} +34 -2
  39. package/dist/tsup/chunk-NWCAIWHL.cjs.map +1 -0
  40. package/dist/tsup/{chunk-BKX7UDTA.js → chunk-PYRI75DL.js} +314 -65
  41. package/dist/tsup/chunk-PYRI75DL.js.map +1 -0
  42. package/dist/tsup/{chunk-7HTST3NX.js → chunk-RFN3BG6D.js} +3 -3
  43. package/dist/tsup/chunk-RFN3BG6D.js.map +1 -0
  44. package/dist/tsup/{chunk-DN6MD3KK.js → chunk-RGIZNZYO.js} +3 -2
  45. package/dist/tsup/chunk-RGIZNZYO.js.map +1 -0
  46. package/dist/tsup/{chunk-DK3L22R6.js → chunk-VMMXXVDO.js} +4 -4
  47. package/dist/tsup/client/mod.cjs +5 -5
  48. package/dist/tsup/client/mod.cjs.map +1 -1
  49. package/dist/tsup/client/mod.d.cts +12 -3
  50. package/dist/tsup/client/mod.d.ts +12 -3
  51. package/dist/tsup/client/mod.js +4 -4
  52. package/dist/tsup/common/log.cjs +2 -2
  53. package/dist/tsup/common/log.cjs.map +1 -1
  54. package/dist/tsup/common/log.js +1 -1
  55. package/dist/tsup/common/websocket.cjs +3 -3
  56. package/dist/tsup/common/websocket.cjs.map +1 -1
  57. package/dist/tsup/common/websocket.js +2 -2
  58. package/dist/tsup/{config-DEm0yIVV.d.ts → config-Bc1nnvUl.d.ts} +76 -6
  59. package/dist/tsup/{config-8n2AOhXK.d.cts → config-Do_0guSS.d.cts} +76 -6
  60. package/dist/tsup/{context-DwjOVvlE.d.cts → context-CgNkhjdY.d.cts} +1 -1
  61. package/dist/tsup/{context-DGg2-IOm.d.ts → context-oGQxZDPz.d.ts} +1 -1
  62. package/dist/tsup/db/drizzle.cjs +2 -2
  63. package/dist/tsup/db/drizzle.cjs.map +1 -1
  64. package/dist/tsup/db/drizzle.js +1 -1
  65. package/dist/tsup/db/mod.cjs +3 -3
  66. package/dist/tsup/db/mod.cjs.map +1 -1
  67. package/dist/tsup/db/mod.js +2 -2
  68. package/dist/tsup/dynamic/mod.cjs +2 -2
  69. package/dist/tsup/dynamic/mod.cjs.map +1 -1
  70. package/dist/tsup/dynamic/mod.d.cts +2 -1
  71. package/dist/tsup/dynamic/mod.d.ts +2 -1
  72. package/dist/tsup/dynamic/mod.js +1 -1
  73. package/dist/tsup/inspector/mod.cjs +5 -5
  74. package/dist/tsup/inspector/mod.cjs.map +1 -1
  75. package/dist/tsup/inspector/mod.js +4 -4
  76. package/dist/tsup/inspector/workflow.cjs +4 -4
  77. package/dist/tsup/inspector/workflow.cjs.map +1 -1
  78. package/dist/tsup/inspector/workflow.js +3 -3
  79. package/dist/tsup/inspector-tab/mod.cjs +2 -2
  80. package/dist/tsup/inspector-tab/mod.cjs.map +1 -1
  81. package/dist/tsup/inspector-tab/mod.d.cts +3 -2
  82. package/dist/tsup/inspector-tab/mod.d.ts +3 -2
  83. package/dist/tsup/inspector-tab/mod.js +1 -1
  84. package/dist/tsup/mod.cjs +140 -136
  85. package/dist/tsup/mod.cjs.map +1 -1
  86. package/dist/tsup/mod.d.cts +4 -3
  87. package/dist/tsup/mod.d.ts +4 -3
  88. package/dist/tsup/mod.js +44 -40
  89. package/dist/tsup/mod.js.map +1 -1
  90. package/dist/tsup/test/mod.cjs +6 -6
  91. package/dist/tsup/test/mod.cjs.map +1 -1
  92. package/dist/tsup/test/mod.d.cts +2 -1
  93. package/dist/tsup/test/mod.d.ts +2 -1
  94. package/dist/tsup/test/mod.js +4 -4
  95. package/dist/tsup/unstable/migrations.cjs.map +1 -1
  96. package/dist/tsup/utils.cjs +2 -2
  97. package/dist/tsup/utils.cjs.map +1 -1
  98. package/dist/tsup/utils.js +1 -1
  99. package/dist/tsup/workflow/mod.cjs +10 -10
  100. package/dist/tsup/workflow/mod.cjs.map +1 -1
  101. package/dist/tsup/workflow/mod.d.cts +4 -3
  102. package/dist/tsup/workflow/mod.d.ts +4 -3
  103. package/dist/tsup/workflow/mod.js +5 -5
  104. package/package.json +10 -9
  105. package/src/actor/actions.ts +36 -0
  106. package/src/actor/config.ts +27 -0
  107. package/src/actor/definition.ts +6 -1
  108. package/src/actor/mod.ts +5 -0
  109. package/src/actor/tracing.ts +28 -0
  110. package/src/client/actor-conn.ts +34 -1
  111. package/src/client/actor-handle.ts +29 -1
  112. package/src/client/auth.ts +37 -0
  113. package/src/client/client.ts +10 -2
  114. package/src/client/config.ts +12 -1
  115. package/src/client/mod.ts +9 -0
  116. package/src/client/token-provider.test.ts +86 -0
  117. package/src/client/token-provider.ts +93 -0
  118. package/src/client/utils.ts +1 -0
  119. package/src/engine-client/actor-websocket-client.ts +2 -1
  120. package/src/engine-client/api-endpoints.ts +28 -0
  121. package/src/engine-client/driver.ts +4 -0
  122. package/src/engine-client/mod.ts +201 -64
  123. package/src/registry/config/envoy.ts +1 -17
  124. package/src/registry/config/index.test.ts +2 -2
  125. package/src/registry/config/index.ts +7 -2
  126. package/src/registry/index.ts +28 -0
  127. package/src/registry/native.ts +18 -0
  128. package/src/registry/runtime.ts +8 -0
  129. package/dist/tsup/chunk-7A5EIXJJ.cjs.map +0 -1
  130. package/dist/tsup/chunk-7BRRGBRY.cjs.map +0 -1
  131. package/dist/tsup/chunk-7HTST3NX.js.map +0 -1
  132. package/dist/tsup/chunk-BKX7UDTA.js.map +0 -1
  133. package/dist/tsup/chunk-DN6MD3KK.js.map +0 -1
  134. package/dist/tsup/chunk-EB77NZL4.js.map +0 -1
  135. package/dist/tsup/chunk-EO5UOXXY.cjs.map +0 -1
  136. package/dist/tsup/chunk-G4U5L6AX.cjs.map +0 -1
  137. package/dist/tsup/chunk-IJUXBWH4.cjs.map +0 -1
  138. package/dist/tsup/chunk-OGPFN6BT.cjs.map +0 -1
  139. package/dist/tsup/chunk-PPEIPCW4.cjs.map +0 -1
  140. package/dist/tsup/chunk-RJC5O2SD.cjs.map +0 -1
  141. package/dist/tsup/chunk-Z4RAXFSD.cjs.map +0 -1
  142. /package/dist/tsup/{chunk-X7RMJQGY.js.map → chunk-2CI2IEVN.js.map} +0 -0
  143. /package/dist/tsup/{chunk-WME6KOXW.js.map → chunk-5EGLHQJJ.js.map} +0 -0
  144. /package/dist/tsup/{chunk-464WE4RF.js.map → chunk-5IBUZFLP.js.map} +0 -0
  145. /package/dist/tsup/{chunk-4SC2NSAV.js.map → chunk-CJXBGTVT.js.map} +0 -0
  146. /package/dist/tsup/{chunk-DK3L22R6.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.19",
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-cli": "2.3.19",
217
- "@rivetkit/engine-envoy-protocol": "2.3.19",
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.19",
220
- "@rivetkit/rivetkit-wasm": "2.3.19",
221
- "@rivetkit/traces": "2.3.19",
222
- "@rivetkit/virtual-websocket": "2.3.19",
223
- "@rivetkit/workflow-engine": "2.3.19",
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/actors.git",
274
+ "url": "https://github.com/rivet-dev/rivet.git",
274
275
  "directory": "rivetkit-typescript/packages/rivetkit"
275
276
  }
276
277
  }
@@ -1,4 +1,5 @@
1
1
  import type { PrimitiveSchema } from "./schema";
2
+ import type { ActionSampleRates } from "./tracing";
2
3
 
3
4
  export type RuntimeActionHandler = (...args: any[]) => any;
4
5
 
@@ -46,6 +47,41 @@ export function flattenActionInputSchemas(
46
47
  return flattened;
47
48
  }
48
49
 
50
+ /**
51
+ * Flatten per-action sample rates into dot-separated action names.
52
+ *
53
+ * @throws TypeError when a rate names something that is not an action.
54
+ */
55
+ export function flattenActionTraceSamplers(
56
+ actions: unknown,
57
+ rates: ActionSampleRates = {},
58
+ ): Record<string, number> {
59
+ const handlers = flattenActionHandlers(actions);
60
+ const flattened = Object.fromEntries(flattenSampleRates(rates, []));
61
+ for (const name of Object.keys(flattened)) {
62
+ if (!Object.hasOwn(handlers, name)) {
63
+ throw new TypeError(
64
+ `tracing.actions.${name} does not name an action`,
65
+ );
66
+ }
67
+ }
68
+ return flattened;
69
+ }
70
+
71
+ function flattenSampleRates(
72
+ rates: ActionSampleRates,
73
+ path: readonly string[],
74
+ ): Array<[name: string, rate: number]> {
75
+ return Object.entries(rates).flatMap(
76
+ ([segment, rate]): Array<[name: string, rate: number]> => {
77
+ const childPath = [...path, segment];
78
+ return typeof rate === "number"
79
+ ? [[childPath.join("."), rate]]
80
+ : flattenSampleRates(rate, childPath);
81
+ },
82
+ );
83
+ }
84
+
49
85
  interface ActionEntry {
50
86
  name: string;
51
87
  path: string[];
@@ -16,6 +16,7 @@ import type {
16
16
  PrimitiveSchema,
17
17
  QueueSchemaConfig,
18
18
  } from "./schema";
19
+ import type { ActionSampleRates, ActorTracingOptions } from "./tracing";
19
20
 
20
21
  export const DEFAULT_SLEEP_GRACE_PERIOD = 15_000;
21
22
 
@@ -1342,6 +1343,12 @@ export const ActorOptionsSchema = GlobalActorOptionsBaseSchema.extend(
1342
1343
  export type ActorOptions = z.infer<typeof ActorOptionsSchema>;
1343
1344
  export type ActorOptionsInput = z.input<typeof ActorOptionsSchema>;
1344
1345
 
1346
+ const zSampleRate = z.number().min(0).max(1);
1347
+
1348
+ const zActionSampleRates: z.ZodType<ActionSampleRates> = z.lazy(() =>
1349
+ z.record(z.string(), z.union([zSampleRate, zActionSampleRates])),
1350
+ );
1351
+
1345
1352
  export const ActorConfigSchema = z
1346
1353
  .object({
1347
1354
  onCreate: zFunction().optional(),
@@ -1359,6 +1366,13 @@ export const ActorConfigSchema = z
1359
1366
  onWebSocket: zFunction().optional(),
1360
1367
  actions: zActionTree.default(() => ({})),
1361
1368
  actionInputSchemas: z.record(z.string(), z.any()).optional(),
1369
+ tracing: z
1370
+ .object({
1371
+ sampler: zSampleRate.optional(),
1372
+ actions: zActionSampleRates.optional(),
1373
+ })
1374
+ .strict()
1375
+ .optional(),
1362
1376
  connParamsSchema: z.any().optional(),
1363
1377
  events: z.record(z.string(), z.any()).optional(),
1364
1378
  queues: z.record(z.string(), z.any()).optional(),
@@ -1904,6 +1918,12 @@ interface BaseActorConfig<
1904
1918
  */
1905
1919
  actionInputSchemas?: ActionInputSchemas;
1906
1920
 
1921
+ /**
1922
+ * How much of this actor's work is recorded in traces. Defaults to the
1923
+ * process sampler from `OTEL_TRACES_SAMPLER`.
1924
+ */
1925
+ tracing?: ActorTracingOptions<TActions>;
1926
+
1907
1927
  /**
1908
1928
  * Optional schema for validating connection params in native runtimes.
1909
1929
  */
@@ -2061,6 +2081,7 @@ export type ActorConfigInput<
2061
2081
  } & Omit<
2062
2082
  z.input<typeof ActorConfigSchema>,
2063
2083
  | "actions"
2084
+ | "tracing"
2064
2085
  | "events"
2065
2086
  | "queues"
2066
2087
  | "onCreate"
@@ -2437,6 +2458,12 @@ export const DocActorConfigSchema = z
2437
2458
  .describe(
2438
2459
  "Optional schemas for validating action argument tuples in native runtimes. May mirror nested action groups or use dot-separated low-level action names.",
2439
2460
  ),
2461
+ tracing: z
2462
+ .unknown()
2463
+ .optional()
2464
+ .describe(
2465
+ "Trace sampling for this actor: `sampler` is the share of its invocations that are recorded, from 0 to 1, with optional per-action rates under `actions`. A rate lowers what is recorded and never raises it. Defaults to the process sampler from OTEL_TRACES_SAMPLER.",
2466
+ ),
2440
2467
  connParamsSchema: z
2441
2468
  .unknown()
2442
2469
  .optional()
@@ -1,6 +1,10 @@
1
1
  import type { AnyDatabaseProvider } from "@/common/database/config";
2
2
  import type { RegistryConfig } from "@/registry/config";
3
- import { flattenActionHandlers, flattenActionInputSchemas } from "./actions";
3
+ import {
4
+ flattenActionHandlers,
5
+ flattenActionInputSchemas,
6
+ flattenActionTraceSamplers,
7
+ } from "./actions";
4
8
  import {
5
9
  type Actions,
6
10
  type ActorConfig,
@@ -209,6 +213,7 @@ export function actor<
209
213
  >;
210
214
  flattenActionHandlers(config.actions);
211
215
  flattenActionInputSchemas(config.actions, config.actionInputSchemas);
216
+ flattenActionTraceSamplers(config.actions, config.tracing?.actions);
212
217
  return new ActorDefinition(config);
213
218
  }
214
219
 
package/src/actor/mod.ts CHANGED
@@ -67,3 +67,8 @@ export {
67
67
  queue,
68
68
  type Type,
69
69
  } from "./schema";
70
+ export type {
71
+ ActionSampleRates,
72
+ ActionTraceSamplers,
73
+ ActorTracingOptions,
74
+ } from "./tracing";
@@ -0,0 +1,28 @@
1
+ /** Sample rates keyed by action name, nested like the actor's action groups. */
2
+ export interface ActionSampleRates {
3
+ readonly [name: string]: number | ActionSampleRates;
4
+ }
5
+
6
+ /**
7
+ * {@link ActionSampleRates} limited to the actions of one actor, so a rate for
8
+ * an action that does not exist is a compile error.
9
+ */
10
+ export type ActionTraceSamplers<TActions> = {
11
+ readonly [K in keyof TActions]?: TActions[K] extends (
12
+ ...args: never[]
13
+ ) => unknown
14
+ ? number
15
+ : ActionTraceSamplers<TActions[K]>;
16
+ };
17
+
18
+ /** Trace sampling for one actor definition. */
19
+ export interface ActorTracingOptions<TActions> {
20
+ /**
21
+ * Share of this actor's invocations that are recorded, from 0 to 1. A rate
22
+ * lowers what is recorded and never raises it, so an invocation inside a
23
+ * trace the caller did not record is not recorded at any rate.
24
+ */
25
+ readonly sampler?: number;
26
+ /** Rates that replace `sampler` for single actions. */
27
+ readonly actions?: ActionTraceSamplers<TActions>;
28
+ }
@@ -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
+ };
@@ -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) {