@striae-org/striae 4.3.4 → 5.1.0
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/.env.example +9 -2
- package/app/components/actions/case-export/download-handlers.ts +66 -11
- package/app/components/actions/case-import/confirmation-import.ts +50 -7
- package/app/components/actions/case-import/confirmation-package.ts +99 -22
- package/app/components/actions/case-import/orchestrator.ts +116 -13
- package/app/components/actions/case-import/validation.ts +171 -7
- package/app/components/actions/case-import/zip-processing.ts +224 -127
- package/app/components/actions/case-manage.ts +74 -15
- package/app/components/actions/confirm-export.ts +32 -3
- package/app/components/actions/generate-pdf.ts +43 -1
- package/app/components/actions/image-manage.ts +13 -45
- package/app/components/navbar/navbar.module.css +0 -10
- package/app/components/navbar/navbar.tsx +0 -22
- package/app/components/sidebar/case-import/case-import.module.css +7 -131
- package/app/components/sidebar/case-import/case-import.tsx +7 -14
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
- package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
- package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
- package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
- package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
- package/app/config-example/config.json +5 -0
- package/app/routes/auth/login.tsx +1 -1
- package/app/routes/striae/hooks/use-striae-reset-helpers.ts +4 -0
- package/app/routes/striae/striae.tsx +15 -4
- package/app/utils/data/operations/case-operations.ts +13 -1
- package/app/utils/data/operations/confirmation-summary-operations.ts +38 -1
- package/app/utils/data/operations/file-annotation-operations.ts +13 -1
- package/app/utils/data/operations/signing-operations.ts +93 -0
- package/app/utils/data/operations/types.ts +6 -0
- package/app/utils/forensics/export-encryption.ts +316 -0
- package/app/utils/forensics/export-verification.ts +1 -409
- package/app/utils/forensics/index.ts +1 -0
- package/app/utils/ui/case-messages.ts +5 -2
- package/package.json +2 -2
- package/scripts/deploy-config.sh +244 -7
- package/scripts/deploy-pages-secrets.sh +0 -6
- package/scripts/deploy-worker-secrets.sh +66 -5
- package/scripts/encrypt-r2-backfill.mjs +376 -0
- package/worker-configuration.d.ts +13 -7
- package/workers/audit-worker/package.json +1 -4
- package/workers/audit-worker/src/audit-worker.example.ts +522 -61
- package/workers/audit-worker/wrangler.jsonc.example +6 -1
- package/workers/data-worker/package.json +1 -4
- package/workers/data-worker/src/data-worker.example.ts +409 -1
- package/workers/data-worker/src/encryption-utils.ts +269 -0
- package/workers/data-worker/worker-configuration.d.ts +1 -1
- package/workers/data-worker/wrangler.jsonc.example +6 -2
- package/workers/image-worker/package.json +1 -4
- package/workers/image-worker/src/encryption-utils.ts +217 -0
- package/workers/image-worker/src/image-worker.example.ts +196 -127
- package/workers/image-worker/wrangler.jsonc.example +8 -1
- package/workers/keys-worker/package.json +1 -4
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -4
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -4
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
|
@@ -1,179 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
decryptBinaryFromStorage,
|
|
3
|
+
encryptBinaryForStorage,
|
|
4
|
+
type DataAtRestEnvelope
|
|
5
|
+
} from './encryption-utils';
|
|
6
|
+
|
|
1
7
|
interface Env {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
8
|
+
IMAGES_API_TOKEN: string;
|
|
9
|
+
STRIAE_FILES: R2Bucket;
|
|
10
|
+
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY: string;
|
|
11
|
+
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY: string;
|
|
12
|
+
DATA_AT_REST_ENCRYPTION_KEY_ID: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface UploadResult {
|
|
16
|
+
id: string;
|
|
17
|
+
filename: string;
|
|
18
|
+
uploaded: string;
|
|
19
|
+
requireSignedURLs: boolean;
|
|
20
|
+
variants: string[];
|
|
5
21
|
}
|
|
6
22
|
|
|
7
|
-
interface
|
|
23
|
+
interface UploadResponse {
|
|
24
|
+
success: boolean;
|
|
25
|
+
errors: Array<{ code: number; message: string }>;
|
|
26
|
+
messages: string[];
|
|
27
|
+
result: UploadResult;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface SuccessResponse {
|
|
8
31
|
success: boolean;
|
|
9
|
-
errors?: Array<{
|
|
10
|
-
code: number;
|
|
11
|
-
message: string;
|
|
12
|
-
}>;
|
|
13
|
-
messages?: string[];
|
|
14
|
-
result?: {
|
|
15
|
-
id: string;
|
|
16
|
-
filename: string;
|
|
17
|
-
uploaded: string;
|
|
18
|
-
requireSignedURLs: boolean;
|
|
19
|
-
variants: string[];
|
|
20
|
-
};
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
interface ErrorResponse {
|
|
24
35
|
error: string;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
type APIResponse =
|
|
38
|
+
type APIResponse = UploadResponse | SuccessResponse | ErrorResponse;
|
|
28
39
|
|
|
29
|
-
const API_BASE = "https://api.cloudflare.com/client/v4/accounts";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* CORS headers to allow requests from the Striae app
|
|
33
|
-
*/
|
|
34
40
|
const corsHeaders: Record<string, string> = {
|
|
35
41
|
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
36
42
|
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
|
|
37
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Auth-Key'
|
|
38
|
-
'Content-Type': 'application/json'
|
|
43
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Auth-Key'
|
|
39
44
|
};
|
|
40
45
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
{
|
|
46
|
+
const createJsonResponse = (data: APIResponse, status: number = 200): Response => new Response(
|
|
47
|
+
JSON.stringify(data),
|
|
48
|
+
{
|
|
49
|
+
status,
|
|
50
|
+
headers: {
|
|
51
|
+
...corsHeaders,
|
|
52
|
+
'Content-Type': 'application/json'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
44
55
|
);
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
const authHeader = request.headers.get(
|
|
48
|
-
const expectedToken = `Bearer ${env.
|
|
57
|
+
function hasValidToken(request: Request, env: Env): boolean {
|
|
58
|
+
const authHeader = request.headers.get('Authorization');
|
|
59
|
+
const expectedToken = `Bearer ${env.IMAGES_API_TOKEN}`;
|
|
49
60
|
return authHeader === expectedToken;
|
|
50
|
-
}
|
|
61
|
+
}
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
async function handleImageUpload(request: Request, env: Env): Promise<Response> {
|
|
56
|
-
if (!hasValidToken(request, env)) {
|
|
57
|
-
return createResponse({ error: 'Unauthorized' }, 403);
|
|
63
|
+
function requireEncryptionUploadConfig(env: Env): void {
|
|
64
|
+
if (!env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY || !env.DATA_AT_REST_ENCRYPTION_KEY_ID) {
|
|
65
|
+
throw new Error('Data-at-rest encryption is not configured for image uploads');
|
|
58
66
|
}
|
|
67
|
+
}
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
function requireEncryptionRetrievalConfig(env: Env): void {
|
|
70
|
+
if (!env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY) {
|
|
71
|
+
throw new Error('Data-at-rest decryption is not configured for image retrieval');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
function parseFileId(pathname: string): string | null {
|
|
76
|
+
const encodedFileId = pathname.startsWith('/') ? pathname.slice(1) : pathname;
|
|
77
|
+
if (!encodedFileId) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
let decodedFileId = '';
|
|
82
|
+
try {
|
|
83
|
+
decodedFileId = decodeURIComponent(encodedFileId);
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!decodedFileId || decodedFileId.includes('/')) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return decodedFileId;
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!hasValidToken(request, env)) {
|
|
83
|
-
return createResponse({ error: 'Unauthorized' }, 403);
|
|
95
|
+
function extractEnvelope(file: R2ObjectBody): DataAtRestEnvelope | null {
|
|
96
|
+
const metadata = file.customMetadata;
|
|
97
|
+
if (!metadata) {
|
|
98
|
+
return null;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
const { algorithm, encryptionVersion, keyId, dataIv, wrappedKey } = metadata;
|
|
102
|
+
if (
|
|
103
|
+
typeof algorithm !== 'string' ||
|
|
104
|
+
typeof encryptionVersion !== 'string' ||
|
|
105
|
+
typeof keyId !== 'string' ||
|
|
106
|
+
typeof dataIv !== 'string' ||
|
|
107
|
+
typeof wrappedKey !== 'string'
|
|
108
|
+
) {
|
|
109
|
+
return null;
|
|
91
110
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const data: CloudflareImagesResponse = await response.json();
|
|
102
|
-
return createResponse(data, response.status);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
algorithm,
|
|
114
|
+
encryptionVersion,
|
|
115
|
+
keyId,
|
|
116
|
+
dataIv,
|
|
117
|
+
wrappedKey
|
|
118
|
+
};
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
async function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
121
|
+
function deriveFileKind(contentType: string): string {
|
|
122
|
+
if (contentType.startsWith('image/')) {
|
|
123
|
+
return 'image';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 'file';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function handleImageUpload(request: Request, env: Env): Promise<Response> {
|
|
130
|
+
if (!hasValidToken(request, env)) {
|
|
131
|
+
return createJsonResponse({ error: 'Unauthorized' }, 403);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
requireEncryptionUploadConfig(env);
|
|
135
|
+
|
|
136
|
+
const formData = await request.formData();
|
|
137
|
+
const fileValue = formData.get('file');
|
|
138
|
+
if (!(fileValue instanceof Blob)) {
|
|
139
|
+
return createJsonResponse({ error: 'Missing file upload payload' }, 400);
|
|
140
|
+
}
|
|
123
141
|
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
142
|
+
const fileBlob = fileValue;
|
|
143
|
+
const uploadedAt = new Date().toISOString();
|
|
144
|
+
const filename = fileValue instanceof File && fileValue.name ? fileValue.name : 'upload.bin';
|
|
145
|
+
const contentType = fileBlob.type || 'application/octet-stream';
|
|
146
|
+
const fileId = crypto.randomUUID().replace(/-/g, '');
|
|
147
|
+
const plaintextBytes = await fileBlob.arrayBuffer();
|
|
127
148
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
const encryptedPayload = await encryptBinaryForStorage(
|
|
150
|
+
plaintextBytes,
|
|
151
|
+
env.DATA_AT_REST_ENCRYPTION_PUBLIC_KEY,
|
|
152
|
+
env.DATA_AT_REST_ENCRYPTION_KEY_ID
|
|
153
|
+
);
|
|
131
154
|
|
|
132
|
-
|
|
133
|
-
|
|
155
|
+
await env.STRIAE_FILES.put(fileId, encryptedPayload.ciphertext, {
|
|
156
|
+
customMetadata: {
|
|
157
|
+
algorithm: encryptedPayload.envelope.algorithm,
|
|
158
|
+
encryptionVersion: encryptedPayload.envelope.encryptionVersion,
|
|
159
|
+
keyId: encryptedPayload.envelope.keyId,
|
|
160
|
+
dataIv: encryptedPayload.envelope.dataIv,
|
|
161
|
+
wrappedKey: encryptedPayload.envelope.wrappedKey,
|
|
162
|
+
contentType,
|
|
163
|
+
originalFilename: filename,
|
|
164
|
+
byteLength: String(fileBlob.size),
|
|
165
|
+
createdAt: uploadedAt,
|
|
166
|
+
fileKind: deriveFileKind(contentType)
|
|
167
|
+
}
|
|
168
|
+
});
|
|
134
169
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
170
|
+
return createJsonResponse({
|
|
171
|
+
success: true,
|
|
172
|
+
errors: [],
|
|
173
|
+
messages: [],
|
|
174
|
+
result: {
|
|
175
|
+
id: fileId,
|
|
176
|
+
filename,
|
|
177
|
+
uploaded: uploadedAt,
|
|
178
|
+
requireSignedURLs: false,
|
|
179
|
+
variants: []
|
|
180
|
+
}
|
|
138
181
|
});
|
|
139
182
|
}
|
|
140
183
|
|
|
141
|
-
async function
|
|
184
|
+
async function handleImageDelete(request: Request, env: Env): Promise<Response> {
|
|
142
185
|
if (!hasValidToken(request, env)) {
|
|
143
|
-
return
|
|
186
|
+
return createJsonResponse({ error: 'Unauthorized' }, 403);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const fileId = parseFileId(new URL(request.url).pathname);
|
|
190
|
+
if (!fileId) {
|
|
191
|
+
return createJsonResponse({ error: 'Image ID is required' }, 400);
|
|
144
192
|
}
|
|
145
193
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return createResponse({ error: 'Image delivery URL is required' }, 400);
|
|
194
|
+
const existing = await env.STRIAE_FILES.head(fileId);
|
|
195
|
+
if (!existing) {
|
|
196
|
+
return createJsonResponse({ error: 'File not found' }, 404);
|
|
150
197
|
}
|
|
151
198
|
|
|
152
|
-
|
|
199
|
+
await env.STRIAE_FILES.delete(fileId);
|
|
200
|
+
return createJsonResponse({ success: true });
|
|
201
|
+
}
|
|
153
202
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return createResponse({ error: 'Image delivery URL must be URL-encoded' }, 400);
|
|
203
|
+
async function handleImageServing(request: Request, env: Env): Promise<Response> {
|
|
204
|
+
if (!hasValidToken(request, env)) {
|
|
205
|
+
return createJsonResponse({ error: 'Unauthorized' }, 403);
|
|
158
206
|
}
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
208
|
+
requireEncryptionRetrievalConfig(env);
|
|
209
|
+
|
|
210
|
+
const fileId = parseFileId(new URL(request.url).pathname);
|
|
211
|
+
if (!fileId) {
|
|
212
|
+
return createJsonResponse({ error: 'Image ID is required' }, 400);
|
|
165
213
|
}
|
|
166
214
|
|
|
167
|
-
|
|
168
|
-
|
|
215
|
+
const file = await env.STRIAE_FILES.get(fileId);
|
|
216
|
+
if (!file) {
|
|
217
|
+
return createJsonResponse({ error: 'File not found' }, 404);
|
|
169
218
|
}
|
|
170
|
-
|
|
171
|
-
|
|
219
|
+
|
|
220
|
+
const envelope = extractEnvelope(file);
|
|
221
|
+
if (!envelope) {
|
|
222
|
+
return createJsonResponse({ error: 'Missing data-at-rest envelope metadata' }, 500);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const encryptedData = await file.arrayBuffer();
|
|
226
|
+
const plaintext = await decryptBinaryFromStorage(
|
|
227
|
+
encryptedData,
|
|
228
|
+
envelope,
|
|
229
|
+
env.DATA_AT_REST_ENCRYPTION_PRIVATE_KEY
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const contentType = file.customMetadata?.contentType || 'application/octet-stream';
|
|
233
|
+
const filename = file.customMetadata?.originalFilename || fileId;
|
|
234
|
+
|
|
235
|
+
return new Response(plaintext, {
|
|
236
|
+
status: 200,
|
|
237
|
+
headers: {
|
|
238
|
+
...corsHeaders,
|
|
239
|
+
'Cache-Control': 'no-store',
|
|
240
|
+
'Content-Type': contentType,
|
|
241
|
+
'Content-Disposition': `inline; filename="${filename.replace(/"/g, '')}"`
|
|
242
|
+
}
|
|
243
|
+
});
|
|
172
244
|
}
|
|
173
245
|
|
|
174
|
-
/**
|
|
175
|
-
* Main worker functions
|
|
176
|
-
*/
|
|
177
246
|
export default {
|
|
178
247
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
179
248
|
if (request.method === 'OPTIONS') {
|
|
@@ -189,11 +258,11 @@ export default {
|
|
|
189
258
|
case 'DELETE':
|
|
190
259
|
return handleImageDelete(request, env);
|
|
191
260
|
default:
|
|
192
|
-
return
|
|
261
|
+
return createJsonResponse({ error: 'Method not allowed' }, 405);
|
|
193
262
|
}
|
|
194
263
|
} catch (error) {
|
|
195
264
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
196
|
-
return
|
|
265
|
+
return createJsonResponse({ error: errorMessage }, 500);
|
|
197
266
|
}
|
|
198
267
|
}
|
|
199
268
|
};
|
|
@@ -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-03-
|
|
5
|
+
"compatibility_date": "2026-03-24",
|
|
6
6
|
"compatibility_flags": [
|
|
7
7
|
"nodejs_compat"
|
|
8
8
|
],
|
|
@@ -11,5 +11,12 @@
|
|
|
11
11
|
"enabled": true
|
|
12
12
|
},
|
|
13
13
|
|
|
14
|
+
"r2_buckets": [
|
|
15
|
+
{
|
|
16
|
+
"binding": "STRIAE_FILES",
|
|
17
|
+
"bucket_name": "FILES_BUCKET_NAME"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
|
|
14
21
|
"placement": { "mode": "smart" }
|
|
15
22
|
}
|
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
7
7
|
"dev": "wrangler dev",
|
|
8
|
-
"start": "wrangler dev"
|
|
9
|
-
"test": "vitest"
|
|
8
|
+
"start": "wrangler dev"
|
|
10
9
|
},
|
|
11
10
|
"devDependencies": {
|
|
12
11
|
"@cloudflare/puppeteer": "^1.0.6",
|
|
13
|
-
"@cloudflare/vitest-pool-workers": "^0.13.0",
|
|
14
|
-
"vitest": "~4.1.0",
|
|
15
12
|
"wrangler": "^4.76.0"
|
|
16
13
|
},
|
|
17
14
|
"overrides": {
|
|
@@ -6,13 +6,10 @@
|
|
|
6
6
|
"generate:assets": "node scripts/generate-assets.js",
|
|
7
7
|
"deploy": "wrangler deploy",
|
|
8
8
|
"dev": "wrangler dev",
|
|
9
|
-
"start": "wrangler dev"
|
|
10
|
-
"test": "vitest"
|
|
9
|
+
"start": "wrangler dev"
|
|
11
10
|
},
|
|
12
11
|
"devDependencies": {
|
|
13
12
|
"@cloudflare/puppeteer": "^1.0.6",
|
|
14
|
-
"@cloudflare/vitest-pool-workers": "^0.13.0",
|
|
15
|
-
"vitest": "~4.1.0",
|
|
16
13
|
"wrangler": "^4.76.0"
|
|
17
14
|
},
|
|
18
15
|
"overrides": {
|
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
7
7
|
"dev": "wrangler dev",
|
|
8
|
-
"start": "wrangler dev"
|
|
9
|
-
"test": "vitest"
|
|
8
|
+
"start": "wrangler dev"
|
|
10
9
|
},
|
|
11
10
|
"devDependencies": {
|
|
12
11
|
"@cloudflare/puppeteer": "^1.0.6",
|
|
13
|
-
"@cloudflare/vitest-pool-workers": "^0.13.0",
|
|
14
|
-
"vitest": "~4.1.0",
|
|
15
12
|
"wrangler": "^4.76.0"
|
|
16
13
|
},
|
|
17
14
|
"overrides": {
|
package/wrangler.toml.example
CHANGED