@hookflo/tern 3.0.13-beta → 3.0.17-beta
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/index.js +1 -1
- package/dist/upstash/controls.d.ts +1 -1
- package/dist/upstash/controls.js +48 -10
- package/dist/upstash/queue.js +46 -4
- package/dist/upstash/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -134,7 +134,7 @@ class WebhookVerificationService {
|
|
|
134
134
|
}
|
|
135
135
|
static resolveCanonicalEventId(platform, metadata) {
|
|
136
136
|
const rawId = this.resolveRawEventId(platform, metadata);
|
|
137
|
-
return `${platform}
|
|
137
|
+
return `${platform}_${rawId}`;
|
|
138
138
|
}
|
|
139
139
|
static resolveRawEventId(platform, metadata) {
|
|
140
140
|
const candidate = metadata?.id || metadata?.delivery || metadata?.requestId || metadata?.timestamp;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DLQMessage, EventFilter, ReplayResult, TernControlsConfig, TernEvent } from './types';
|
|
2
2
|
export declare function createTernControls(config: TernControlsConfig): {
|
|
3
3
|
dlq(): Promise<DLQMessage[]>;
|
|
4
|
-
replay(
|
|
4
|
+
replay(dlqId: string): Promise<ReplayResult>;
|
|
5
5
|
events(filter?: EventFilter): Promise<TernEvent[]>;
|
|
6
6
|
};
|
package/dist/upstash/controls.js
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createTernControls = createTernControls;
|
|
4
4
|
const QSTASH_API_BASE = 'https://qstash.upstash.io/v2';
|
|
5
|
+
function parseBody(body) {
|
|
6
|
+
if (!body)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (typeof body === 'object') {
|
|
9
|
+
return body;
|
|
10
|
+
}
|
|
11
|
+
if (typeof body === 'string') {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(body);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
try {
|
|
17
|
+
const decoded = Buffer.from(body, 'base64').toString('utf8');
|
|
18
|
+
return JSON.parse(decoded);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function resolvePlatform(message) {
|
|
28
|
+
const headers = message.headers;
|
|
29
|
+
const headerPlatform = headers?.['x-tern-platform'] ?? headers?.['X-Tern-Platform'];
|
|
30
|
+
if (typeof headerPlatform === 'string' && headerPlatform.trim().length > 0) {
|
|
31
|
+
return headerPlatform;
|
|
32
|
+
}
|
|
33
|
+
const body = parseBody(message.body);
|
|
34
|
+
const bodyPlatform = body?.platform;
|
|
35
|
+
if (typeof bodyPlatform === 'string' && bodyPlatform.trim().length > 0) {
|
|
36
|
+
return bodyPlatform;
|
|
37
|
+
}
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
5
40
|
function createHeaders(token) {
|
|
6
41
|
return {
|
|
7
42
|
Authorization: `Bearer ${token}`,
|
|
@@ -23,31 +58,34 @@ function createTernControls(config) {
|
|
|
23
58
|
headers: createHeaders(config.token),
|
|
24
59
|
});
|
|
25
60
|
if (!response.ok) {
|
|
26
|
-
throw new Error(`[tern] Failed to fetch DLQ
|
|
61
|
+
throw new Error(`[tern] Failed to fetch DLQ: ${response.status} ${response.statusText}`);
|
|
27
62
|
}
|
|
28
63
|
const data = await response.json();
|
|
29
64
|
const messages = data.messages || [];
|
|
30
65
|
return messages.map((message) => ({
|
|
31
66
|
id: String(message.messageId || message.id || ''),
|
|
32
|
-
|
|
33
|
-
|
|
67
|
+
dlqId: String(message.dlqId || ''),
|
|
68
|
+
platform: resolvePlatform(message),
|
|
69
|
+
payload: parseBody(message.body)?.payload,
|
|
34
70
|
failedAt: String(message.updatedAt || message.createdAt || new Date().toISOString()),
|
|
35
71
|
attempts: Number(message.retried || message.attempts || 0),
|
|
36
72
|
error: typeof message.error === 'string' ? message.error : undefined,
|
|
37
73
|
}));
|
|
38
74
|
},
|
|
39
|
-
async replay(
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
async replay(dlqId) {
|
|
76
|
+
if (!dlqId || dlqId.trim() === '') {
|
|
77
|
+
throw new Error('[tern] replay() requires a dlqId, not a messageId. Get dlqId from controls.dlq()');
|
|
78
|
+
}
|
|
79
|
+
const response = await fetch(`${QSTASH_API_BASE}/dlq/${dlqId}`, {
|
|
80
|
+
method: 'DELETE',
|
|
42
81
|
headers: createHeaders(config.token),
|
|
43
82
|
});
|
|
44
83
|
if (!response.ok) {
|
|
45
|
-
throw new Error(`[tern] Failed to replay DLQ message ${
|
|
84
|
+
throw new Error(`[tern] Failed to replay DLQ message ${dlqId}: ${response.status} ${response.statusText}`);
|
|
46
85
|
}
|
|
47
|
-
const data = await response.json();
|
|
48
86
|
return {
|
|
49
87
|
success: true,
|
|
50
|
-
messageId:
|
|
88
|
+
messageId: dlqId,
|
|
51
89
|
replayedAt: new Date().toISOString(),
|
|
52
90
|
};
|
|
53
91
|
},
|
|
@@ -64,7 +102,7 @@ function createTernControls(config) {
|
|
|
64
102
|
const status = deriveStatus(String(message.state || message.status || 'retrying'));
|
|
65
103
|
return {
|
|
66
104
|
id: String(message.messageId || message.id || ''),
|
|
67
|
-
platform:
|
|
105
|
+
platform: resolvePlatform(message),
|
|
68
106
|
status,
|
|
69
107
|
attempts: Number(message.retried || message.attempts || 0),
|
|
70
108
|
createdAt: String(message.createdAt || new Date().toISOString()),
|
package/dist/upstash/queue.js
CHANGED
|
@@ -133,6 +133,44 @@ function resolveQueueConfig(queue) {
|
|
|
133
133
|
}
|
|
134
134
|
return queue;
|
|
135
135
|
}
|
|
136
|
+
function toStableJson(value) {
|
|
137
|
+
if (Array.isArray(value)) {
|
|
138
|
+
return `[${value.map((item) => toStableJson(item)).join(',')}]`;
|
|
139
|
+
}
|
|
140
|
+
if (value && typeof value === 'object') {
|
|
141
|
+
const entries = Object.entries(value)
|
|
142
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
143
|
+
.map(([key, nested]) => `${JSON.stringify(key)}:${toStableJson(nested)}`);
|
|
144
|
+
return `{${entries.join(',')}}`;
|
|
145
|
+
}
|
|
146
|
+
return JSON.stringify(value);
|
|
147
|
+
}
|
|
148
|
+
async function resolveDeduplicationId(request, verificationResult) {
|
|
149
|
+
const payload = verificationResult.payload;
|
|
150
|
+
const headers = request.headers;
|
|
151
|
+
const payloadId = typeof payload?.id === 'string' ? payload.id : null;
|
|
152
|
+
const payloadRequestId = typeof payload?.request_id === 'string' ? payload.request_id : null;
|
|
153
|
+
const nestedPayloadId = payload?.data && typeof payload.data === 'object' && typeof payload.data.id === 'string'
|
|
154
|
+
? payload.data.id
|
|
155
|
+
: null;
|
|
156
|
+
const explicit = headers.get('x-webhook-id') ||
|
|
157
|
+
headers.get('x-github-delivery') ||
|
|
158
|
+
headers.get('idempotency-key') ||
|
|
159
|
+
headers.get('upstash-deduplication-id') ||
|
|
160
|
+
verificationResult.eventId ||
|
|
161
|
+
(typeof verificationResult.metadata?.id === 'string' ? verificationResult.metadata.id : null) ||
|
|
162
|
+
payloadId ||
|
|
163
|
+
payloadRequestId ||
|
|
164
|
+
nestedPayloadId;
|
|
165
|
+
if (explicit)
|
|
166
|
+
return explicit;
|
|
167
|
+
const raw = toStableJson(payload || {});
|
|
168
|
+
const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(raw));
|
|
169
|
+
const hash = Array.from(new Uint8Array(buffer))
|
|
170
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
171
|
+
.join('');
|
|
172
|
+
return hash.slice(0, 32);
|
|
173
|
+
}
|
|
136
174
|
async function handleReceive(request, platform, secret, queueConfig, toleranceInSeconds) {
|
|
137
175
|
const verificationResult = await index_1.WebhookVerificationService.verifyWithPlatformConfig(request.clone(), platform, secret, toleranceInSeconds);
|
|
138
176
|
if (!verificationResult.isValid) {
|
|
@@ -144,13 +182,17 @@ async function handleReceive(request, platform, secret, queueConfig, toleranceIn
|
|
|
144
182
|
payload: verificationResult.payload,
|
|
145
183
|
metadata: verificationResult.metadata || {},
|
|
146
184
|
};
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
const deduplicationId = await resolveDeduplicationId(request, verificationResult);
|
|
186
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
187
|
+
console.log(`[tern] deduplication-id: ${deduplicationId} (platform: ${platform})`);
|
|
188
|
+
}
|
|
150
189
|
const publishPayload = {
|
|
151
190
|
url: request.url,
|
|
152
191
|
body: queuedMessage,
|
|
153
|
-
deduplicationId
|
|
192
|
+
deduplicationId,
|
|
193
|
+
headers: {
|
|
194
|
+
'x-tern-platform': platform,
|
|
195
|
+
},
|
|
154
196
|
};
|
|
155
197
|
if (queueConfig.retries !== undefined) {
|
|
156
198
|
publishPayload.retries = queueConfig.retries;
|
package/dist/upstash/types.d.ts
CHANGED
package/package.json
CHANGED