@sentry/junior 0.71.0 → 0.71.2

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;
@@ -23711,19 +23738,59 @@ async function processConversationQueueMessage(message, options) {
23711
23738
  state: options.state
23712
23739
  });
23713
23740
  }
23714
- async function handleConversationQueueMessage(message, options) {
23715
- const verified = verifySignedConversationQueueMessage(message);
23716
- if (!verified) {
23717
- throw new Error("Unauthorized conversation queue message");
23741
+ async function handleConversationQueueMessage(message, metadata, options) {
23742
+ const verification = verifyConversationQueueMessage(message);
23743
+ if (verification.status === "rejected") {
23744
+ logConversationQueueMessageRejected(verification.reason, metadata);
23745
+ return;
23746
+ }
23747
+ if (verification.status === "unavailable") {
23748
+ throw new Error(
23749
+ `Conversation queue message verification unavailable: ${verification.reason}`
23750
+ );
23751
+ }
23752
+ try {
23753
+ await runWithTurnRequestDeadline(
23754
+ () => processConversationQueueMessage(verification.message, options)
23755
+ );
23756
+ } catch (error) {
23757
+ if (isConversationQueueMessageRejectedError(error)) {
23758
+ logConversationQueueMessageRejected(error.reason, metadata, {
23759
+ conversationId: error.conversationId
23760
+ });
23761
+ return;
23762
+ }
23763
+ throw error;
23718
23764
  }
23719
- await runWithTurnRequestDeadline(
23720
- () => processConversationQueueMessage(verified, options)
23765
+ }
23766
+ function logConversationQueueMessageRejected(reason, metadata, context = {}) {
23767
+ logWarn(
23768
+ "conversation_queue_message_rejected",
23769
+ context.conversationId ? { conversationId: context.conversationId } : {},
23770
+ {
23771
+ "app.queue.consumer_group": metadata.consumerGroup,
23772
+ "app.queue.delivery_count": metadata.deliveryCount,
23773
+ "app.queue.message_id": metadata.messageId,
23774
+ "app.queue.reject_reason": reason,
23775
+ "app.queue.topic_name": metadata.topicName
23776
+ },
23777
+ "Conversation queue message rejected without retry"
23721
23778
  );
23722
23779
  }
23780
+ function handleConversationQueueRetry(error, metadata) {
23781
+ if (!isConversationQueueMessageRejectedError(error)) {
23782
+ return void 0;
23783
+ }
23784
+ logConversationQueueMessageRejected(error.reason, metadata, {
23785
+ conversationId: error.conversationId
23786
+ });
23787
+ return { acknowledge: true };
23788
+ }
23723
23789
  function createVercelConversationWorkCallback(options) {
23724
23790
  return handleCallback(
23725
- (message) => handleConversationQueueMessage(message, options),
23791
+ (message, metadata) => handleConversationQueueMessage(message, metadata, options),
23726
23792
  {
23793
+ retry: handleConversationQueueRetry,
23727
23794
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23728
23795
  }
23729
23796
  );
@@ -23735,7 +23802,8 @@ function registerVercelConversationWorkDevConsumer(options) {
23735
23802
  return registerDevConsumer({
23736
23803
  client: new QueueClient(),
23737
23804
  consumerGroup: CONVERSATION_WORK_DEV_CONSUMER_GROUP,
23738
- handler: (message) => handleConversationQueueMessage(message, options),
23805
+ handler: (message, metadata) => handleConversationQueueMessage(message, metadata, options),
23806
+ retry: handleConversationQueueRetry,
23739
23807
  topic: resolveConversationWorkQueueTopic(options),
23740
23808
  visibilityTimeoutSeconds: options.visibilityTimeoutSeconds ?? resolveConversationWorkVisibilityTimeoutSeconds()
23741
23809
  });
@@ -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.2",
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.2"
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.2"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",