@findatruck/convex-client 0.1.0 → 0.2.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/index.cjs CHANGED
@@ -65,11 +65,19 @@ var ConvexClient = class {
65
65
  constructor(config) {
66
66
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
67
67
  this.workerSecret = config.workerSecret;
68
+ this.retryConfig = {
69
+ maxAttempts: config.retry?.maxAttempts ?? 5,
70
+ baseDelayMs: config.retry?.baseDelayMs ?? 250,
71
+ maxDelayMs: config.retry?.maxDelayMs ?? 15e3,
72
+ maxElapsedMs: config.retry?.maxElapsedMs ?? 6e4,
73
+ perAttemptTimeoutMs: config.retry?.perAttemptTimeoutMs ?? 2e4
74
+ };
68
75
  }
69
76
  /**
70
77
  * Posts driver view data to the Convex ingestion endpoint.
78
+ * Uses retry logic with exponential backoff for resilience.
71
79
  */
72
- async postDriverView(data) {
80
+ async postDriverView(data, options) {
73
81
  const route = routes.ingestDriverView;
74
82
  const parseResult = route.requestSchema.safeParse(data);
75
83
  if (!parseResult.success) {
@@ -78,12 +86,14 @@ var ConvexClient = class {
78
86
  route.path
79
87
  );
80
88
  }
81
- await this.makeRequest(route.path, route.method, data);
89
+ const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
90
+ await this.makeRequestWithRetry(effectiveBaseUrl, route.path, route.method, data);
82
91
  }
83
92
  /**
84
93
  * Posts provider account status updates to the Convex ingestion endpoint.
94
+ * Does NOT use retry logic (single attempt only).
85
95
  */
86
- async postProviderAccountStatus(data) {
96
+ async postProviderAccountStatus(data, options) {
87
97
  const route = routes.ingestProviderAccountStatus;
88
98
  const parseResult = route.requestSchema.safeParse(data);
89
99
  if (!parseResult.success) {
@@ -92,15 +102,16 @@ var ConvexClient = class {
92
102
  route.path
93
103
  );
94
104
  }
95
- await this.makeRequest(route.path, route.method, data);
105
+ const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
106
+ await this.makeRequest(effectiveBaseUrl, route.path, route.method, data);
96
107
  }
97
- async makeRequest(path, method, body) {
98
- const url = `${this.baseUrl}${path}`;
108
+ async makeRequest(baseUrl, path, method, body) {
109
+ const url = `${baseUrl}${path}`;
99
110
  const response = await fetch(url, {
100
111
  method,
101
112
  headers: {
102
113
  "Content-Type": "application/json",
103
- "Authorization": `Bearer ${this.workerSecret}`
114
+ "x-worker-secret": this.workerSecret
104
115
  },
105
116
  body: JSON.stringify(body)
106
117
  });
@@ -119,21 +130,151 @@ var ConvexClient = class {
119
130
  );
120
131
  }
121
132
  }
133
+ async makeRequestWithRetry(baseUrl, path, method, body) {
134
+ const url = `${baseUrl}${path}`;
135
+ const { maxAttempts, baseDelayMs, maxDelayMs, maxElapsedMs, perAttemptTimeoutMs } = this.retryConfig;
136
+ const retryOnStatuses = [429, 500, 502, 503, 504];
137
+ const startedAt = Date.now();
138
+ let attempt = 0;
139
+ const calcBackoff = (retriesSoFar) => Math.floor(Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** retriesSoFar));
140
+ while (true) {
141
+ attempt += 1;
142
+ if (Date.now() - startedAt > maxElapsedMs) {
143
+ throw new ConvexClientError(
144
+ `Request to ${path} exceeded max elapsed time ${maxElapsedMs}ms`,
145
+ path
146
+ );
147
+ }
148
+ let response;
149
+ let error;
150
+ try {
151
+ response = await this.attemptFetch(url, method, body, perAttemptTimeoutMs);
152
+ if (response.ok) return;
153
+ if (!retryOnStatuses.includes(response.status)) {
154
+ let responseBody;
155
+ try {
156
+ responseBody = await response.json();
157
+ } catch {
158
+ responseBody = await response.text();
159
+ }
160
+ throw new ConvexClientError(
161
+ `Request to ${path} failed with status ${response.status}`,
162
+ path,
163
+ response.status,
164
+ responseBody
165
+ );
166
+ }
167
+ } catch (e) {
168
+ error = e;
169
+ if (e instanceof ConvexClientError) throw e;
170
+ if (this.isAbortError(e)) throw e;
171
+ }
172
+ if (attempt >= maxAttempts) {
173
+ if (response) {
174
+ let responseBody;
175
+ try {
176
+ responseBody = await response.json();
177
+ } catch {
178
+ responseBody = await response.text();
179
+ }
180
+ throw new ConvexClientError(
181
+ `Request to ${path} failed after ${attempt} attempts with status ${response.status}`,
182
+ path,
183
+ response.status,
184
+ responseBody
185
+ );
186
+ }
187
+ if (error instanceof Error) throw error;
188
+ throw new ConvexClientError(
189
+ `Request to ${path} failed without response after ${attempt} attempts`,
190
+ path
191
+ );
192
+ }
193
+ let delayMs = 0;
194
+ const retryAfter = response?.headers.get("retry-after");
195
+ if (retryAfter) {
196
+ const parsed = this.parseRetryAfter(retryAfter);
197
+ if (parsed != null) delayMs = Math.min(parsed, maxDelayMs);
198
+ }
199
+ if (delayMs === 0) {
200
+ delayMs = calcBackoff(attempt - 1);
201
+ }
202
+ const elapsed = Date.now() - startedAt;
203
+ if (elapsed + delayMs > maxElapsedMs) {
204
+ delayMs = Math.max(0, maxElapsedMs - elapsed);
205
+ if (delayMs === 0) {
206
+ if (response) {
207
+ throw new ConvexClientError(
208
+ `Request to ${path} time budget exhausted before retry`,
209
+ path,
210
+ response.status
211
+ );
212
+ }
213
+ if (error instanceof Error) throw error;
214
+ throw new ConvexClientError(
215
+ `Request to ${path} time budget exhausted before retry`,
216
+ path
217
+ );
218
+ }
219
+ }
220
+ await this.sleep(delayMs);
221
+ }
222
+ }
223
+ async attemptFetch(url, method, body, perAttemptTimeoutMs) {
224
+ const controller = new AbortController();
225
+ const timeoutId = perAttemptTimeoutMs > 0 ? setTimeout(() => {
226
+ controller.abort(new Error(`per-attempt timeout ${perAttemptTimeoutMs}ms`));
227
+ }, perAttemptTimeoutMs) : null;
228
+ try {
229
+ return await fetch(url, {
230
+ method,
231
+ headers: {
232
+ "Content-Type": "application/json",
233
+ "x-worker-secret": this.workerSecret
234
+ },
235
+ body: JSON.stringify(body),
236
+ signal: controller.signal
237
+ });
238
+ } finally {
239
+ if (timeoutId) clearTimeout(timeoutId);
240
+ }
241
+ }
242
+ parseRetryAfter(value) {
243
+ const secs = Number(value);
244
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
245
+ const dateMs = Date.parse(value);
246
+ if (!Number.isNaN(dateMs)) {
247
+ const delta = dateMs - Date.now();
248
+ return Math.max(0, delta);
249
+ }
250
+ return null;
251
+ }
252
+ sleep(ms) {
253
+ return new Promise((resolve) => {
254
+ if (ms <= 0) {
255
+ resolve();
256
+ return;
257
+ }
258
+ setTimeout(resolve, ms);
259
+ });
260
+ }
261
+ isAbortError(err) {
262
+ return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && /abort/i.test(err.name + " " + err.message);
263
+ }
122
264
  };
123
265
 
124
266
  // src/validation.ts
125
267
  function checkWorkerSecret(req, expectedSecret) {
126
- const authHeader = req.headers.get("Authorization");
127
- if (!authHeader) {
268
+ const secretHeader = req.headers.get("x-worker-secret");
269
+ if (!secretHeader) {
128
270
  return false;
129
271
  }
130
- const expectedValue = `Bearer ${expectedSecret}`;
131
- if (authHeader.length !== expectedValue.length) {
272
+ if (secretHeader.length !== expectedSecret.length) {
132
273
  return false;
133
274
  }
134
275
  let result = 0;
135
- for (let i = 0; i < authHeader.length; i++) {
136
- result |= authHeader.charCodeAt(i) ^ expectedValue.charCodeAt(i);
276
+ for (let i = 0; i < secretHeader.length; i++) {
277
+ result |= secretHeader.charCodeAt(i) ^ expectedSecret.charCodeAt(i);
137
278
  }
138
279
  return result === 0;
139
280
  }
package/dist/index.d.cts CHANGED
@@ -3,6 +3,21 @@ import { ConvexUpdateData, UpdateScrapeStatusMessage } from '@findatruck/shared-
3
3
  export { BatchConvexUpdateData, BatchConvexUpdateSchema, ConvexDriverData, ConvexDriverSchema, ConvexUpdateData, ConvexUpdateSchema, ScrapeStatus, UpdateScrapeStatusMessage, UpdateScrapeStatusMessage as UpdateScrapeStatusMessageType } from '@findatruck/shared-schemas';
4
4
  import { z } from 'zod';
5
5
 
6
+ /**
7
+ * Configuration for retry behavior
8
+ */
9
+ interface RetryConfig {
10
+ /** Max number of attempts (first try + retries). Default: 5 */
11
+ maxAttempts?: number;
12
+ /** Base delay in ms before backoff. Default: 250 */
13
+ baseDelayMs?: number;
14
+ /** Max delay cap in ms. Default: 15_000 */
15
+ maxDelayMs?: number;
16
+ /** Max total elapsed time budget in ms (includes all attempts). Default: 60_000 */
17
+ maxElapsedMs?: number;
18
+ /** Per-attempt timeout in ms. Default: 20_000 */
19
+ perAttemptTimeoutMs?: number;
20
+ }
6
21
  /**
7
22
  * Configuration for the ConvexClient
8
23
  */
@@ -11,6 +26,15 @@ interface ConvexClientConfig {
11
26
  baseUrl: string;
12
27
  /** Worker shared secret for authentication */
13
28
  workerSecret: string;
29
+ /** Optional retry configuration for postDriverView (postProviderAccountStatus does not retry) */
30
+ retry?: RetryConfig;
31
+ }
32
+ /**
33
+ * Options for individual request methods
34
+ */
35
+ interface RequestOptions {
36
+ /** Override the base URL for this specific request */
37
+ baseUrl?: string;
14
38
  }
15
39
  /**
16
40
  * Error thrown by the ConvexClient when requests fail
@@ -40,6 +64,10 @@ type ValidationResult<T> = {
40
64
  * const client = new ConvexClient({
41
65
  * baseUrl: process.env.CONVEX_HTTP_URL,
42
66
  * workerSecret: process.env.WORKER_SHARED_SECRET,
67
+ * retry: {
68
+ * maxAttempts: 5,
69
+ * perAttemptTimeoutMs: 5000,
70
+ * },
43
71
  * });
44
72
  *
45
73
  * await client.postDriverView(convexUpdate);
@@ -49,16 +77,24 @@ type ValidationResult<T> = {
49
77
  declare class ConvexClient {
50
78
  private readonly baseUrl;
51
79
  private readonly workerSecret;
80
+ private readonly retryConfig;
52
81
  constructor(config: ConvexClientConfig);
53
82
  /**
54
83
  * Posts driver view data to the Convex ingestion endpoint.
84
+ * Uses retry logic with exponential backoff for resilience.
55
85
  */
56
- postDriverView(data: ConvexUpdateData): Promise<void>;
86
+ postDriverView(data: ConvexUpdateData, options?: RequestOptions): Promise<void>;
57
87
  /**
58
88
  * Posts provider account status updates to the Convex ingestion endpoint.
89
+ * Does NOT use retry logic (single attempt only).
59
90
  */
60
- postProviderAccountStatus(data: UpdateScrapeStatusMessage): Promise<void>;
91
+ postProviderAccountStatus(data: UpdateScrapeStatusMessage, options?: RequestOptions): Promise<void>;
61
92
  private makeRequest;
93
+ private makeRequestWithRetry;
94
+ private attemptFetch;
95
+ private parseRetryAfter;
96
+ private sleep;
97
+ private isAbortError;
62
98
  }
63
99
 
64
100
  /**
@@ -121,7 +157,7 @@ type RouteName = keyof typeof routes;
121
157
 
122
158
  /**
123
159
  * Constant-time string comparison to prevent timing attacks.
124
- * Returns true if the Authorization header matches the expected Bearer token.
160
+ * Returns true if the x-worker-secret header matches the expected secret.
125
161
  */
126
162
  declare function checkWorkerSecret(req: Request, expectedSecret: string): boolean;
127
163
  /**
@@ -151,4 +187,4 @@ declare const responses: {
151
187
  readonly serverError: (error?: string) => Response;
152
188
  };
153
189
 
154
- export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
190
+ export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RequestOptions, type RetryConfig, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,21 @@ import { ConvexUpdateData, UpdateScrapeStatusMessage } from '@findatruck/shared-
3
3
  export { BatchConvexUpdateData, BatchConvexUpdateSchema, ConvexDriverData, ConvexDriverSchema, ConvexUpdateData, ConvexUpdateSchema, ScrapeStatus, UpdateScrapeStatusMessage, UpdateScrapeStatusMessage as UpdateScrapeStatusMessageType } from '@findatruck/shared-schemas';
4
4
  import { z } from 'zod';
5
5
 
6
+ /**
7
+ * Configuration for retry behavior
8
+ */
9
+ interface RetryConfig {
10
+ /** Max number of attempts (first try + retries). Default: 5 */
11
+ maxAttempts?: number;
12
+ /** Base delay in ms before backoff. Default: 250 */
13
+ baseDelayMs?: number;
14
+ /** Max delay cap in ms. Default: 15_000 */
15
+ maxDelayMs?: number;
16
+ /** Max total elapsed time budget in ms (includes all attempts). Default: 60_000 */
17
+ maxElapsedMs?: number;
18
+ /** Per-attempt timeout in ms. Default: 20_000 */
19
+ perAttemptTimeoutMs?: number;
20
+ }
6
21
  /**
7
22
  * Configuration for the ConvexClient
8
23
  */
@@ -11,6 +26,15 @@ interface ConvexClientConfig {
11
26
  baseUrl: string;
12
27
  /** Worker shared secret for authentication */
13
28
  workerSecret: string;
29
+ /** Optional retry configuration for postDriverView (postProviderAccountStatus does not retry) */
30
+ retry?: RetryConfig;
31
+ }
32
+ /**
33
+ * Options for individual request methods
34
+ */
35
+ interface RequestOptions {
36
+ /** Override the base URL for this specific request */
37
+ baseUrl?: string;
14
38
  }
15
39
  /**
16
40
  * Error thrown by the ConvexClient when requests fail
@@ -40,6 +64,10 @@ type ValidationResult<T> = {
40
64
  * const client = new ConvexClient({
41
65
  * baseUrl: process.env.CONVEX_HTTP_URL,
42
66
  * workerSecret: process.env.WORKER_SHARED_SECRET,
67
+ * retry: {
68
+ * maxAttempts: 5,
69
+ * perAttemptTimeoutMs: 5000,
70
+ * },
43
71
  * });
44
72
  *
45
73
  * await client.postDriverView(convexUpdate);
@@ -49,16 +77,24 @@ type ValidationResult<T> = {
49
77
  declare class ConvexClient {
50
78
  private readonly baseUrl;
51
79
  private readonly workerSecret;
80
+ private readonly retryConfig;
52
81
  constructor(config: ConvexClientConfig);
53
82
  /**
54
83
  * Posts driver view data to the Convex ingestion endpoint.
84
+ * Uses retry logic with exponential backoff for resilience.
55
85
  */
56
- postDriverView(data: ConvexUpdateData): Promise<void>;
86
+ postDriverView(data: ConvexUpdateData, options?: RequestOptions): Promise<void>;
57
87
  /**
58
88
  * Posts provider account status updates to the Convex ingestion endpoint.
89
+ * Does NOT use retry logic (single attempt only).
59
90
  */
60
- postProviderAccountStatus(data: UpdateScrapeStatusMessage): Promise<void>;
91
+ postProviderAccountStatus(data: UpdateScrapeStatusMessage, options?: RequestOptions): Promise<void>;
61
92
  private makeRequest;
93
+ private makeRequestWithRetry;
94
+ private attemptFetch;
95
+ private parseRetryAfter;
96
+ private sleep;
97
+ private isAbortError;
62
98
  }
63
99
 
64
100
  /**
@@ -121,7 +157,7 @@ type RouteName = keyof typeof routes;
121
157
 
122
158
  /**
123
159
  * Constant-time string comparison to prevent timing attacks.
124
- * Returns true if the Authorization header matches the expected Bearer token.
160
+ * Returns true if the x-worker-secret header matches the expected secret.
125
161
  */
126
162
  declare function checkWorkerSecret(req: Request, expectedSecret: string): boolean;
127
163
  /**
@@ -151,4 +187,4 @@ declare const responses: {
151
187
  readonly serverError: (error?: string) => Response;
152
188
  };
153
189
 
154
- export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
190
+ export { ConvexClient, type ConvexClientConfig, ConvexClientError, type RequestOptions, type RetryConfig, type RouteDefinition, type RouteName, type ValidationResult, checkWorkerSecret, parseAndValidate, responses, routes };
package/dist/index.js CHANGED
@@ -32,11 +32,19 @@ var ConvexClient = class {
32
32
  constructor(config) {
33
33
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
34
34
  this.workerSecret = config.workerSecret;
35
+ this.retryConfig = {
36
+ maxAttempts: config.retry?.maxAttempts ?? 5,
37
+ baseDelayMs: config.retry?.baseDelayMs ?? 250,
38
+ maxDelayMs: config.retry?.maxDelayMs ?? 15e3,
39
+ maxElapsedMs: config.retry?.maxElapsedMs ?? 6e4,
40
+ perAttemptTimeoutMs: config.retry?.perAttemptTimeoutMs ?? 2e4
41
+ };
35
42
  }
36
43
  /**
37
44
  * Posts driver view data to the Convex ingestion endpoint.
45
+ * Uses retry logic with exponential backoff for resilience.
38
46
  */
39
- async postDriverView(data) {
47
+ async postDriverView(data, options) {
40
48
  const route = routes.ingestDriverView;
41
49
  const parseResult = route.requestSchema.safeParse(data);
42
50
  if (!parseResult.success) {
@@ -45,12 +53,14 @@ var ConvexClient = class {
45
53
  route.path
46
54
  );
47
55
  }
48
- await this.makeRequest(route.path, route.method, data);
56
+ const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
57
+ await this.makeRequestWithRetry(effectiveBaseUrl, route.path, route.method, data);
49
58
  }
50
59
  /**
51
60
  * Posts provider account status updates to the Convex ingestion endpoint.
61
+ * Does NOT use retry logic (single attempt only).
52
62
  */
53
- async postProviderAccountStatus(data) {
63
+ async postProviderAccountStatus(data, options) {
54
64
  const route = routes.ingestProviderAccountStatus;
55
65
  const parseResult = route.requestSchema.safeParse(data);
56
66
  if (!parseResult.success) {
@@ -59,15 +69,16 @@ var ConvexClient = class {
59
69
  route.path
60
70
  );
61
71
  }
62
- await this.makeRequest(route.path, route.method, data);
72
+ const effectiveBaseUrl = options?.baseUrl?.replace(/\/$/, "") ?? this.baseUrl;
73
+ await this.makeRequest(effectiveBaseUrl, route.path, route.method, data);
63
74
  }
64
- async makeRequest(path, method, body) {
65
- const url = `${this.baseUrl}${path}`;
75
+ async makeRequest(baseUrl, path, method, body) {
76
+ const url = `${baseUrl}${path}`;
66
77
  const response = await fetch(url, {
67
78
  method,
68
79
  headers: {
69
80
  "Content-Type": "application/json",
70
- "Authorization": `Bearer ${this.workerSecret}`
81
+ "x-worker-secret": this.workerSecret
71
82
  },
72
83
  body: JSON.stringify(body)
73
84
  });
@@ -86,21 +97,151 @@ var ConvexClient = class {
86
97
  );
87
98
  }
88
99
  }
100
+ async makeRequestWithRetry(baseUrl, path, method, body) {
101
+ const url = `${baseUrl}${path}`;
102
+ const { maxAttempts, baseDelayMs, maxDelayMs, maxElapsedMs, perAttemptTimeoutMs } = this.retryConfig;
103
+ const retryOnStatuses = [429, 500, 502, 503, 504];
104
+ const startedAt = Date.now();
105
+ let attempt = 0;
106
+ const calcBackoff = (retriesSoFar) => Math.floor(Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** retriesSoFar));
107
+ while (true) {
108
+ attempt += 1;
109
+ if (Date.now() - startedAt > maxElapsedMs) {
110
+ throw new ConvexClientError(
111
+ `Request to ${path} exceeded max elapsed time ${maxElapsedMs}ms`,
112
+ path
113
+ );
114
+ }
115
+ let response;
116
+ let error;
117
+ try {
118
+ response = await this.attemptFetch(url, method, body, perAttemptTimeoutMs);
119
+ if (response.ok) return;
120
+ if (!retryOnStatuses.includes(response.status)) {
121
+ let responseBody;
122
+ try {
123
+ responseBody = await response.json();
124
+ } catch {
125
+ responseBody = await response.text();
126
+ }
127
+ throw new ConvexClientError(
128
+ `Request to ${path} failed with status ${response.status}`,
129
+ path,
130
+ response.status,
131
+ responseBody
132
+ );
133
+ }
134
+ } catch (e) {
135
+ error = e;
136
+ if (e instanceof ConvexClientError) throw e;
137
+ if (this.isAbortError(e)) throw e;
138
+ }
139
+ if (attempt >= maxAttempts) {
140
+ if (response) {
141
+ let responseBody;
142
+ try {
143
+ responseBody = await response.json();
144
+ } catch {
145
+ responseBody = await response.text();
146
+ }
147
+ throw new ConvexClientError(
148
+ `Request to ${path} failed after ${attempt} attempts with status ${response.status}`,
149
+ path,
150
+ response.status,
151
+ responseBody
152
+ );
153
+ }
154
+ if (error instanceof Error) throw error;
155
+ throw new ConvexClientError(
156
+ `Request to ${path} failed without response after ${attempt} attempts`,
157
+ path
158
+ );
159
+ }
160
+ let delayMs = 0;
161
+ const retryAfter = response?.headers.get("retry-after");
162
+ if (retryAfter) {
163
+ const parsed = this.parseRetryAfter(retryAfter);
164
+ if (parsed != null) delayMs = Math.min(parsed, maxDelayMs);
165
+ }
166
+ if (delayMs === 0) {
167
+ delayMs = calcBackoff(attempt - 1);
168
+ }
169
+ const elapsed = Date.now() - startedAt;
170
+ if (elapsed + delayMs > maxElapsedMs) {
171
+ delayMs = Math.max(0, maxElapsedMs - elapsed);
172
+ if (delayMs === 0) {
173
+ if (response) {
174
+ throw new ConvexClientError(
175
+ `Request to ${path} time budget exhausted before retry`,
176
+ path,
177
+ response.status
178
+ );
179
+ }
180
+ if (error instanceof Error) throw error;
181
+ throw new ConvexClientError(
182
+ `Request to ${path} time budget exhausted before retry`,
183
+ path
184
+ );
185
+ }
186
+ }
187
+ await this.sleep(delayMs);
188
+ }
189
+ }
190
+ async attemptFetch(url, method, body, perAttemptTimeoutMs) {
191
+ const controller = new AbortController();
192
+ const timeoutId = perAttemptTimeoutMs > 0 ? setTimeout(() => {
193
+ controller.abort(new Error(`per-attempt timeout ${perAttemptTimeoutMs}ms`));
194
+ }, perAttemptTimeoutMs) : null;
195
+ try {
196
+ return await fetch(url, {
197
+ method,
198
+ headers: {
199
+ "Content-Type": "application/json",
200
+ "x-worker-secret": this.workerSecret
201
+ },
202
+ body: JSON.stringify(body),
203
+ signal: controller.signal
204
+ });
205
+ } finally {
206
+ if (timeoutId) clearTimeout(timeoutId);
207
+ }
208
+ }
209
+ parseRetryAfter(value) {
210
+ const secs = Number(value);
211
+ if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
212
+ const dateMs = Date.parse(value);
213
+ if (!Number.isNaN(dateMs)) {
214
+ const delta = dateMs - Date.now();
215
+ return Math.max(0, delta);
216
+ }
217
+ return null;
218
+ }
219
+ sleep(ms) {
220
+ return new Promise((resolve) => {
221
+ if (ms <= 0) {
222
+ resolve();
223
+ return;
224
+ }
225
+ setTimeout(resolve, ms);
226
+ });
227
+ }
228
+ isAbortError(err) {
229
+ return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && /abort/i.test(err.name + " " + err.message);
230
+ }
89
231
  };
90
232
 
91
233
  // src/validation.ts
92
234
  function checkWorkerSecret(req, expectedSecret) {
93
- const authHeader = req.headers.get("Authorization");
94
- if (!authHeader) {
235
+ const secretHeader = req.headers.get("x-worker-secret");
236
+ if (!secretHeader) {
95
237
  return false;
96
238
  }
97
- const expectedValue = `Bearer ${expectedSecret}`;
98
- if (authHeader.length !== expectedValue.length) {
239
+ if (secretHeader.length !== expectedSecret.length) {
99
240
  return false;
100
241
  }
101
242
  let result = 0;
102
- for (let i = 0; i < authHeader.length; i++) {
103
- result |= authHeader.charCodeAt(i) ^ expectedValue.charCodeAt(i);
243
+ for (let i = 0; i < secretHeader.length; i++) {
244
+ result |= secretHeader.charCodeAt(i) ^ expectedSecret.charCodeAt(i);
104
245
  }
105
246
  return result === 0;
106
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findatruck/convex-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@findatruck/shared-schemas": "^2.9.0"
26
+ "@findatruck/shared-schemas": "^2.10.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "zod": "^4.1.11"