@trigger.dev/sdk 4.5.6 → 4.5.8

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 (50) hide show
  1. package/dist/commonjs/v3/agentSkillsRuntime.js +17 -7
  2. package/dist/commonjs/v3/agentSkillsRuntime.js.map +1 -1
  3. package/dist/commonjs/v3/ai.d.ts +49 -8
  4. package/dist/commonjs/v3/ai.js +258 -42
  5. package/dist/commonjs/v3/ai.js.map +1 -1
  6. package/dist/commonjs/v3/aiAutoTelemetry.js +3 -3
  7. package/dist/commonjs/v3/aiAutoTelemetry.js.map +1 -1
  8. package/dist/commonjs/v3/auth.d.ts +7 -3
  9. package/dist/commonjs/v3/auth.js +68 -13
  10. package/dist/commonjs/v3/auth.js.map +1 -1
  11. package/dist/commonjs/v3/auth.test.d.ts +1 -0
  12. package/dist/commonjs/v3/auth.test.js +149 -0
  13. package/dist/commonjs/v3/auth.test.js.map +1 -0
  14. package/dist/commonjs/v3/createStartSessionAction.test.js +33 -0
  15. package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
  16. package/dist/commonjs/v3/index.js +17 -7
  17. package/dist/commonjs/v3/index.js.map +1 -1
  18. package/dist/commonjs/v3/runs.d.ts +15 -15
  19. package/dist/commonjs/v3/skill.js +17 -7
  20. package/dist/commonjs/v3/skill.js.map +1 -1
  21. package/dist/commonjs/v3/test/test-session-handle.js +20 -8
  22. package/dist/commonjs/v3/test/test-session-handle.js.map +1 -1
  23. package/dist/commonjs/v3/triggerClient.js +17 -7
  24. package/dist/commonjs/v3/triggerClient.js.map +1 -1
  25. package/dist/commonjs/v3/triggerClient.types.test.js +17 -7
  26. package/dist/commonjs/v3/triggerClient.types.test.js.map +1 -1
  27. package/dist/commonjs/version.js +1 -1
  28. package/dist/esm/v3/ai.d.ts +49 -8
  29. package/dist/esm/v3/ai.js +259 -43
  30. package/dist/esm/v3/ai.js.map +1 -1
  31. package/dist/esm/v3/aiAutoTelemetry.js +3 -3
  32. package/dist/esm/v3/aiAutoTelemetry.js.map +1 -1
  33. package/dist/esm/v3/auth.d.ts +7 -3
  34. package/dist/esm/v3/auth.js +69 -14
  35. package/dist/esm/v3/auth.js.map +1 -1
  36. package/dist/esm/v3/auth.test.d.ts +1 -0
  37. package/dist/esm/v3/auth.test.js +147 -0
  38. package/dist/esm/v3/auth.test.js.map +1 -0
  39. package/dist/esm/v3/createStartSessionAction.test.js +33 -0
  40. package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
  41. package/dist/esm/v3/test/test-session-handle.js +20 -8
  42. package/dist/esm/v3/test/test-session-handle.js.map +1 -1
  43. package/dist/esm/version.js +1 -1
  44. package/docs/ai-chat/changelog.mdx +2 -2
  45. package/docs/ai-chat/compaction.mdx +2 -2
  46. package/docs/ai-chat/custom-agents.mdx +15 -20
  47. package/docs/ai-chat/fast-starts.mdx +2 -2
  48. package/docs/ai-chat/pending-messages.mdx +6 -2
  49. package/docs/ai-chat/reference.mdx +2 -2
  50. package/package.json +3 -3
package/dist/esm/v3/ai.js CHANGED
@@ -1,4 +1,4 @@
1
- import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, getSchemaParseFn, headerValue, InputStreamOncePromise, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, resourceCatalog, SemanticInternalAttributes, SESSION_IN_EVENT_ID_HEADER, sessionStreams, taskContext, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
1
+ import { accessoryAttributes, apiClientManager, controlSubtype, generateJWT, getSchemaParseFn, headerValue, InputStreamOncePromise, isAdditionalApiKey, isSchemaZodEsque, logger, ManualWaitpointPromise, OutOfMemoryError, resourceCatalog, SemanticInternalAttributes, SESSION_IN_EVENT_ID_HEADER, sessionStreams, taskContext, TRIGGER_CONTROL_SUBTYPE, } from "@trigger.dev/core/v3";
2
2
  // Runtime VALUES go through the ESM/CJS shim so the CJS build can `require`
3
3
  // ESM-only `ai@7` (see ../imports/ai-runtime.ts).
4
4
  import { trace } from "@opentelemetry/api";
@@ -3630,6 +3630,9 @@ function chatAgent(options) {
3630
3630
  // Declared here so the finally can detach it — a handler leaked past
3631
3631
  // its turn duplicates every mid-stream message into the shared buffer.
3632
3632
  let turnMsgSub;
3633
+ let capturedPartialResponse;
3634
+ let responseCommitted = false;
3635
+ const turnBufferedChunks = [];
3633
3636
  try {
3634
3637
  // Extract turn-level context before entering the span. Slim
3635
3638
  // wire: at most one delta message per record. `headStartMessages`
@@ -4307,11 +4310,12 @@ function chatAgent(options) {
4307
4310
  generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId,
4308
4311
  onFinish: ({ responseMessage, finishReason, }) => {
4309
4312
  capturedResponseMessage = responseMessage;
4313
+ capturedPartialResponse = responseMessage;
4310
4314
  capturedFinishReason = finishReason;
4311
4315
  resolveOnFinish();
4312
4316
  },
4313
4317
  });
4314
- await pipeChat(uiStream, {
4318
+ await pipeChat(tapUIMessageChunks(uiStream, turnBufferedChunks), {
4315
4319
  signal: combinedSignal,
4316
4320
  spanName: "stream response",
4317
4321
  });
@@ -4505,6 +4509,11 @@ function chatAgent(options) {
4505
4509
  locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
4506
4510
  }
4507
4511
  }
4512
+ if (capturedResponseMessage) {
4513
+ responseCommitted = true;
4514
+ capturedPartialResponse = capturedResponseMessage;
4515
+ turnBufferedChunks.length = 0;
4516
+ }
4508
4517
  if (runSignal.aborted)
4509
4518
  return "exit";
4510
4519
  // Await deferred background work (e.g. DB writes from onTurnStart)
@@ -4695,6 +4704,7 @@ function chatAgent(options) {
4695
4704
  parts: [...(msg.parts ?? []), ...lateParts],
4696
4705
  };
4697
4706
  capturedResponseMessage = accumulatedUIMessages[idx];
4707
+ capturedPartialResponse = capturedResponseMessage;
4698
4708
  turnCompleteEvent.responseMessage = capturedResponseMessage;
4699
4709
  turnCompleteEvent.uiMessages = accumulatedUIMessages;
4700
4710
  }
@@ -4937,10 +4947,65 @@ function chatAgent(options) {
4937
4947
  !accumulatedUIMessages.some((m) => m.id === erroredWireMessage.id)
4938
4948
  ? [...accumulatedUIMessages, erroredWireMessage]
4939
4949
  : accumulatedUIMessages;
4940
- // Fire onTurnComplete on the error path too — the docs promise it
4941
- // runs "after every turn, successful or errored" so customers can
4942
- // mark the turn failed. `responseMessage` is undefined/partial and
4943
- // `error` carries the thrown value.
4950
+ let partialResponse = capturedPartialResponse ??
4951
+ (await assemblePartialFromChunks(turnBufferedChunks));
4952
+ if (partialResponse) {
4953
+ partialResponse = cleanupAbortedParts(partialResponse);
4954
+ }
4955
+ let partialIdx = partialResponse?.id
4956
+ ? erroredUIMessages.findIndex((m) => m.id === partialResponse.id)
4957
+ : -1;
4958
+ if (partialResponse && capturedPartialResponse === undefined && partialIdx !== -1) {
4959
+ partialResponse = undefined;
4960
+ partialIdx = -1;
4961
+ }
4962
+ if (partialResponse && !partialResponse.id) {
4963
+ partialResponse = { ...partialResponse, id: generateMessageId() };
4964
+ }
4965
+ if (partialResponse && !responseCommitted) {
4966
+ const queuedParts = locals.get(chatResponsePartsKey);
4967
+ if (queuedParts && queuedParts.length > 0) {
4968
+ partialResponse = {
4969
+ ...partialResponse,
4970
+ parts: [...partialResponse.parts, ...queuedParts],
4971
+ };
4972
+ locals.set(chatResponsePartsKey, []);
4973
+ }
4974
+ }
4975
+ const includePartial = partialResponse != null && !responseCommitted;
4976
+ let erroredUIMessagesWithPartial = !includePartial
4977
+ ? erroredUIMessages
4978
+ : partialIdx === -1
4979
+ ? [...erroredUIMessages, partialResponse]
4980
+ : erroredUIMessages.map((m, i) => i === partialIdx ? partialResponse : m);
4981
+ let erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
4982
+ if (includePartial) {
4983
+ erroredNewUIMessages.push(partialResponse);
4984
+ }
4985
+ let erroredNewModelMessages = [];
4986
+ if (!responseCommitted) {
4987
+ try {
4988
+ if (erroredNewUIMessages.length > 0) {
4989
+ erroredNewModelMessages = await toModelMessages(erroredNewUIMessages.map((m) => stripProviderMetadata(m)));
4990
+ }
4991
+ if (erroredUIMessagesWithPartial !== accumulatedUIMessages) {
4992
+ if (partialIdx === -1) {
4993
+ const appended = erroredUIMessagesWithPartial.slice(accumulatedUIMessages.length);
4994
+ accumulatedMessages.push(...(await toModelMessages(appended.map((m) => stripProviderMetadata(m)))));
4995
+ }
4996
+ else {
4997
+ accumulatedMessages = await toModelMessages(erroredUIMessagesWithPartial);
4998
+ }
4999
+ accumulatedUIMessages = erroredUIMessagesWithPartial;
5000
+ locals.set(chatCurrentUIMessagesKey, accumulatedUIMessages);
5001
+ }
5002
+ }
5003
+ catch {
5004
+ erroredNewModelMessages = [];
5005
+ erroredUIMessagesWithPartial = erroredUIMessages;
5006
+ erroredNewUIMessages = erroredWireMessage ? [erroredWireMessage] : [];
5007
+ }
5008
+ }
4944
5009
  if (onTurnComplete) {
4945
5010
  try {
4946
5011
  await tracer.startActiveSpan("onTurnComplete()", async () => {
@@ -4948,11 +5013,11 @@ function chatAgent(options) {
4948
5013
  ctx,
4949
5014
  chatId: currentWirePayload.chatId,
4950
5015
  messages: accumulatedMessages,
4951
- uiMessages: erroredUIMessages,
4952
- newMessages: [],
4953
- newUIMessages: erroredWireMessage ? [erroredWireMessage] : [],
4954
- responseMessage: undefined,
4955
- rawResponseMessage: undefined,
5016
+ uiMessages: erroredUIMessagesWithPartial,
5017
+ newMessages: erroredNewModelMessages,
5018
+ newUIMessages: erroredNewUIMessages,
5019
+ responseMessage: partialResponse,
5020
+ rawResponseMessage: partialResponse,
4956
5021
  turn,
4957
5022
  runId: ctx.run.id,
4958
5023
  chatAccessToken: "",
@@ -4996,7 +5061,7 @@ function chatAgent(options) {
4996
5061
  await writeChatSnapshot(sessionIdForSnapshot, {
4997
5062
  version: 1,
4998
5063
  savedAt: Date.now(),
4999
- messages: erroredUIMessages,
5064
+ messages: erroredUIMessagesWithPartial,
5000
5065
  lastOutEventId: errorTurnCompleteResult?.lastEventId,
5001
5066
  lastInEventId: errorSnapshotInCursor !== undefined ? String(errorSnapshotInCursor) : undefined,
5002
5067
  });
@@ -5606,31 +5671,110 @@ function createStopSignal() {
5606
5671
  * The `TriggerChatTransport` intercepts this to close the ReadableStream
5607
5672
  * for the current turn. Call after piping the response stream.
5608
5673
  *
5674
+ * Returns two resume cursors for the turn boundary, both saveable from the
5675
+ * task instead of round-tripping them back from the client:
5676
+ * - `lastEventId` — the turn-complete control record's seq_num on
5677
+ * `session.out`; where the next turn's output stream resumes.
5678
+ * - `sessionInEventId` — the committed-consume cursor on `session.in` as of
5679
+ * this turn-complete, letting a raw loop correlate the boundary with the
5680
+ * exact input record it acknowledged. Trigger owns input-cursor recovery,
5681
+ * so this is for correlation / out-of-sync detection, not required.
5682
+ *
5683
+ * Either is `undefined` when the corresponding cursor isn't available.
5684
+ *
5609
5685
  * @example
5610
5686
  * ```ts
5611
5687
  * await chat.pipe(result);
5612
- * await chat.writeTurnComplete();
5688
+ * const { lastEventId, sessionInEventId } = await chat.writeTurnComplete();
5689
+ * await db.chats.update(chatId, { lastEventId, sessionInEventId });
5613
5690
  * ```
5614
5691
  */
5615
5692
  async function chatWriteTurnComplete(options) {
5616
- await writeTurnCompleteChunk(undefined, options?.publicAccessToken);
5693
+ const result = await writeTurnCompleteChunk(undefined, options?.publicAccessToken);
5694
+ // Same cursor written to the `session-in-event-id` header inside
5695
+ // `writeTurnCompleteChunk`; surfaced here so the caller can persist it.
5696
+ const inCursor = getChatSession().in.lastDispatchedSeqNum();
5697
+ return {
5698
+ lastEventId: result?.lastEventId,
5699
+ ...(inCursor !== undefined ? { sessionInEventId: String(inCursor) } : {}),
5700
+ };
5701
+ }
5702
+ /**
5703
+ * Pass every chunk through untouched while recording it in `buffer`. Handles
5704
+ * both the `AsyncIterable` and `ReadableStream` shapes `toUIMessageStream()`
5705
+ * can return, and propagates a source error to the consumer after buffering
5706
+ * whatever streamed first. See {@link pipeChatAndCapture} for why.
5707
+ */
5708
+ function tapUIMessageChunks(source, buffer) {
5709
+ if (isReadableStream(source)) {
5710
+ return source.pipeThrough(new TransformStream({
5711
+ transform(chunk, controller) {
5712
+ buffer.push(chunk);
5713
+ controller.enqueue(chunk);
5714
+ },
5715
+ }));
5716
+ }
5717
+ return (async function* () {
5718
+ for await (const chunk of source) {
5719
+ buffer.push(chunk);
5720
+ yield chunk;
5721
+ }
5722
+ })();
5723
+ }
5724
+ /**
5725
+ * Reconstruct a partial assistant `UIMessage` from the raw chunks that
5726
+ * streamed before a failure — the fallback for {@link pipeChatAndCapture}
5727
+ * when a transport error abandons the stream before `onFinish` runs. Uses the
5728
+ * same `readUIMessageStream` reducer as the boot-time replay path. Returns
5729
+ * `undefined` if there's nothing to assemble or the reducer throws.
5730
+ */
5731
+ async function assemblePartialFromChunks(chunks) {
5732
+ const relevant = chunks.filter((c) => {
5733
+ const type = c.type;
5734
+ return typeof type === "string" && !type.startsWith("trigger:");
5735
+ });
5736
+ if (relevant.length === 0)
5737
+ return undefined;
5738
+ try {
5739
+ const stream = new ReadableStream({
5740
+ start(controller) {
5741
+ for (const chunk of relevant)
5742
+ controller.enqueue(chunk);
5743
+ controller.close();
5744
+ },
5745
+ });
5746
+ let last;
5747
+ for await (const message of readUIMessageStream({ stream })) {
5748
+ last = message;
5749
+ }
5750
+ return last;
5751
+ }
5752
+ catch {
5753
+ return undefined;
5754
+ }
5617
5755
  }
5618
5756
  /**
5619
5757
  * Pipe a `StreamTextResult` (or similar) to the chat stream and capture
5620
5758
  * the assistant's response message via `onFinish`.
5621
5759
  *
5622
5760
  * Combines `toUIMessageStream()` + `onFinish` callback + `chat.pipe()`.
5623
- * Returns the captured `UIMessage`, or `undefined` if capture failed.
5761
+ * Never throws on a stopped or failed stream: it returns a
5762
+ * {@link PipeAndCaptureResult} whose `message` holds any partial output
5763
+ * captured before the stop/failure, alongside a typed `status` (and `error`
5764
+ * on failure). Save the partial after a stop or error without separate
5765
+ * capture logic.
5624
5766
  *
5625
5767
  * @example
5626
5768
  * ```ts
5627
5769
  * const result = streamText({ model, messages, abortSignal: signal });
5628
- * const response = await chat.pipeAndCapture(result, { signal });
5629
- * if (response) conversation.addResponse(response);
5770
+ * const { message, status, error } = await chat.pipeAndCapture(result, { signal });
5771
+ * if (message) conversation.addResponse(message);
5772
+ * if (status === "error") logger.error("turn failed", { error });
5630
5773
  * ```
5631
5774
  */
5632
5775
  async function pipeChatAndCapture(source, options) {
5633
5776
  let captured;
5777
+ let capturedFinishReason;
5634
5778
  let resolveOnFinish;
5635
5779
  const onFinishPromise = new Promise((r) => {
5636
5780
  resolveOnFinish = r;
@@ -5648,17 +5792,57 @@ async function pipeChatAndCapture(source, options) {
5648
5792
  // the frontend replaces the partial message — wiping the
5649
5793
  // pre-injection text from the UI and the captured response.
5650
5794
  generateMessageId: resolvedOptions.generateMessageId ?? generateMessageId,
5651
- onFinish: ({ responseMessage }) => {
5795
+ onFinish: ({ responseMessage, finishReason, }) => {
5652
5796
  captured = responseMessage;
5797
+ capturedFinishReason = finishReason;
5653
5798
  resolveOnFinish();
5654
5799
  },
5655
5800
  });
5656
- await pipeChat(uiStream, {
5657
- signal: options?.signal,
5658
- spanName: options?.spanName ?? "stream response",
5659
- });
5660
- await onFinishPromise;
5661
- return captured;
5801
+ // Buffer chunks as they flow to the pipe so a transport failure that
5802
+ // abandons the UI stream before `onFinish` fires — the one termination path
5803
+ // that skips `onFinish` — can still reconstruct the partial. This retains
5804
+ // chunk references only; the reassembly runs solely in the failure fallback
5805
+ // below, so the happy path pays nothing extra.
5806
+ const bufferedChunks = [];
5807
+ const tappedStream = tapUIMessageChunks(uiStream, bufferedChunks);
5808
+ let status = "complete";
5809
+ let error;
5810
+ try {
5811
+ await pipeChat(tappedStream, {
5812
+ signal: options?.signal,
5813
+ spanName: options?.spanName ?? "stream response",
5814
+ });
5815
+ // The pipe can drain cleanly on a stop — the source stream just ends
5816
+ // early — so classify by the signal rather than relying on a throw.
5817
+ if (options?.signal?.aborted) {
5818
+ status = "aborted";
5819
+ }
5820
+ }
5821
+ catch (err) {
5822
+ if ((err instanceof Error && err.name === "AbortError") || options?.signal?.aborted) {
5823
+ status = "aborted";
5824
+ }
5825
+ else {
5826
+ status = "error";
5827
+ error = err;
5828
+ }
5829
+ }
5830
+ // `onFinish` fires even on abort, carrying the partial — but a hard stop can
5831
+ // prevent it from firing at all, so race it against a timeout to avoid
5832
+ // hanging the caller. Mirrors chat.agent's capture path.
5833
+ await Promise.race([onFinishPromise, new Promise((r) => setTimeout(r, 2_000))]);
5834
+ // A transport failure can abandon the UI stream before `onFinish` fires, so
5835
+ // reconstruct the partial from the buffered chunks rather than losing output
5836
+ // that already streamed. Only runs when `onFinish` produced nothing.
5837
+ if (!captured && bufferedChunks.length > 0) {
5838
+ captured = await assemblePartialFromChunks(bufferedChunks);
5839
+ }
5840
+ return {
5841
+ message: captured,
5842
+ status,
5843
+ finishReason: capturedFinishReason,
5844
+ ...(error !== undefined ? { error } : {}),
5845
+ };
5662
5846
  }
5663
5847
  /**
5664
5848
  * Accumulates conversation messages across turns.
@@ -5673,8 +5857,8 @@ async function pipeChatAndCapture(source, options) {
5673
5857
  * for (let turn = 0; turn < 100; turn++) {
5674
5858
  * const messages = await conversation.addIncoming(payload.messages, payload.trigger, turn);
5675
5859
  * const result = streamText({ model, messages });
5676
- * const response = await chat.pipeAndCapture(result);
5677
- * if (response) await conversation.addResponse(response);
5860
+ * const { message } = await chat.pipeAndCapture(result);
5861
+ * if (message) await conversation.addResponse(message);
5678
5862
  * }
5679
5863
  * ```
5680
5864
  */
@@ -6168,7 +6352,7 @@ function createChatSession(payload, options) {
6168
6352
  }
6169
6353
  let response;
6170
6354
  try {
6171
- response = await pipeChatAndCapture(source, {
6355
+ const captured = await pipeChatAndCapture(source, {
6172
6356
  signal: combinedSignal,
6173
6357
  // On a non-final handover turn, thread the spliced partial so a
6174
6358
  // resumed tool round's tool-output chunks merge into the
@@ -6177,20 +6361,27 @@ function createChatSession(payload, options) {
6177
6361
  // fresh response into the prior assistant message).
6178
6362
  ...(handoverThisTurn ? { originalMessages: accumulator.uiMessages } : {}),
6179
6363
  });
6180
- }
6181
- catch (error) {
6182
- if (error instanceof Error && error.name === "AbortError") {
6183
- if (runSignal.aborted) {
6184
- // Full cancel — don't accumulate
6185
- sessionMsgSub.off();
6186
- await chatWriteTurnComplete();
6187
- return undefined;
6188
- }
6189
- // Stop — fall through to accumulate partial response
6364
+ if (runSignal.aborted) {
6365
+ // Full cancel — don't accumulate
6366
+ sessionMsgSub.off();
6367
+ await chatWriteTurnComplete();
6368
+ return undefined;
6190
6369
  }
6191
- else {
6192
- throw error;
6370
+ // Surface a genuine stream failure to the caller. A user stop
6371
+ // (status "aborted") falls through so the partial is accumulated.
6372
+ if (captured.status === "error") {
6373
+ if (captured.message) {
6374
+ const partial = cleanupAbortedParts(captured.message);
6375
+ const queuedParts = locals.get(chatResponsePartsKey);
6376
+ if (queuedParts && queuedParts.length > 0) {
6377
+ partial.parts = [...(partial.parts ?? []), ...queuedParts];
6378
+ locals.set(chatResponsePartsKey, []);
6379
+ }
6380
+ await accumulator.addResponse(partial);
6381
+ }
6382
+ throw captured.error;
6193
6383
  }
6384
+ response = captured.message;
6194
6385
  }
6195
6386
  finally {
6196
6387
  // Detach at stream end (like the agent loop): the steering queue
@@ -6732,24 +6923,49 @@ async function mintPublicTokenWithOverride(args) {
6732
6923
  throw new Error("chat.createStartSessionAction: no API access token configured for JWT mint.");
6733
6924
  }
6734
6925
  const ctx = { endpoint: "auth", chatId: args.chatId };
6735
- const url = `${resolveChatStartBaseURL("auth", args.chatId, args.baseURLOption)}/api/v1/auth/jwt/claims`;
6926
+ const scopes = [`read:sessions:${args.chatId}`, `write:sessions:${args.chatId}`];
6927
+ const serverMint = isAdditionalApiKey(accessToken);
6928
+ const endpoint = serverMint ? "/api/v1/auth/public-tokens" : "/api/v1/auth/jwt/claims";
6929
+ const url = `${resolveChatStartBaseURL("auth", args.chatId, args.baseURLOption)}${endpoint}`;
6736
6930
  const init = {
6737
6931
  method: "POST",
6738
6932
  headers: overrideRequestHeaders(accessToken),
6933
+ ...(serverMint
6934
+ ? {
6935
+ body: JSON.stringify({
6936
+ scopes,
6937
+ expirationTime: args.expirationTime instanceof Date
6938
+ ? Math.floor(args.expirationTime.getTime() / 1000)
6939
+ : args.expirationTime,
6940
+ }),
6941
+ }
6942
+ : {}),
6739
6943
  };
6740
6944
  const response = args.fetchOverride
6741
6945
  ? await args.fetchOverride(url, init, ctx)
6742
6946
  : await fetch(url, init);
6743
6947
  if (!response.ok) {
6948
+ // An additional API key cannot self-sign, so it must use the server mint
6949
+ // endpoint. On a server too old to expose it, explain the recovery path
6950
+ // instead of surfacing a bare 404 (mirrors auth.createServerPublicToken).
6951
+ if (serverMint && response.status === 404) {
6952
+ throw new Error("This additional API key cannot self-sign public tokens, and the server does not support public-token minting. Upgrade the server or use the root API key.");
6953
+ }
6744
6954
  const text = await response.text().catch(() => "");
6745
6955
  throw new Error(`auth.createPublicToken failed: ${response.status} ${text}`);
6746
6956
  }
6747
- const claims = (await response.json());
6957
+ const responseBody = (await response.json());
6958
+ if (serverMint) {
6959
+ if (typeof responseBody.token !== "string") {
6960
+ throw new Error("auth.createPublicToken failed: server response did not include a token");
6961
+ }
6962
+ return responseBody.token;
6963
+ }
6748
6964
  return generateJWT({
6749
6965
  secretKey: accessToken,
6750
6966
  payload: {
6751
- ...claims,
6752
- scopes: [`read:sessions:${args.chatId}`, `write:sessions:${args.chatId}`],
6967
+ ...responseBody,
6968
+ scopes,
6753
6969
  },
6754
6970
  expirationTime: args.expirationTime,
6755
6971
  });