@striae-org/striae 5.1.0 → 5.1.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/.env.example +3 -2
- package/app/components/actions/case-export/download-handlers.ts +18 -1
- package/app/components/actions/case-manage.ts +17 -1
- package/app/components/actions/generate-pdf.ts +9 -3
- package/app/components/actions/image-manage.ts +43 -11
- package/app/routes/striae/striae.tsx +1 -0
- package/app/types/file.ts +18 -2
- package/app/utils/api/image-api-client.ts +49 -1
- package/functions/api/image/[[path]].ts +2 -1
- package/package.json +1 -1
- package/scripts/deploy-config.sh +75 -47
- package/scripts/deploy-worker-secrets.sh +2 -2
- package/worker-configuration.d.ts +5 -3
- package/workers/data-worker/wrangler.jsonc.example +1 -3
- package/workers/image-worker/src/image-worker.example.ts +266 -15
- package/workers/image-worker/worker-configuration.d.ts +3 -2
- package/scripts/encrypt-r2-backfill.mjs +0 -376
package/.env.example
CHANGED
|
@@ -47,7 +47,6 @@ PAGES_CUSTOM_DOMAIN=your_custom_domain_here
|
|
|
47
47
|
KEYS_WORKER_NAME=your_keys_worker_name_here
|
|
48
48
|
KEYS_WORKER_DOMAIN=your_keys_worker_domain_here
|
|
49
49
|
KEYS_AUTH=your_custom_keys_auth_token_here
|
|
50
|
-
ACCOUNT_HASH=your_cloudflare_images_account_hash_here
|
|
51
50
|
|
|
52
51
|
# ================================
|
|
53
52
|
# USER WORKER ENVIRONMENT VARIABLES
|
|
@@ -70,7 +69,6 @@ MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
|
|
|
70
69
|
EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
|
|
71
70
|
EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
|
|
72
71
|
EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
|
|
73
|
-
DATA_AT_REST_ENCRYPTION_ENABLED=true
|
|
74
72
|
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
|
|
75
73
|
DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
|
|
76
74
|
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
|
|
@@ -88,6 +86,9 @@ AUDIT_WORKER_DOMAIN=your_audit_worker_domain_here
|
|
|
88
86
|
# ================================
|
|
89
87
|
IMAGES_WORKER_NAME=your_images_worker_name_here
|
|
90
88
|
IMAGES_WORKER_DOMAIN=your_images_worker_domain_here
|
|
89
|
+
IMAGE_SIGNED_URL_SECRET=your_image_signed_url_secret_here
|
|
90
|
+
# Optional: defaults to 3600 and max is 86400.
|
|
91
|
+
IMAGE_SIGNED_URL_TTL_SECONDS=3600
|
|
91
92
|
|
|
92
93
|
# ================================
|
|
93
94
|
# PDF WORKER ENVIRONMENT VARIABLES
|
|
@@ -965,7 +965,24 @@ For questions about this export, contact your Striae system administrator.
|
|
|
965
965
|
*/
|
|
966
966
|
async function fetchImageAsBlob(user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> {
|
|
967
967
|
try {
|
|
968
|
-
const
|
|
968
|
+
const imageAccess = await getImageUrl(user, fileData, caseNumber, 'Export Package');
|
|
969
|
+
const { blob, revoke, url } = imageAccess;
|
|
970
|
+
|
|
971
|
+
if (!blob) {
|
|
972
|
+
const signedResponse = await fetch(url, {
|
|
973
|
+
method: 'GET',
|
|
974
|
+
headers: {
|
|
975
|
+
'Accept': 'application/octet-stream,image/*'
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
if (!signedResponse.ok) {
|
|
980
|
+
throw new Error(`Signed URL fetch failed with status ${signedResponse.status}`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
return await signedResponse.blob();
|
|
984
|
+
}
|
|
985
|
+
|
|
969
986
|
try {
|
|
970
987
|
return blob;
|
|
971
988
|
} finally {
|
|
@@ -685,7 +685,23 @@ const getVerificationPublicSigningKey = (preferredKeyId?: string): { keyId: stri
|
|
|
685
685
|
|
|
686
686
|
const fetchImageAsBlob = async (user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> => {
|
|
687
687
|
try {
|
|
688
|
-
const
|
|
688
|
+
const imageAccess = await getImageUrl(user, fileData, caseNumber, 'Archive Package');
|
|
689
|
+
const { blob, revoke, url } = imageAccess;
|
|
690
|
+
|
|
691
|
+
if (!blob) {
|
|
692
|
+
const signedResponse = await fetch(url, {
|
|
693
|
+
method: 'GET',
|
|
694
|
+
headers: {
|
|
695
|
+
'Accept': 'application/octet-stream,image/*'
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (!signedResponse.ok) {
|
|
700
|
+
throw new Error(`Signed URL fetch failed with status ${signedResponse.status}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return await signedResponse.blob();
|
|
704
|
+
}
|
|
689
705
|
|
|
690
706
|
try {
|
|
691
707
|
return blob;
|
|
@@ -6,6 +6,7 @@ import { fetchPdfApi } from '~/utils/api';
|
|
|
6
6
|
interface GeneratePDFParams {
|
|
7
7
|
user: User;
|
|
8
8
|
selectedImage: string | undefined;
|
|
9
|
+
sourceImageId?: string;
|
|
9
10
|
selectedFilename: string | undefined;
|
|
10
11
|
userCompany: string;
|
|
11
12
|
userFirstName: string;
|
|
@@ -44,6 +45,10 @@ const resolvePdfImageUrl = async (selectedImage: string | undefined): Promise<st
|
|
|
44
45
|
return selectedImage;
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
if (selectedImage.startsWith('/')) {
|
|
49
|
+
return new URL(selectedImage, window.location.origin).toString();
|
|
50
|
+
}
|
|
51
|
+
|
|
47
52
|
if (selectedImage.startsWith('data:')) {
|
|
48
53
|
return selectedImage;
|
|
49
54
|
}
|
|
@@ -64,6 +69,7 @@ const resolvePdfImageUrl = async (selectedImage: string | undefined): Promise<st
|
|
|
64
69
|
export const generatePDF = async ({
|
|
65
70
|
user,
|
|
66
71
|
selectedImage,
|
|
72
|
+
sourceImageId,
|
|
67
73
|
selectedFilename,
|
|
68
74
|
userCompany,
|
|
69
75
|
userFirstName,
|
|
@@ -187,7 +193,7 @@ export const generatePDF = async ({
|
|
|
187
193
|
processingTime,
|
|
188
194
|
blob.size,
|
|
189
195
|
[],
|
|
190
|
-
|
|
196
|
+
sourceImageId, // Source file ID
|
|
191
197
|
selectedFilename // Source original filename
|
|
192
198
|
);
|
|
193
199
|
} catch (auditError) {
|
|
@@ -215,7 +221,7 @@ export const generatePDF = async ({
|
|
|
215
221
|
processingTime,
|
|
216
222
|
0, // No file size for failed generation
|
|
217
223
|
[errorText || 'PDF generation failed'],
|
|
218
|
-
|
|
224
|
+
sourceImageId, // Source file ID
|
|
219
225
|
selectedFilename // Source original filename
|
|
220
226
|
);
|
|
221
227
|
} catch (auditError) {
|
|
@@ -242,7 +248,7 @@ export const generatePDF = async ({
|
|
|
242
248
|
processingTime,
|
|
243
249
|
0, // No file size for failed generation
|
|
244
250
|
[error instanceof Error ? error.message : 'Unknown error generating PDF'],
|
|
245
|
-
|
|
251
|
+
sourceImageId, // Source file ID
|
|
246
252
|
selectedFilename // Source original filename
|
|
247
253
|
);
|
|
248
254
|
} catch (auditError) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import { fetchImageApi, uploadImageApi } from '~/utils/api';
|
|
2
|
+
import { createSignedImageUrlApi, fetchImageApi, uploadImageApi } from '~/utils/api';
|
|
3
3
|
import { canUploadFile, getCaseData, updateCaseData, deleteFileAnnotations } from '~/utils/data';
|
|
4
|
-
import type { CaseData, FileData, ImageUploadResponse } from '~/types';
|
|
4
|
+
import type { CaseData, FileData, ImageAccessResult, ImageUploadResponse } from '~/types';
|
|
5
5
|
import { auditService } from '~/services/audit';
|
|
6
6
|
|
|
7
7
|
export interface DeleteFileResult {
|
|
@@ -255,20 +255,49 @@ export const deleteFile = async (
|
|
|
255
255
|
}
|
|
256
256
|
};
|
|
257
257
|
|
|
258
|
-
export const getImageUrl = async (
|
|
258
|
+
export const getImageUrl = async (
|
|
259
|
+
user: User,
|
|
260
|
+
fileData: FileData,
|
|
261
|
+
caseNumber: string,
|
|
262
|
+
accessReason?: string
|
|
263
|
+
): Promise<ImageAccessResult> => {
|
|
259
264
|
const startTime = Date.now();
|
|
260
265
|
const defaultAccessReason = accessReason || 'Image viewer access';
|
|
261
|
-
|
|
266
|
+
|
|
262
267
|
try {
|
|
268
|
+
try {
|
|
269
|
+
const signedUrlResponse = await createSignedImageUrlApi(user, fileData.id);
|
|
270
|
+
|
|
271
|
+
await auditService.logFileAccess(
|
|
272
|
+
user,
|
|
273
|
+
fileData.originalFilename || fileData.id,
|
|
274
|
+
fileData.id,
|
|
275
|
+
'signed-url',
|
|
276
|
+
caseNumber,
|
|
277
|
+
'success',
|
|
278
|
+
Date.now() - startTime,
|
|
279
|
+
defaultAccessReason,
|
|
280
|
+
fileData.originalFilename
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
url: signedUrlResponse.result.url,
|
|
285
|
+
revoke: () => {},
|
|
286
|
+
urlType: 'signed',
|
|
287
|
+
expiresAt: signedUrlResponse.result.expiresAt
|
|
288
|
+
};
|
|
289
|
+
} catch {
|
|
290
|
+
// Fallback to direct blob retrieval during migration.
|
|
291
|
+
}
|
|
292
|
+
|
|
263
293
|
const workerResponse = await fetchImageApi(user, `/${encodeURIComponent(fileData.id)}`, {
|
|
264
294
|
method: 'GET',
|
|
265
295
|
headers: {
|
|
266
296
|
'Accept': 'application/octet-stream,image/*'
|
|
267
297
|
}
|
|
268
298
|
});
|
|
269
|
-
|
|
299
|
+
|
|
270
300
|
if (!workerResponse.ok) {
|
|
271
|
-
// Log failed image access
|
|
272
301
|
await auditService.logFileAccess(
|
|
273
302
|
user,
|
|
274
303
|
fileData.originalFilename || fileData.id,
|
|
@@ -285,8 +314,7 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
|
|
|
285
314
|
|
|
286
315
|
const blob = await workerResponse.blob();
|
|
287
316
|
const objectUrl = URL.createObjectURL(blob);
|
|
288
|
-
|
|
289
|
-
// Log successful image access
|
|
317
|
+
|
|
290
318
|
await auditService.logFileAccess(
|
|
291
319
|
user,
|
|
292
320
|
fileData.originalFilename || fileData.id,
|
|
@@ -298,10 +326,14 @@ export const getImageUrl = async (user: User, fileData: FileData, caseNumber: st
|
|
|
298
326
|
defaultAccessReason,
|
|
299
327
|
fileData.originalFilename
|
|
300
328
|
);
|
|
301
|
-
|
|
302
|
-
return {
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
blob,
|
|
332
|
+
url: objectUrl,
|
|
333
|
+
revoke: () => URL.revokeObjectURL(objectUrl),
|
|
334
|
+
urlType: 'blob'
|
|
335
|
+
};
|
|
303
336
|
} catch (error) {
|
|
304
|
-
// Log any unexpected errors if not already logged
|
|
305
337
|
if (!(error instanceof Error && error.message.includes('Failed to retrieve image'))) {
|
|
306
338
|
await auditService.logFileAccess(
|
|
307
339
|
user,
|
package/app/types/file.ts
CHANGED
|
@@ -12,8 +12,6 @@ export interface FileUploadResponse {
|
|
|
12
12
|
id: string;
|
|
13
13
|
filename: string;
|
|
14
14
|
uploaded: string;
|
|
15
|
-
requireSignedURLs: boolean;
|
|
16
|
-
variants: string[];
|
|
17
15
|
};
|
|
18
16
|
errors: Array<{
|
|
19
17
|
code: number;
|
|
@@ -27,4 +25,22 @@ export interface ImageUploadResponse {
|
|
|
27
25
|
result: FileUploadResponse['result'];
|
|
28
26
|
errors: FileUploadResponse['errors'];
|
|
29
27
|
messages: FileUploadResponse['messages'];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SignedImageUrlResponse {
|
|
31
|
+
success: boolean;
|
|
32
|
+
result: {
|
|
33
|
+
fileId: string;
|
|
34
|
+
url: string;
|
|
35
|
+
expiresAt: string;
|
|
36
|
+
expiresInSeconds: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ImageAccessResult {
|
|
41
|
+
url: string;
|
|
42
|
+
revoke: () => void;
|
|
43
|
+
blob?: Blob;
|
|
44
|
+
urlType: 'signed' | 'blob';
|
|
45
|
+
expiresAt?: string;
|
|
30
46
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
|
-
import { type ImageUploadResponse } from '~/types';
|
|
2
|
+
import { type ImageUploadResponse, type SignedImageUrlResponse } from '~/types';
|
|
3
3
|
|
|
4
4
|
const IMAGE_API_BASE = '/api/image';
|
|
5
5
|
|
|
@@ -93,6 +93,54 @@ function parseUploadResponse(payload: string): ImageUploadResponse {
|
|
|
93
93
|
return parsed;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function parseSignedUrlResponse(payload: string): SignedImageUrlResponse {
|
|
97
|
+
const parsed = JSON.parse(payload) as SignedImageUrlResponse;
|
|
98
|
+
if (!parsed.success || !parsed.result?.url || !parsed.result?.fileId || !parsed.result?.expiresAt) {
|
|
99
|
+
throw new Error('Signed URL response is invalid');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return parsed;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function createSignedImageUrlApi(
|
|
106
|
+
user: User,
|
|
107
|
+
fileId: string,
|
|
108
|
+
expiresInSeconds?: number
|
|
109
|
+
): Promise<SignedImageUrlResponse> {
|
|
110
|
+
const response = await fetchImageApi(user, `/${encodeURIComponent(fileId)}/signed-url`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
'Accept': 'application/json'
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify(
|
|
117
|
+
typeof expiresInSeconds === 'number'
|
|
118
|
+
? { expiresInSeconds }
|
|
119
|
+
: {}
|
|
120
|
+
)
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`Signed URL request failed with status ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parsed = parseSignedUrlResponse(await response.text());
|
|
128
|
+
const rawUrl = parsed.result.url;
|
|
129
|
+
let normalizedUrl = rawUrl;
|
|
130
|
+
|
|
131
|
+
if (rawUrl.startsWith('/')) {
|
|
132
|
+
normalizedUrl = new URL(rawUrl, window.location.origin).toString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...parsed,
|
|
137
|
+
result: {
|
|
138
|
+
...parsed.result,
|
|
139
|
+
url: normalizedUrl
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
96
144
|
export async function uploadImageApi(
|
|
97
145
|
user: User,
|
|
98
146
|
file: File,
|
|
@@ -82,12 +82,13 @@ export const onRequest = async ({ request, env }: ImageProxyContext): Promise<Re
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const requestUrl = new URL(request.url);
|
|
86
|
+
|
|
85
87
|
const identity = await verifyFirebaseIdentityFromRequest(request, env);
|
|
86
88
|
if (!identity) {
|
|
87
89
|
return textResponse('Unauthorized', 401);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
const requestUrl = new URL(request.url);
|
|
91
92
|
const proxyPathResult = extractProxyPath(requestUrl);
|
|
92
93
|
if (!proxyPathResult.ok) {
|
|
93
94
|
return proxyPathResult.reason === 'bad-encoding'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/scripts/deploy-config.sh
CHANGED
|
@@ -81,13 +81,23 @@ require_command grep
|
|
|
81
81
|
|
|
82
82
|
is_placeholder() {
|
|
83
83
|
local value="$1"
|
|
84
|
-
local normalized
|
|
84
|
+
local normalized
|
|
85
|
+
|
|
86
|
+
normalized=$(printf '%s' "$value" | tr -d '\r' | tr '[:upper:]' '[:lower:]')
|
|
87
|
+
normalized=$(printf '%s' "$normalized" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
|
|
88
|
+
normalized=${normalized#\"}
|
|
89
|
+
normalized=${normalized%\"}
|
|
85
90
|
|
|
86
91
|
if [ -z "$normalized" ]; then
|
|
87
92
|
return 1
|
|
88
93
|
fi
|
|
89
94
|
|
|
90
|
-
[[ "$normalized"
|
|
95
|
+
[[ "$normalized" =~ ^your_[a-z0-9_]+_here$ || \
|
|
96
|
+
"$normalized" =~ ^your-[a-z0-9-]+-here$ || \
|
|
97
|
+
"$normalized" == "placeholder" || \
|
|
98
|
+
"$normalized" == "changeme" || \
|
|
99
|
+
"$normalized" == "replace_me" || \
|
|
100
|
+
"$normalized" == "replace-me" ]]
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
# Check if .env file exists
|
|
@@ -626,38 +636,8 @@ configure_data_at_rest_encryption_credentials() {
|
|
|
626
636
|
echo -e "${BLUE}🗃️ DATA-AT-REST ENCRYPTION CONFIGURATION${NC}"
|
|
627
637
|
echo "========================================"
|
|
628
638
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
local enable_choice=""
|
|
632
|
-
|
|
633
|
-
current_enabled=$(strip_carriage_returns "$current_enabled")
|
|
634
|
-
normalized_enabled=$(printf '%s' "$current_enabled" | tr '[:upper:]' '[:lower:]')
|
|
635
|
-
|
|
636
|
-
if [ -z "$normalized_enabled" ] || is_placeholder "$normalized_enabled"; then
|
|
637
|
-
DATA_AT_REST_ENCRYPTION_ENABLED="false"
|
|
638
|
-
elif [ "$normalized_enabled" = "1" ] || [ "$normalized_enabled" = "true" ] || [ "$normalized_enabled" = "yes" ] || [ "$normalized_enabled" = "on" ]; then
|
|
639
|
-
DATA_AT_REST_ENCRYPTION_ENABLED="true"
|
|
640
|
-
else
|
|
641
|
-
DATA_AT_REST_ENCRYPTION_ENABLED="false"
|
|
642
|
-
fi
|
|
643
|
-
|
|
644
|
-
if [ "$update_env" != "true" ]; then
|
|
645
|
-
read -p "Enable data-at-rest encryption for new writes? (y/N, Enter keeps current): " enable_choice
|
|
646
|
-
enable_choice=$(strip_carriage_returns "$enable_choice")
|
|
647
|
-
case "$enable_choice" in
|
|
648
|
-
y|Y|yes|YES)
|
|
649
|
-
DATA_AT_REST_ENCRYPTION_ENABLED="true"
|
|
650
|
-
;;
|
|
651
|
-
n|N|no|NO)
|
|
652
|
-
DATA_AT_REST_ENCRYPTION_ENABLED="false"
|
|
653
|
-
;;
|
|
654
|
-
"")
|
|
655
|
-
;;
|
|
656
|
-
*)
|
|
657
|
-
echo -e "${YELLOW}⚠️ Unrecognized choice '$enable_choice'; keeping current setting${NC}"
|
|
658
|
-
;;
|
|
659
|
-
esac
|
|
660
|
-
fi
|
|
639
|
+
# Data-at-rest encryption is mandatory for all environments.
|
|
640
|
+
DATA_AT_REST_ENCRYPTION_ENABLED="true"
|
|
661
641
|
|
|
662
642
|
export DATA_AT_REST_ENCRYPTION_ENABLED
|
|
663
643
|
write_env_var "DATA_AT_REST_ENCRYPTION_ENABLED" "$DATA_AT_REST_ENCRYPTION_ENABLED"
|
|
@@ -781,7 +761,7 @@ required_vars=(
|
|
|
781
761
|
# Worker-Specific Secrets (required for deployment)
|
|
782
762
|
"KEYS_AUTH"
|
|
783
763
|
"PDF_WORKER_AUTH"
|
|
784
|
-
"
|
|
764
|
+
"IMAGE_SIGNED_URL_SECRET"
|
|
785
765
|
"BROWSER_API_TOKEN"
|
|
786
766
|
"MANIFEST_SIGNING_PRIVATE_KEY"
|
|
787
767
|
"MANIFEST_SIGNING_KEY_ID"
|
|
@@ -968,7 +948,6 @@ validate_generated_configs() {
|
|
|
968
948
|
assert_contains_literal "workers/user-worker/wrangler.jsonc" "$KV_STORE_ID" "KV_STORE_ID missing in user worker config"
|
|
969
949
|
|
|
970
950
|
assert_contains_literal "app/config/config.json" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in app/config/config.json"
|
|
971
|
-
assert_contains_literal "app/config/config.json" "$ACCOUNT_HASH" "ACCOUNT_HASH missing in app/config/config.json"
|
|
972
951
|
assert_contains_literal "app/config/config.json" "$EXPORT_ENCRYPTION_KEY_ID" "EXPORT_ENCRYPTION_KEY_ID missing in app/config/config.json"
|
|
973
952
|
assert_contains_literal "app/config/config.json" "\"export_encryption_public_key\":" "export_encryption_public_key missing in app/config/config.json"
|
|
974
953
|
assert_contains_literal "app/routes/auth/login.tsx" "const APP_CANONICAL_ORIGIN = 'https://$PAGES_CUSTOM_DOMAIN';" "PAGES_CUSTOM_DOMAIN missing in app/routes/auth/login.tsx canonical origin"
|
|
@@ -989,7 +968,7 @@ validate_generated_configs() {
|
|
|
989
968
|
assert_contains_literal "workers/user-worker/src/user-worker.ts" "https://$PAGES_CUSTOM_DOMAIN" "PAGES_CUSTOM_DOMAIN missing in user-worker source"
|
|
990
969
|
|
|
991
970
|
local placeholder_pattern
|
|
992
|
-
placeholder_pattern="(\"(ACCOUNT_ID|PAGES_PROJECT_NAME|PAGES_CUSTOM_DOMAIN|KEYS_WORKER_NAME|USER_WORKER_NAME|DATA_WORKER_NAME|AUDIT_WORKER_NAME|IMAGES_WORKER_NAME|PDF_WORKER_NAME|KEYS_WORKER_DOMAIN|USER_WORKER_DOMAIN|DATA_WORKER_DOMAIN|AUDIT_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN|PDF_WORKER_DOMAIN|DATA_BUCKET_NAME|AUDIT_BUCKET_NAME|FILES_BUCKET_NAME|KV_STORE_ID|
|
|
971
|
+
placeholder_pattern="(\"(ACCOUNT_ID|PAGES_PROJECT_NAME|PAGES_CUSTOM_DOMAIN|KEYS_WORKER_NAME|USER_WORKER_NAME|DATA_WORKER_NAME|AUDIT_WORKER_NAME|IMAGES_WORKER_NAME|PDF_WORKER_NAME|KEYS_WORKER_DOMAIN|USER_WORKER_DOMAIN|DATA_WORKER_DOMAIN|AUDIT_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN|PDF_WORKER_DOMAIN|DATA_BUCKET_NAME|AUDIT_BUCKET_NAME|FILES_BUCKET_NAME|KV_STORE_ID|MANIFEST_SIGNING_KEY_ID|MANIFEST_SIGNING_PUBLIC_KEY|EXPORT_ENCRYPTION_KEY_ID|EXPORT_ENCRYPTION_PUBLIC_KEY|YOUR_FIREBASE_API_KEY|YOUR_FIREBASE_AUTH_DOMAIN|YOUR_FIREBASE_PROJECT_ID|YOUR_FIREBASE_STORAGE_BUCKET|YOUR_FIREBASE_MESSAGING_SENDER_ID|YOUR_FIREBASE_APP_ID|YOUR_FIREBASE_MEASUREMENT_ID)\"|'(PAGES_CUSTOM_DOMAIN|DATA_WORKER_DOMAIN|IMAGES_WORKER_DOMAIN)')"
|
|
993
972
|
|
|
994
973
|
local files_to_scan=(
|
|
995
974
|
"wrangler.toml"
|
|
@@ -1250,6 +1229,58 @@ prompt_for_secrets() {
|
|
|
1250
1229
|
fi
|
|
1251
1230
|
|
|
1252
1231
|
# Function to prompt for a variable
|
|
1232
|
+
is_auto_generated_secret_var() {
|
|
1233
|
+
local var_name=$1
|
|
1234
|
+
case "$var_name" in
|
|
1235
|
+
USER_DB_AUTH|R2_KEY_SECRET|KEYS_AUTH|PDF_WORKER_AUTH|IMAGES_API_TOKEN|IMAGE_SIGNED_URL_SECRET)
|
|
1236
|
+
return 0
|
|
1237
|
+
;;
|
|
1238
|
+
*)
|
|
1239
|
+
return 1
|
|
1240
|
+
;;
|
|
1241
|
+
esac
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
is_secret_placeholder_value() {
|
|
1245
|
+
local var_name=$1
|
|
1246
|
+
local value=$2
|
|
1247
|
+
case "$var_name" in
|
|
1248
|
+
USER_DB_AUTH)
|
|
1249
|
+
[ "$value" = "your_custom_user_db_auth_token_here" ]
|
|
1250
|
+
;;
|
|
1251
|
+
R2_KEY_SECRET)
|
|
1252
|
+
[ "$value" = "your_custom_r2_secret_here" ]
|
|
1253
|
+
;;
|
|
1254
|
+
KEYS_AUTH)
|
|
1255
|
+
[ "$value" = "your_custom_keys_auth_token_here" ]
|
|
1256
|
+
;;
|
|
1257
|
+
PDF_WORKER_AUTH)
|
|
1258
|
+
[ "$value" = "your_custom_pdf_worker_auth_token_here" ]
|
|
1259
|
+
;;
|
|
1260
|
+
IMAGES_API_TOKEN)
|
|
1261
|
+
[ "$value" = "your_cloudflare_images_api_token_here" ]
|
|
1262
|
+
;;
|
|
1263
|
+
IMAGE_SIGNED_URL_SECRET)
|
|
1264
|
+
[ "$value" = "your_image_signed_url_secret_here" ]
|
|
1265
|
+
;;
|
|
1266
|
+
*)
|
|
1267
|
+
return 1
|
|
1268
|
+
;;
|
|
1269
|
+
esac
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
generate_secret_value() {
|
|
1273
|
+
local var_name=$1
|
|
1274
|
+
case "$var_name" in
|
|
1275
|
+
IMAGE_SIGNED_URL_SECRET)
|
|
1276
|
+
openssl rand -base64 48 2>/dev/null | tr '+/' '-_' | tr -d '='
|
|
1277
|
+
;;
|
|
1278
|
+
*)
|
|
1279
|
+
openssl rand -hex 32 2>/dev/null
|
|
1280
|
+
;;
|
|
1281
|
+
esac
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1253
1284
|
prompt_for_var() {
|
|
1254
1285
|
local var_name=$1
|
|
1255
1286
|
local description=$2
|
|
@@ -1263,19 +1294,19 @@ prompt_for_secrets() {
|
|
|
1263
1294
|
current_value=$(resolve_existing_domain_value "$var_name" "$current_value")
|
|
1264
1295
|
fi
|
|
1265
1296
|
|
|
1266
|
-
# Auto-generate
|
|
1267
|
-
if
|
|
1297
|
+
# Auto-generate selected secrets - but allow keeping current.
|
|
1298
|
+
if is_auto_generated_secret_var "$var_name"; then
|
|
1268
1299
|
echo -e "${BLUE}$var_name${NC}"
|
|
1269
1300
|
echo -e "${YELLOW}$description${NC}"
|
|
1270
1301
|
|
|
1271
|
-
if [ "$update_env" != "true" ] && [ -n "$current_value" ] && ! is_placeholder "$current_value" &&
|
|
1302
|
+
if [ "$update_env" != "true" ] && [ -n "$current_value" ] && ! is_placeholder "$current_value" && ! is_secret_placeholder_value "$var_name" "$current_value"; then
|
|
1272
1303
|
# Current value exists and is not a placeholder
|
|
1273
1304
|
echo -e "${GREEN}Current value: [HIDDEN]${NC}"
|
|
1274
1305
|
read -p "Generate new secret? (press Enter to keep current, or type 'y' to generate): " gen_choice
|
|
1275
1306
|
gen_choice=$(strip_carriage_returns "$gen_choice")
|
|
1276
1307
|
|
|
1277
1308
|
if [ "$gen_choice" = "y" ] || [ "$gen_choice" = "Y" ]; then
|
|
1278
|
-
new_value=$(
|
|
1309
|
+
new_value=$(generate_secret_value "$var_name" || echo "")
|
|
1279
1310
|
if [ -n "$new_value" ]; then
|
|
1280
1311
|
echo -e "${GREEN}✅ $var_name auto-generated${NC}"
|
|
1281
1312
|
else
|
|
@@ -1302,7 +1333,7 @@ prompt_for_secrets() {
|
|
|
1302
1333
|
else
|
|
1303
1334
|
# No current value or placeholder value - auto-generate
|
|
1304
1335
|
echo -e "${YELLOW}Auto-generating secret...${NC}"
|
|
1305
|
-
new_value=$(
|
|
1336
|
+
new_value=$(generate_secret_value "$var_name" || echo "")
|
|
1306
1337
|
if [ -n "$new_value" ]; then
|
|
1307
1338
|
echo -e "${GREEN}✅ $var_name auto-generated${NC}"
|
|
1308
1339
|
else
|
|
@@ -1428,7 +1459,7 @@ prompt_for_secrets() {
|
|
|
1428
1459
|
echo "==================================="
|
|
1429
1460
|
prompt_for_var "USER_DB_AUTH" "Custom user database authentication token (generate with: openssl rand -hex 16)"
|
|
1430
1461
|
prompt_for_var "R2_KEY_SECRET" "Custom R2 storage authentication token (generate with: openssl rand -hex 16)"
|
|
1431
|
-
prompt_for_var "IMAGES_API_TOKEN" "
|
|
1462
|
+
prompt_for_var "IMAGES_API_TOKEN" "Image worker API token (shared between workers)"
|
|
1432
1463
|
|
|
1433
1464
|
echo -e "${BLUE}🔥 FIREBASE AUTH CONFIGURATION${NC}"
|
|
1434
1465
|
echo "==============================="
|
|
@@ -1534,7 +1565,7 @@ prompt_for_secrets() {
|
|
|
1534
1565
|
echo "============================"
|
|
1535
1566
|
prompt_for_var "KEYS_AUTH" "Keys worker authentication token (generate with: openssl rand -hex 16)"
|
|
1536
1567
|
prompt_for_var "PDF_WORKER_AUTH" "PDF worker authentication token (generate with: openssl rand -hex 16)"
|
|
1537
|
-
prompt_for_var "
|
|
1568
|
+
prompt_for_var "IMAGE_SIGNED_URL_SECRET" "Image signed URL secret (generate with: openssl rand -base64 48 | tr '+/' '-_' | tr -d '=')"
|
|
1538
1569
|
prompt_for_var "BROWSER_API_TOKEN" "Cloudflare Browser Rendering API token (for PDF Worker)"
|
|
1539
1570
|
|
|
1540
1571
|
configure_manifest_signing_credentials
|
|
@@ -1680,15 +1711,12 @@ update_wrangler_configs() {
|
|
|
1680
1711
|
local escaped_manifest_signing_public_key
|
|
1681
1712
|
local escaped_export_encryption_key_id
|
|
1682
1713
|
local escaped_export_encryption_public_key
|
|
1683
|
-
local escaped_account_hash
|
|
1684
1714
|
escaped_manifest_signing_key_id=$(escape_for_sed_replacement "$MANIFEST_SIGNING_KEY_ID")
|
|
1685
1715
|
escaped_manifest_signing_public_key=$(escape_for_sed_replacement "$MANIFEST_SIGNING_PUBLIC_KEY")
|
|
1686
1716
|
escaped_export_encryption_key_id=$(escape_for_sed_replacement "$EXPORT_ENCRYPTION_KEY_ID")
|
|
1687
1717
|
escaped_export_encryption_public_key=$(escape_for_sed_replacement "$EXPORT_ENCRYPTION_PUBLIC_KEY")
|
|
1688
|
-
escaped_account_hash=$(escape_for_sed_replacement "$ACCOUNT_HASH")
|
|
1689
1718
|
|
|
1690
1719
|
sed -i "s|\"url\": \"[^\"]*\"|\"url\": \"https://$escaped_pages_custom_domain\"|g" app/config/config.json
|
|
1691
|
-
sed -i "s|\"account_hash\": \"[^\"]*\"|\"account_hash\": \"$escaped_account_hash\"|g" app/config/config.json
|
|
1692
1720
|
sed -i "s|\"MANIFEST_SIGNING_KEY_ID\"|\"$escaped_manifest_signing_key_id\"|g" app/config/config.json
|
|
1693
1721
|
sed -i "s|\"MANIFEST_SIGNING_PUBLIC_KEY\"|\"$escaped_manifest_signing_public_key\"|g" app/config/config.json
|
|
1694
1722
|
sed -i "s|\"EXPORT_ENCRYPTION_KEY_ID\"|\"$escaped_export_encryption_key_id\"|g" app/config/config.json
|
|
@@ -231,7 +231,7 @@ fi
|
|
|
231
231
|
|
|
232
232
|
# Keys Worker
|
|
233
233
|
if ! set_worker_secrets "Keys Worker" "workers/keys-worker" \
|
|
234
|
-
"KEYS_AUTH" "USER_DB_AUTH" "R2_KEY_SECRET" "
|
|
234
|
+
"KEYS_AUTH" "USER_DB_AUTH" "R2_KEY_SECRET" "IMAGES_API_TOKEN" "PDF_WORKER_AUTH"; then
|
|
235
235
|
echo -e "${YELLOW}⚠️ Skipping Keys Worker (not configured)${NC}"
|
|
236
236
|
fi
|
|
237
237
|
|
|
@@ -253,7 +253,7 @@ fi
|
|
|
253
253
|
|
|
254
254
|
# Images Worker
|
|
255
255
|
if ! set_worker_secrets "Images Worker" "workers/image-worker" \
|
|
256
|
-
"IMAGES_API_TOKEN" "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" "DATA_AT_REST_ENCRYPTION_KEY_ID"; then
|
|
256
|
+
"IMAGES_API_TOKEN" "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" "DATA_AT_REST_ENCRYPTION_KEY_ID" "IMAGE_SIGNED_URL_SECRET"; then
|
|
257
257
|
echo -e "${YELLOW}⚠️ Skipping Images Worker (not configured)${NC}"
|
|
258
258
|
fi
|
|
259
259
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
// Generated by Wrangler by running `wrangler types` (hash:
|
|
2
|
+
// Generated by Wrangler by running `wrangler types` (hash: abf7efe3b0d9f2d66e012084f057d73b)
|
|
3
3
|
// Runtime types generated with workerd@1.20250823.0 2026-03-24 nodejs_compat
|
|
4
4
|
declare namespace Cloudflare {
|
|
5
5
|
interface Env {
|
|
@@ -35,7 +35,6 @@ declare namespace Cloudflare {
|
|
|
35
35
|
EXPORT_ENCRYPTION_PRIVATE_KEY: string;
|
|
36
36
|
EXPORT_ENCRYPTION_KEY_ID: string;
|
|
37
37
|
EXPORT_ENCRYPTION_PUBLIC_KEY: string;
|
|
38
|
-
DATA_AT_REST_ENCRYPTION_ENABLED: string;
|
|
39
38
|
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY: string;
|
|
40
39
|
DATA_AT_REST_ENCRYPTION_KEY_ID: string;
|
|
41
40
|
DATA_AT_REST_ENCRYPTION_PUBLIC_KEY: string;
|
|
@@ -44,11 +43,14 @@ declare namespace Cloudflare {
|
|
|
44
43
|
AUDIT_WORKER_DOMAIN: string;
|
|
45
44
|
IMAGES_WORKER_NAME: string;
|
|
46
45
|
IMAGES_WORKER_DOMAIN: string;
|
|
46
|
+
IMAGE_SIGNED_URL_SECRET: string;
|
|
47
|
+
IMAGE_SIGNED_URL_TTL_SECONDS: string;
|
|
47
48
|
PDF_WORKER_NAME: string;
|
|
48
49
|
PDF_WORKER_DOMAIN: string;
|
|
49
50
|
PDF_WORKER_AUTH: string;
|
|
50
51
|
BROWSER_API_TOKEN: string;
|
|
51
52
|
PRIMERSHEAR_EMAILS: string;
|
|
53
|
+
DATA_AT_REST_ENCRYPTION_ENABLED: string;
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
interface Env extends Cloudflare.Env {}
|
|
@@ -56,7 +58,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
|
|
|
56
58
|
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
|
|
57
59
|
};
|
|
58
60
|
declare namespace NodeJS {
|
|
59
|
-
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "KEYS_WORKER_NAME" | "KEYS_WORKER_DOMAIN" | "KEYS_AUTH" | "ACCOUNT_HASH" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "
|
|
61
|
+
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "ACCOUNT_ID" | "USER_DB_AUTH" | "R2_KEY_SECRET" | "IMAGES_API_TOKEN" | "API_KEY" | "AUTH_DOMAIN" | "PROJECT_ID" | "STORAGE_BUCKET" | "MESSAGING_SENDER_ID" | "APP_ID" | "MEASUREMENT_ID" | "FIREBASE_SERVICE_ACCOUNT_EMAIL" | "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "KEYS_WORKER_NAME" | "KEYS_WORKER_DOMAIN" | "KEYS_AUTH" | "ACCOUNT_HASH" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS" | "DATA_AT_REST_ENCRYPTION_ENABLED">> {}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// Begin runtime types
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
// Required secrets: R2_KEY_SECRET, MANIFEST_SIGNING_PRIVATE_KEY, MANIFEST_SIGNING_KEY_ID, EXPORT_ENCRYPTION_PRIVATE_KEY, EXPORT_ENCRYPTION_KEY_ID
|
|
3
|
-
// Optional data-at-rest secrets/vars:
|
|
4
|
-
// - DATA_AT_REST_ENCRYPTION_ENABLED=true
|
|
2
|
+
// Required secrets: R2_KEY_SECRET, MANIFEST_SIGNING_PRIVATE_KEY, MANIFEST_SIGNING_KEY_ID, EXPORT_ENCRYPTION_PRIVATE_KEY, EXPORT_ENCRYPTION_KEY_ID
|
|
5
3
|
// - DATA_AT_REST_ENCRYPTION_PRIVATE_KEY (required for decrypting encrypted records)
|
|
6
4
|
// - DATA_AT_REST_ENCRYPTION_PUBLIC_KEY and DATA_AT_REST_ENCRYPTION_KEY_ID (required when encrypt-on-write is enabled)
|
|
7
5
|
"name": "DATA_WORKER_NAME",
|