@sentry/junior 0.71.0 → 0.71.1

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/app.js CHANGED
@@ -93,8 +93,8 @@ import {
93
93
  pluginCatalogConfigFromPluginSet,
94
94
  pluginHookRegistrationsFromPluginSet,
95
95
  resolveConversationWorkQueueTopic,
96
- verifySignedConversationQueueMessage
97
- } from "./chunk-IGLNC5H6.js";
96
+ verifyConversationQueueMessage
97
+ } from "./chunk-XE2VFQQN.js";
98
98
  import {
99
99
  SlackActionError,
100
100
  createSlackDestination,
@@ -23259,6 +23259,21 @@ import {
23259
23259
  registerDevConsumer
23260
23260
  } from "@vercel/queue";
23261
23261
 
23262
+ // src/chat/task-execution/queue.ts
23263
+ var ConversationQueueMessageRejectedError = class extends Error {
23264
+ conversationId;
23265
+ reason;
23266
+ constructor(reason, message, options = {}) {
23267
+ super(message);
23268
+ this.name = "ConversationQueueMessageRejectedError";
23269
+ this.reason = reason;
23270
+ this.conversationId = options.conversationId;
23271
+ }
23272
+ };
23273
+ function isConversationQueueMessageRejectedError(error) {
23274
+ return error instanceof ConversationQueueMessageRejectedError;
23275
+ }
23276
+
23262
23277
  // src/chat/task-execution/worker.ts
23263
23278
  var CONVERSATION_WORK_DEFER_DELAY_MS = 15e3;
23264
23279
  var CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 24e4;
@@ -23279,11 +23294,21 @@ async function sendWakeNudge(args) {
23279
23294
  idempotencyKey: args.idempotencyKey
23280
23295
  }
23281
23296
  );
23282
- await markConversationWorkEnqueued({
23283
- conversationId: args.conversationId,
23284
- nowMs: args.nowMs,
23285
- state: args.options.state
23286
- });
23297
+ try {
23298
+ await markConversationWorkEnqueued({
23299
+ conversationId: args.conversationId,
23300
+ nowMs: args.nowMs,
23301
+ state: args.options.state
23302
+ });
23303
+ } catch (error) {
23304
+ logException(
23305
+ error,
23306
+ "conversation_work_enqueue_marker_failed",
23307
+ { conversationId: args.conversationId },
23308
+ {},
23309
+ "Conversation work enqueue marker failed after queue acceptance"
23310
+ );
23311
+ }
23287
23312
  }
23288
23313
  async function requestLostLeaseRecovery(args) {
23289
23314
  const continuationMarked = await requestConversationContinuation({
@@ -23361,8 +23386,10 @@ async function processConversationWork(message, options) {
23361
23386
  return { status: "no_work" };
23362
23387
  }
23363
23388
  if (!sameDestination(initial.destination, message.destination)) {
23364
- throw new Error(
23365
- `Conversation work queue destination changed for ${conversationId}`
23389
+ throw new ConversationQueueMessageRejectedError(
23390
+ "destination_mismatch",
23391
+ `Conversation work queue destination changed for ${conversationId}`,
23392
+ { conversationId }
23366
23393
  );
23367
23394
  }
23368
23395
  const destination = initial.destination;
@@ -23712,18 +23739,45 @@ async function processConversationQueueMessage(message, options) {
23712
23739
  });
23713
23740
  }
23714
23741
  async function handleConversationQueueMessage(message, options) {
23715
- const verified = verifySignedConversationQueueMessage(message);
23716
- if (!verified) {
23717
- throw new Error("Unauthorized conversation queue message");
23742
+ const verification = verifyConversationQueueMessage(message);
23743
+ if (verification.status === "rejected") {
23744
+ throw new ConversationQueueMessageRejectedError(
23745
+ verification.reason,
23746
+ "Unauthorized conversation queue message"
23747
+ );
23748
+ }
23749
+ if (verification.status === "unavailable") {
23750
+ throw new Error(
23751
+ `Conversation queue message verification unavailable: ${verification.reason}`
23752
+ );
23718
23753
  }
23719
23754
  await runWithTurnRequestDeadline(
23720
- () => processConversationQueueMessage(verified, options)
23755
+ () => processConversationQueueMessage(verification.message, options)
23756
+ );
23757
+ }
23758
+ function handleConversationQueueRetry(error, metadata) {
23759
+ if (!isConversationQueueMessageRejectedError(error)) {
23760
+ return void 0;
23761
+ }
23762
+ logWarn(
23763
+ "conversation_queue_message_rejected",
23764
+ error.conversationId ? { conversationId: error.conversationId } : {},
23765
+ {
23766
+ "app.queue.consumer_group": metadata.consumerGroup,
23767
+ "app.queue.delivery_count": metadata.deliveryCount,
23768
+ "app.queue.message_id": metadata.messageId,
23769
+ "app.queue.reject_reason": error.reason,
23770
+ "app.queue.topic_name": metadata.topicName
23771
+ },
23772
+ "Conversation queue message rejected without retry"
23721
23773
  );
23774
+ return { acknowledge: true };
23722
23775
  }
23723
23776
  function createVercelConversationWorkCallback(options) {
23724
23777
  return handleCallback(
23725
23778
  (message) => handleConversationQueueMessage(message, options),
23726
23779
  {
23780
+ retry: handleConversationQueueRetry,
23727
23781
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23728
23782
  }
23729
23783
  );
@@ -23736,6 +23790,7 @@ function registerVercelConversationWorkDevConsumer(options) {
23736
23790
  client: new QueueClient(),
23737
23791
  consumerGroup: CONVERSATION_WORK_DEV_CONSUMER_GROUP,
23738
23792
  handler: (message) => handleConversationQueueMessage(message, options),
23793
+ retry: handleConversationQueueRetry,
23739
23794
  topic: resolveConversationWorkQueueTopic(options),
23740
23795
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23741
23796
  });
@@ -1,12 +1,25 @@
1
1
  import type { ConversationQueueMessage } from "./queue";
2
2
  declare const CONVERSATION_WORK_QUEUE_SIGNATURE_VERSION = "v1";
3
+ export declare const CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS: number;
3
4
  interface SignedConversationQueueMessage extends ConversationQueueMessage {
4
5
  signature: string;
5
6
  signatureVersion: typeof CONVERSATION_WORK_QUEUE_SIGNATURE_VERSION;
6
7
  signedAtMs: number;
7
8
  }
9
+ export type ConversationQueueMessageVerificationResult = {
10
+ message: ConversationQueueMessage;
11
+ status: "verified";
12
+ } | {
13
+ reason: "expired" | "malformed" | "signature_mismatch";
14
+ status: "rejected";
15
+ } | {
16
+ reason: "invalid_clock" | "missing_secret";
17
+ status: "unavailable";
18
+ };
8
19
  /** Sign a conversation queue payload before it crosses the public callback route. */
9
20
  export declare function signConversationQueueMessage(message: ConversationQueueMessage, nowMs?: number): SignedConversationQueueMessage;
21
+ /** Explain whether a queue payload is verified, rejected, or temporarily unverifiable. */
22
+ export declare function verifyConversationQueueMessage(value: unknown, nowMs?: number): ConversationQueueMessageVerificationResult;
10
23
  /** Verify a signed conversation queue payload from the Vercel Queue callback. */
11
24
  export declare function verifySignedConversationQueueMessage(value: unknown, nowMs?: number): ConversationQueueMessage | undefined;
12
25
  export {};
@@ -3,6 +3,16 @@ export interface ConversationQueueMessage {
3
3
  conversationId: string;
4
4
  destination: Destination;
5
5
  }
6
+ export type ConversationQueueMessageRejectReason = "destination_mismatch" | "expired" | "malformed" | "signature_mismatch" | "unauthorized";
7
+ export declare class ConversationQueueMessageRejectedError extends Error {
8
+ conversationId?: string;
9
+ reason: ConversationQueueMessageRejectReason;
10
+ constructor(reason: ConversationQueueMessageRejectReason, message: string, options?: {
11
+ conversationId?: string;
12
+ });
13
+ }
14
+ /** Return whether a queue payload was permanently rejected at the message boundary. */
15
+ export declare function isConversationQueueMessageRejectedError(error: unknown): error is ConversationQueueMessageRejectedError;
6
16
  export interface ConversationQueueSendOptions {
7
17
  delayMs?: number;
8
18
  idempotencyKey?: string;
@@ -1,5 +1,5 @@
1
1
  import type { StateAdapter } from "chat";
2
- import type { ConversationWorkQueue } from "./queue";
2
+ import { type ConversationWorkQueue } from "./queue";
3
3
  import { type ConversationWorkProcessResult, type ConversationWorkerResult, type ConversationWorkerContext } from "./worker";
4
4
  export declare const CONVERSATION_WORK_VISIBILITY_TIMEOUT_BUFFER_SECONDS = 30;
5
5
  export declare const CONVERSATION_WORK_DEV_CONSUMER_GROUP = "junior_conversation_work_dev";
@@ -1,6 +1,7 @@
1
1
  import type { SendOptions, SendResult } from "@vercel/queue";
2
2
  import type { ConversationWorkQueue } from "./queue";
3
3
  export declare const DEFAULT_CONVERSATION_WORK_QUEUE_TOPIC = "junior_conversation_work";
4
+ export declare const CONVERSATION_WORK_QUEUE_RETENTION_SECONDS: number;
4
5
  interface QueueSender {
5
6
  send<T = unknown>(topicName: string, payload: T, options?: SendOptions): Promise<SendResult>;
6
7
  }
@@ -1,6 +1,6 @@
1
1
  import type { StateAdapter } from "chat";
2
2
  import type { Destination } from "@sentry/junior-plugin-api";
3
- import type { ConversationQueueMessage, ConversationWorkQueue } from "./queue";
3
+ import { type ConversationQueueMessage, type ConversationWorkQueue } from "./queue";
4
4
  import { type InboundMessageRecord } from "./store";
5
5
  export declare const CONVERSATION_WORK_DEFER_DELAY_MS = 15000;
6
6
  export declare const CONVERSATION_WORK_SOFT_YIELD_AFTER_MS = 240000;
@@ -163,24 +163,37 @@ function signConversationQueueMessage(message, nowMs = Date.now()) {
163
163
  signature: signPayload(message, nowMs, secret)
164
164
  };
165
165
  }
166
- function verifySignedConversationQueueMessage(value, nowMs = Date.now()) {
166
+ function verifyConversationQueueMessage(value, nowMs = Date.now()) {
167
167
  const message = parseSignedConversationQueueMessage(value);
168
+ if (!message) {
169
+ return { status: "rejected", reason: "malformed" };
170
+ }
168
171
  const secret = getConversationWorkQueueSecret();
169
- if (!message || !secret || !Number.isFinite(nowMs) || Math.abs(nowMs - message.signedAtMs) > CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS) {
170
- return void 0;
172
+ if (!secret) {
173
+ return { status: "unavailable", reason: "missing_secret" };
174
+ }
175
+ if (!Number.isFinite(nowMs)) {
176
+ return { status: "unavailable", reason: "invalid_clock" };
177
+ }
178
+ if (Math.abs(nowMs - message.signedAtMs) > CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS) {
179
+ return { status: "rejected", reason: "expired" };
171
180
  }
172
181
  const expected = signPayload(message, message.signedAtMs, secret);
173
182
  if (!timingSafeMatch(expected, message.signature)) {
174
- return void 0;
183
+ return { status: "rejected", reason: "signature_mismatch" };
175
184
  }
176
185
  return {
177
- conversationId: message.conversationId,
178
- destination: message.destination
186
+ status: "verified",
187
+ message: {
188
+ conversationId: message.conversationId,
189
+ destination: message.destination
190
+ }
179
191
  };
180
192
  }
181
193
 
182
194
  // src/chat/task-execution/vercel-queue.ts
183
195
  var DEFAULT_CONVERSATION_WORK_QUEUE_TOPIC = "junior_conversation_work";
196
+ var CONVERSATION_WORK_QUEUE_RETENTION_SECONDS = CONVERSATION_WORK_QUEUE_SIGNATURE_MAX_SKEW_MS / 1e3;
184
197
  var defaultQueue;
185
198
  function resolveConversationWorkQueueTopic(options = {}) {
186
199
  const topic = options.topic?.trim();
@@ -203,7 +216,7 @@ function createVercelConversationWorkQueue(options = {}) {
203
216
  {
204
217
  idempotencyKey: sendOptions?.idempotencyKey,
205
218
  delaySeconds: toDelaySeconds(sendOptions),
206
- retentionSeconds: options.retentionSeconds
219
+ retentionSeconds: options.retentionSeconds ?? CONVERSATION_WORK_QUEUE_RETENTION_SECONDS
207
220
  }
208
221
  );
209
222
  return result.messageId ? { messageId: result.messageId } : {};
@@ -219,7 +232,7 @@ export {
219
232
  defineJuniorPlugins,
220
233
  pluginCatalogConfigFromPluginSet,
221
234
  pluginHookRegistrationsFromPluginSet,
222
- verifySignedConversationQueueMessage,
235
+ verifyConversationQueueMessage,
223
236
  resolveConversationWorkQueueTopic,
224
237
  getVercelConversationWorkQueue
225
238
  };
package/dist/nitro.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  pluginCatalogConfigFromPluginSet,
3
3
  pluginHookRegistrationsFromPluginSet,
4
4
  resolveConversationWorkQueueTopic
5
- } from "./chunk-IGLNC5H6.js";
5
+ } from "./chunk-XE2VFQQN.js";
6
6
  import "./chunk-76YMBKW7.js";
7
7
  import {
8
8
  JUNIOR_CONVERSATION_WORK_CALLBACK_ROUTE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.71.0",
3
+ "version": "0.71.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -65,7 +65,7 @@
65
65
  "node-html-markdown": "^2.0.0",
66
66
  "yaml": "^2.9.0",
67
67
  "zod": "^4.4.3",
68
- "@sentry/junior-plugin-api": "0.71.0"
68
+ "@sentry/junior-plugin-api": "0.71.1"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.9.1",
@@ -78,7 +78,7 @@
78
78
  "typescript": "^6.0.3",
79
79
  "vercel": "^54.4.0",
80
80
  "vitest": "^4.1.7",
81
- "@sentry/junior-scheduler": "0.71.0"
81
+ "@sentry/junior-scheduler": "0.71.1"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",