@striae-org/striae 5.4.3 → 5.4.5

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.
@@ -0,0 +1,53 @@
1
+ import { handleImageDelete } from './handlers/delete-image';
2
+ import { handleSignedUrlMinting } from './handlers/mint-signed-url';
3
+ import { handleImageServing } from './handlers/serve-image';
4
+ import { handleImageUpload } from './handlers/upload-image';
5
+ import type { CreateImageWorkerResponse, Env } from './types';
6
+ import { parsePathSegments } from './utils/path-utils';
7
+
8
+ export async function routeImageWorkerRequest(
9
+ request: Request,
10
+ env: Env,
11
+ createJsonResponse: CreateImageWorkerResponse,
12
+ corsHeaders: Record<string, string>
13
+ ): Promise<Response> {
14
+ const requestUrl = new URL(request.url);
15
+ const pathSegments = parsePathSegments(requestUrl.pathname);
16
+ if (!pathSegments) {
17
+ return createJsonResponse({ error: 'Invalid image path encoding' }, 400);
18
+ }
19
+
20
+ switch (request.method) {
21
+ case 'POST': {
22
+ if (pathSegments.length === 0) {
23
+ return handleImageUpload(request, env, createJsonResponse);
24
+ }
25
+
26
+ if (pathSegments.length === 2 && pathSegments[1] === 'signed-url') {
27
+ return handleSignedUrlMinting(request, env, pathSegments[0], createJsonResponse);
28
+ }
29
+
30
+ return createJsonResponse({ error: 'Not found' }, 404);
31
+ }
32
+
33
+ case 'GET': {
34
+ const fileId = pathSegments.length === 1 ? pathSegments[0] : null;
35
+ if (!fileId) {
36
+ return createJsonResponse({ error: 'Image ID is required' }, 400);
37
+ }
38
+
39
+ return handleImageServing(request, env, fileId, createJsonResponse, corsHeaders);
40
+ }
41
+
42
+ case 'DELETE': {
43
+ if (pathSegments.length !== 1) {
44
+ return createJsonResponse({ error: 'Not found' }, 404);
45
+ }
46
+
47
+ return handleImageDelete(request, env, createJsonResponse);
48
+ }
49
+
50
+ default:
51
+ return createJsonResponse({ error: 'Method not allowed' }, 405);
52
+ }
53
+ }
@@ -0,0 +1,193 @@
1
+ import {
2
+ decryptBinaryFromStorage,
3
+ type DataAtRestEnvelope
4
+ } from '../encryption-utils';
5
+ import type {
6
+ DecryptionTelemetryOutcome,
7
+ Env,
8
+ KeyRegistryPayload,
9
+ PrivateKeyRegistry
10
+ } from '../types';
11
+
12
+ function normalizePrivateKeyPem(rawValue: string): string {
13
+ return rawValue.trim().replace(/^['"]|['"]$/g, '').replace(/\\n/g, '\n');
14
+ }
15
+
16
+ function getNonEmptyString(value: unknown): string | null {
17
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
18
+ }
19
+
20
+ export function requireEncryptionUploadConfig(env: Env): void {
21
+ if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
22
+ throw new Error('Data-at-rest encryption is not configured for image uploads');
23
+ }
24
+ }
25
+
26
+ export function requireEncryptionRetrievalConfig(env: Env): void {
27
+ const hasLegacyPrivateKey = typeof env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY === 'string' && env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY.trim().length > 0;
28
+ const hasRegistry = typeof env.DATA_AT_REST_ENCRYPTION_KEYS_JSON === 'string' && env.DATA_AT_REST_ENCRYPTION_KEYS_JSON.trim().length > 0;
29
+
30
+ if (!hasLegacyPrivateKey && !hasRegistry) {
31
+ throw new Error('Data-at-rest decryption registry is not configured for image retrieval');
32
+ }
33
+ }
34
+
35
+ function parseDataAtRestPrivateKeyRegistry(env: Env): PrivateKeyRegistry {
36
+ const keys: Record<string, string> = {};
37
+ const configuredActiveKeyId = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID);
38
+ const registryJson = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_KEYS_JSON);
39
+
40
+ if (registryJson) {
41
+ let parsedRegistry: unknown;
42
+ try {
43
+ parsedRegistry = JSON.parse(registryJson) as unknown;
44
+ } catch {
45
+ throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON is not valid JSON');
46
+ }
47
+
48
+ if (!parsedRegistry || typeof parsedRegistry !== 'object') {
49
+ throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON must be an object');
50
+ }
51
+
52
+ const payload = parsedRegistry as KeyRegistryPayload;
53
+ if (!payload.keys || typeof payload.keys !== 'object') {
54
+ throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON must include a keys object');
55
+ }
56
+
57
+ for (const [keyId, pemValue] of Object.entries(payload.keys as Record<string, unknown>)) {
58
+ const normalizedKeyId = getNonEmptyString(keyId);
59
+ const normalizedPem = getNonEmptyString(pemValue);
60
+ if (!normalizedKeyId || !normalizedPem) {
61
+ continue;
62
+ }
63
+
64
+ keys[normalizedKeyId] = normalizePrivateKeyPem(normalizedPem);
65
+ }
66
+
67
+ const payloadActiveKeyId = getNonEmptyString(payload.activeKeyId);
68
+ const resolvedActiveKeyId = configuredActiveKeyId ?? payloadActiveKeyId;
69
+
70
+ if (Object.keys(keys).length === 0) {
71
+ throw new Error('DATA_AT_REST_ENCRYPTION_KEYS_JSON does not contain any usable keys');
72
+ }
73
+
74
+ if (resolvedActiveKeyId && !keys[resolvedActiveKeyId]) {
75
+ throw new Error('DATA_AT_REST active key ID is not present in DATA_AT_REST_ENCRYPTION_KEYS_JSON');
76
+ }
77
+
78
+ return {
79
+ activeKeyId: resolvedActiveKeyId ?? null,
80
+ keys
81
+ };
82
+ }
83
+
84
+ const legacyKeyId = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_KEY_ID);
85
+ const legacyPrivateKey = getNonEmptyString(env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY);
86
+ if (!legacyKeyId || !legacyPrivateKey) {
87
+ throw new Error('Data-at-rest decryption key registry is not configured');
88
+ }
89
+
90
+ keys[legacyKeyId] = normalizePrivateKeyPem(legacyPrivateKey);
91
+
92
+ return {
93
+ activeKeyId: configuredActiveKeyId ?? legacyKeyId,
94
+ keys
95
+ };
96
+ }
97
+
98
+ function buildPrivateKeyCandidates(
99
+ recordKeyId: string,
100
+ registry: PrivateKeyRegistry
101
+ ): Array<{ keyId: string; privateKeyPem: string }> {
102
+ const candidates: Array<{ keyId: string; privateKeyPem: string }> = [];
103
+ const seen = new Set<string>();
104
+
105
+ const appendCandidate = (candidateKeyId: string | null): void => {
106
+ if (!candidateKeyId || seen.has(candidateKeyId)) {
107
+ return;
108
+ }
109
+
110
+ const privateKeyPem = registry.keys[candidateKeyId];
111
+ if (!privateKeyPem) {
112
+ return;
113
+ }
114
+
115
+ seen.add(candidateKeyId);
116
+ candidates.push({ keyId: candidateKeyId, privateKeyPem });
117
+ };
118
+
119
+ appendCandidate(getNonEmptyString(recordKeyId));
120
+ appendCandidate(registry.activeKeyId);
121
+
122
+ for (const keyId of Object.keys(registry.keys)) {
123
+ appendCandidate(keyId);
124
+ }
125
+
126
+ return candidates;
127
+ }
128
+
129
+ function logFileDecryptionTelemetry(input: {
130
+ recordKeyId: string;
131
+ selectedKeyId: string | null;
132
+ attemptCount: number;
133
+ outcome: DecryptionTelemetryOutcome;
134
+ reason?: string;
135
+ }): void {
136
+ const details = {
137
+ scope: 'file-at-rest',
138
+ recordKeyId: input.recordKeyId,
139
+ selectedKeyId: input.selectedKeyId,
140
+ attemptCount: input.attemptCount,
141
+ fallbackUsed: input.outcome === 'fallback-hit',
142
+ outcome: input.outcome,
143
+ reason: input.reason ?? null
144
+ };
145
+
146
+ if (input.outcome === 'all-failed') {
147
+ console.warn('Key registry decryption failed', details);
148
+ return;
149
+ }
150
+
151
+ console.info('Key registry decryption resolved', details);
152
+ }
153
+
154
+ export async function decryptBinaryWithRegistry(
155
+ ciphertext: ArrayBuffer,
156
+ envelope: DataAtRestEnvelope,
157
+ env: Env
158
+ ): Promise<ArrayBuffer> {
159
+ const keyRegistry = parseDataAtRestPrivateKeyRegistry(env);
160
+ const candidates = buildPrivateKeyCandidates(envelope.keyId, keyRegistry);
161
+ const primaryKeyId = candidates[0]?.keyId ?? null;
162
+ let lastError: unknown;
163
+
164
+ for (let index = 0; index < candidates.length; index += 1) {
165
+ const candidate = candidates[index];
166
+ try {
167
+ const plaintext = await decryptBinaryFromStorage(ciphertext, envelope, candidate.privateKeyPem);
168
+ logFileDecryptionTelemetry({
169
+ recordKeyId: envelope.keyId,
170
+ selectedKeyId: candidate.keyId,
171
+ attemptCount: index + 1,
172
+ outcome: candidate.keyId === primaryKeyId ? 'primary-hit' : 'fallback-hit'
173
+ });
174
+ return plaintext;
175
+ } catch (error) {
176
+ lastError = error;
177
+ }
178
+ }
179
+
180
+ logFileDecryptionTelemetry({
181
+ recordKeyId: envelope.keyId,
182
+ selectedKeyId: null,
183
+ attemptCount: candidates.length,
184
+ outcome: 'all-failed',
185
+ reason: lastError instanceof Error ? lastError.message : 'unknown decryption error'
186
+ });
187
+
188
+ throw new Error(
189
+ `Failed to decrypt stored file after ${candidates.length} key attempt(s): ${
190
+ lastError instanceof Error ? lastError.message : 'unknown decryption error'
191
+ }`
192
+ );
193
+ }
@@ -0,0 +1,163 @@
1
+ import type { Env, SignedAccessPayload } from '../types';
2
+
3
+ const DEFAULT_SIGNED_URL_TTL_SECONDS = 3600;
4
+ const MAX_SIGNED_URL_TTL_SECONDS = 86400;
5
+
6
+ function base64UrlEncode(input: ArrayBuffer | Uint8Array): string {
7
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
8
+ let binary = '';
9
+
10
+ for (let index = 0; index < bytes.length; index += 1) {
11
+ binary += String.fromCharCode(bytes[index]);
12
+ }
13
+
14
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
15
+ }
16
+
17
+ function base64UrlDecode(input: string): Uint8Array | null {
18
+ if (!input || /[^A-Za-z0-9_-]/.test(input)) {
19
+ return null;
20
+ }
21
+
22
+ const paddingLength = (4 - (input.length % 4)) % 4;
23
+ const padded = input.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(paddingLength);
24
+
25
+ try {
26
+ const binary = atob(padded);
27
+ const bytes = new Uint8Array(binary.length);
28
+ for (let index = 0; index < binary.length; index += 1) {
29
+ bytes[index] = binary.charCodeAt(index);
30
+ }
31
+
32
+ return bytes;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export function normalizeSignedUrlTtlSeconds(requestedTtlSeconds: unknown, env: Env): number {
39
+ const defaultFromEnv = Number.parseInt(env.IMAGE_SIGNED_URL_TTL_SECONDS ?? '', 10);
40
+ const fallbackTtl = Number.isFinite(defaultFromEnv) && defaultFromEnv > 0
41
+ ? defaultFromEnv
42
+ : DEFAULT_SIGNED_URL_TTL_SECONDS;
43
+ const requested = typeof requestedTtlSeconds === 'number' ? requestedTtlSeconds : fallbackTtl;
44
+ const normalized = Math.floor(requested);
45
+
46
+ if (!Number.isFinite(normalized) || normalized <= 0) {
47
+ return fallbackTtl;
48
+ }
49
+
50
+ return Math.min(normalized, MAX_SIGNED_URL_TTL_SECONDS);
51
+ }
52
+
53
+ export function requireSignedUrlConfig(env: Env): void {
54
+ const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || env.IMAGES_API_TOKEN || '').trim();
55
+ if (resolvedSecret.length === 0) {
56
+ throw new Error('Signed URL configuration is missing');
57
+ }
58
+ }
59
+
60
+ export function parseSignedUrlBaseUrl(raw: string): string {
61
+ let parsed: URL;
62
+ try {
63
+ parsed = new URL(raw.trim());
64
+ } catch {
65
+ throw new Error(`IMAGE_SIGNED_URL_BASE_URL is not a valid absolute URL: "${raw}"`);
66
+ }
67
+
68
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
69
+ throw new Error(`IMAGE_SIGNED_URL_BASE_URL must use http or https, got: "${parsed.protocol}"`);
70
+ }
71
+
72
+ if (parsed.search || parsed.hash) {
73
+ throw new Error(`IMAGE_SIGNED_URL_BASE_URL must not include a query string or fragment: "${raw}"`);
74
+ }
75
+
76
+ return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/g, '');
77
+ }
78
+
79
+ async function getSignedUrlHmacKey(env: Env): Promise<CryptoKey> {
80
+ const resolvedSecret = (env.IMAGE_SIGNED_URL_SECRET || env.IMAGES_API_TOKEN || '').trim();
81
+ const keyBytes = new TextEncoder().encode(resolvedSecret);
82
+
83
+ return crypto.subtle.importKey(
84
+ 'raw',
85
+ keyBytes,
86
+ { name: 'HMAC', hash: 'SHA-256' },
87
+ false,
88
+ ['sign', 'verify']
89
+ );
90
+ }
91
+
92
+ export async function signSignedAccessPayload(
93
+ payload: SignedAccessPayload,
94
+ env: Env
95
+ ): Promise<string> {
96
+ const payloadJson = JSON.stringify(payload);
97
+ const payloadBase64Url = base64UrlEncode(new TextEncoder().encode(payloadJson));
98
+ const hmacKey = await getSignedUrlHmacKey(env);
99
+ const signature = await crypto.subtle.sign('HMAC', hmacKey, new TextEncoder().encode(payloadBase64Url));
100
+ const signatureBase64Url = base64UrlEncode(signature);
101
+
102
+ return `${payloadBase64Url}.${signatureBase64Url}`;
103
+ }
104
+
105
+ export async function verifySignedAccessToken(
106
+ token: string,
107
+ fileId: string,
108
+ env: Env
109
+ ): Promise<boolean> {
110
+ const tokenParts = token.split('.');
111
+ if (tokenParts.length !== 2) {
112
+ return false;
113
+ }
114
+
115
+ const [payloadBase64Url, signatureBase64Url] = tokenParts;
116
+ if (!payloadBase64Url || !signatureBase64Url) {
117
+ return false;
118
+ }
119
+
120
+ const signatureBytes = base64UrlDecode(signatureBase64Url);
121
+ if (!signatureBytes) {
122
+ return false;
123
+ }
124
+
125
+ const signatureBuffer = new Uint8Array(signatureBytes).buffer;
126
+ const hmacKey = await getSignedUrlHmacKey(env);
127
+ const signatureValid = await crypto.subtle.verify(
128
+ 'HMAC',
129
+ hmacKey,
130
+ signatureBuffer,
131
+ new TextEncoder().encode(payloadBase64Url)
132
+ );
133
+ if (!signatureValid) {
134
+ return false;
135
+ }
136
+
137
+ const payloadBytes = base64UrlDecode(payloadBase64Url);
138
+ if (!payloadBytes) {
139
+ return false;
140
+ }
141
+
142
+ let payload: SignedAccessPayload;
143
+ try {
144
+ payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as SignedAccessPayload;
145
+ } catch {
146
+ return false;
147
+ }
148
+
149
+ if (payload.fileId !== fileId) {
150
+ return false;
151
+ }
152
+
153
+ const nowEpochSeconds = Math.floor(Date.now() / 1000);
154
+ if (!Number.isInteger(payload.exp) || payload.exp <= nowEpochSeconds) {
155
+ return false;
156
+ }
157
+
158
+ if (!Number.isInteger(payload.iat) || payload.iat > nowEpochSeconds + 300) {
159
+ return false;
160
+ }
161
+
162
+ return typeof payload.nonce === 'string' && payload.nonce.length > 0;
163
+ }
@@ -0,0 +1,68 @@
1
+ export interface Env {
2
+ IMAGES_API_TOKEN: string;
3
+ STRIAE_FILES: R2Bucket;
4
+ DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
5
+ DATA_AT_REST_ENCRYPTION_PUBLIC_KEY: string;
6
+ DATA_AT_REST_ENCRYPTION_KEY_ID: string;
7
+ DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
8
+ DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID?: string;
9
+ IMAGE_SIGNED_URL_SECRET?: string;
10
+ IMAGE_SIGNED_URL_TTL_SECONDS?: string;
11
+ IMAGE_SIGNED_URL_BASE_URL?: string;
12
+ }
13
+
14
+ export interface KeyRegistryPayload {
15
+ activeKeyId?: unknown;
16
+ keys?: unknown;
17
+ }
18
+
19
+ export interface PrivateKeyRegistry {
20
+ activeKeyId: string | null;
21
+ keys: Record<string, string>;
22
+ }
23
+
24
+ export type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
25
+
26
+ export interface UploadResult {
27
+ id: string;
28
+ filename: string;
29
+ uploaded: string;
30
+ }
31
+
32
+ export interface UploadResponse {
33
+ success: boolean;
34
+ errors: Array<{ code: number; message: string }>;
35
+ messages: string[];
36
+ result: UploadResult;
37
+ }
38
+
39
+ export interface SuccessResponse {
40
+ success: boolean;
41
+ }
42
+
43
+ export interface ErrorResponse {
44
+ error: string;
45
+ }
46
+
47
+ export interface SignedUrlResult {
48
+ fileId: string;
49
+ url: string;
50
+ expiresAt: string;
51
+ expiresInSeconds: number;
52
+ }
53
+
54
+ export interface SignedUrlResponse {
55
+ success: boolean;
56
+ result: SignedUrlResult;
57
+ }
58
+
59
+ export type APIResponse = UploadResponse | SuccessResponse | ErrorResponse | SignedUrlResponse;
60
+
61
+ export interface SignedAccessPayload {
62
+ fileId: string;
63
+ iat: number;
64
+ exp: number;
65
+ nonce: string;
66
+ }
67
+
68
+ export type CreateImageWorkerResponse = (data: APIResponse, status?: number) => Response;
@@ -0,0 +1,33 @@
1
+ export function deriveFileKind(contentType: string): string {
2
+ if (contentType.startsWith('image/')) {
3
+ return 'image';
4
+ }
5
+
6
+ return 'file';
7
+ }
8
+
9
+ export function buildSafeContentDisposition(filename: string, fallbackFileId: string): string {
10
+ const normalizedFilename = filename
11
+ .normalize('NFKC')
12
+ .replace(/[\r\n]+/g, ' ')
13
+ .split('')
14
+ .filter((character) => {
15
+ const codePoint = character.charCodeAt(0);
16
+ return codePoint >= 0x20 && codePoint !== 0x7f;
17
+ })
18
+ .join('')
19
+ .trim();
20
+
21
+ const safeFilename = normalizedFilename.length > 0 ? normalizedFilename : fallbackFileId;
22
+ const asciiFallback = safeFilename
23
+ .replace(/[^\x20-\x7E]/g, '_')
24
+ .replace(/["\\]/g, '_')
25
+ .trim();
26
+ const asciiFilename = asciiFallback.length > 0 ? asciiFallback : fallbackFileId;
27
+ const encodedUtf8Filename = encodeURIComponent(safeFilename).replace(
28
+ /['()*]/g,
29
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`
30
+ );
31
+
32
+ return `inline; filename="${asciiFilename}"; filename*=UTF-8''${encodedUtf8Filename}`;
33
+ }
@@ -0,0 +1,50 @@
1
+ export function parseFileId(pathname: string): string | null {
2
+ const encodedFileId = pathname.startsWith('/') ? pathname.slice(1) : pathname;
3
+ if (!encodedFileId) {
4
+ return null;
5
+ }
6
+
7
+ let decodedFileId = '';
8
+ try {
9
+ decodedFileId = decodeURIComponent(encodedFileId);
10
+ } catch {
11
+ return null;
12
+ }
13
+
14
+ if (!decodedFileId || decodedFileId.includes('/')) {
15
+ return null;
16
+ }
17
+
18
+ return decodedFileId;
19
+ }
20
+
21
+ export function parsePathSegments(pathname: string): string[] | null {
22
+ const normalized = pathname.startsWith('/') ? pathname.slice(1) : pathname;
23
+ if (!normalized) {
24
+ return [];
25
+ }
26
+
27
+ const rawSegments = normalized.split('/');
28
+ const decodedSegments: string[] = [];
29
+
30
+ for (const segment of rawSegments) {
31
+ if (!segment) {
32
+ return null;
33
+ }
34
+
35
+ let decoded = '';
36
+ try {
37
+ decoded = decodeURIComponent(segment);
38
+ } catch {
39
+ return null;
40
+ }
41
+
42
+ if (!decoded || decoded.includes('/')) {
43
+ return null;
44
+ }
45
+
46
+ decodedSegments.push(decoded);
47
+ }
48
+
49
+ return decodedSegments;
50
+ }
@@ -0,0 +1,27 @@
1
+ import type { DataAtRestEnvelope } from '../encryption-utils';
2
+
3
+ export function extractEnvelope(file: R2ObjectBody): DataAtRestEnvelope | null {
4
+ const metadata = file.customMetadata;
5
+ if (!metadata) {
6
+ return null;
7
+ }
8
+
9
+ const { algorithm, encryptionVersion, keyId, dataIv, wrappedKey } = metadata;
10
+ if (
11
+ typeof algorithm !== 'string' ||
12
+ typeof encryptionVersion !== 'string' ||
13
+ typeof keyId !== 'string' ||
14
+ typeof dataIv !== 'string' ||
15
+ typeof wrappedKey !== 'string'
16
+ ) {
17
+ return null;
18
+ }
19
+
20
+ return {
21
+ algorithm,
22
+ encryptionVersion,
23
+ keyId,
24
+ dataIv,
25
+ wrappedKey
26
+ };
27
+ }
@@ -2,7 +2,7 @@
2
2
  "name": "IMAGES_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/image-worker.ts",
5
- "compatibility_date": "2026-04-02",
5
+ "compatibility_date": "2026-04-03",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-04-02",
5
+ "compatibility_date": "2026-04-03",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],