@striae-org/striae 5.1.1 → 5.2.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.
Files changed (30) hide show
  1. package/.env.example +20 -1
  2. package/app/utils/data/permissions.ts +4 -2
  3. package/package.json +4 -4
  4. package/scripts/deploy-config/modules/env-utils.sh +322 -0
  5. package/scripts/deploy-config/modules/keys.sh +404 -0
  6. package/scripts/deploy-config/modules/prompt.sh +372 -0
  7. package/scripts/deploy-config/modules/scaffolding.sh +336 -0
  8. package/scripts/deploy-config/modules/validation.sh +365 -0
  9. package/scripts/deploy-config.sh +47 -1572
  10. package/scripts/deploy-worker-secrets.sh +100 -5
  11. package/worker-configuration.d.ts +6 -3
  12. package/workers/audit-worker/package.json +1 -1
  13. package/workers/audit-worker/src/audit-worker.example.ts +188 -6
  14. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  15. package/workers/data-worker/package.json +1 -1
  16. package/workers/data-worker/src/data-worker.example.ts +344 -32
  17. package/workers/data-worker/wrangler.jsonc.example +1 -1
  18. package/workers/image-worker/package.json +1 -1
  19. package/workers/image-worker/src/image-worker.example.ts +190 -5
  20. package/workers/image-worker/wrangler.jsonc.example +1 -1
  21. package/workers/keys-worker/package.json +1 -1
  22. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  23. package/workers/pdf-worker/package.json +1 -1
  24. package/workers/pdf-worker/src/pdf-worker.example.ts +0 -1
  25. package/workers/pdf-worker/wrangler.jsonc.example +1 -5
  26. package/workers/user-worker/package.json +17 -17
  27. package/workers/user-worker/src/encryption-utils.ts +244 -0
  28. package/workers/user-worker/src/user-worker.example.ts +333 -31
  29. package/workers/user-worker/wrangler.jsonc.example +1 -1
  30. package/wrangler.toml.example +1 -1
package/.env.example CHANGED
@@ -54,6 +54,17 @@ KEYS_AUTH=your_custom_keys_auth_token_here
54
54
  USER_WORKER_NAME=your_user_worker_name_here
55
55
  USER_WORKER_DOMAIN=your_user_worker_domain_here
56
56
  KV_STORE_ID=your_kv_store_id_here
57
+ USER_KV_ENCRYPTION_PRIVATE_KEY=your_user_kv_encryption_private_key_here
58
+ USER_KV_ENCRYPTION_KEY_ID=your_user_kv_encryption_key_id_here
59
+ USER_KV_ENCRYPTION_PUBLIC_KEY=your_user_kv_encryption_public_key_here
60
+ # Optional write toggle for USER_DB mutation endpoints.
61
+ # true (default): require USER_KV_ENCRYPTION_PUBLIC_KEY and USER_KV_ENCRYPTION_KEY_ID for encrypt-on-write.
62
+ # false: allow read-only deployments using private key material (legacy key or key registry) without write-path keys.
63
+ USER_KV_WRITE_ENDPOINTS_ENABLED=true
64
+ # Optional key registry for rotation-safe USER_DB reads.
65
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
66
+ USER_KV_ENCRYPTION_KEYS_JSON='{"activeKeyId":"your_user_kv_active_encryption_key_id_here","keys":{"your_user_kv_active_encryption_key_id_here":"your_user_kv_encryption_private_key_here"}}'
67
+ USER_KV_ENCRYPTION_ACTIVE_KEY_ID=your_user_kv_active_encryption_key_id_here
57
68
 
58
69
  # ================================
59
70
  # DATA WORKER ENVIRONMENT VARIABLES
@@ -69,10 +80,18 @@ MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
69
80
  EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
70
81
  EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
71
82
  EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
83
+ # Optional key registry for export decrypt compatibility.
84
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
85
+ EXPORT_ENCRYPTION_KEYS_JSON='{"activeKeyId":"your_export_encryption_active_key_id_here","keys":{"your_export_encryption_active_key_id_here":"your_export_encryption_private_key_here"}}'
86
+ EXPORT_ENCRYPTION_ACTIVE_KEY_ID=your_export_encryption_active_key_id_here
87
+ DATA_AT_REST_ENCRYPTION_ENABLED=true
72
88
  DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
73
89
  DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
74
90
  DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
75
-
91
+ # Optional key registry for data/files/audit decryption compatibility.
92
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
93
+ DATA_AT_REST_ENCRYPTION_KEYS_JSON='{"activeKeyId":"your_data_at_rest_active_encryption_key_id_here","keys":{"your_data_at_rest_active_encryption_key_id_here":"your_data_at_rest_encryption_private_key_here"}}'
94
+ DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID=your_data_at_rest_active_encryption_key_id_here
76
95
 
77
96
  # ================================
78
97
  # AUDIT WORKER ENVIRONMENT VARIABLES
@@ -42,8 +42,10 @@ export const getUserData = async (user: User): Promise<UserData | null> => {
42
42
  if (response.status === 404) {
43
43
  return null; // User not found
44
44
  }
45
-
46
- throw new Error('Failed to fetch user data');
45
+
46
+ const responseBody = await response.text().catch(() => '');
47
+ const detail = responseBody ? `: ${responseBody}` : '';
48
+ throw new Error(`Failed to fetch user data (${response.status} ${response.statusText})${detail}`);
47
49
  } catch (error) {
48
50
  console.error('Error fetching user data:', error);
49
51
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.1.1",
3
+ "version": "5.2.0",
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",
@@ -54,7 +54,7 @@
54
54
  "workers/*/src/*.example.ts",
55
55
  "workers/*/src/*.example.js",
56
56
  "workers/*/src/*.ts",
57
- "workers/pdf-worker/scripts/*.js",
57
+ "workers/pdf-worker/scripts/*.js",
58
58
  "!workers/*/src/*worker.ts",
59
59
  "workers/pdf-worker/src/assets/generated-assets.example.ts",
60
60
  "workers/pdf-worker/src/formats/format-striae.ts",
@@ -67,7 +67,7 @@
67
67
  "vite.config.ts",
68
68
  "worker-configuration.d.ts",
69
69
  "wrangler.toml.example",
70
- "LICENSE"
70
+ "LICENSE"
71
71
  ],
72
72
  "sideEffects": false,
73
73
  "type": "module",
@@ -106,7 +106,7 @@
106
106
  "deploy-workers:image": "cd workers/image-worker && npm run deploy",
107
107
  "deploy-workers:keys": "cd workers/keys-worker && npm run deploy",
108
108
  "deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
109
- "deploy-workers:user": "cd workers/user-worker && npm run deploy"
109
+ "deploy-workers:user": "cd workers/user-worker && npm run deploy"
110
110
  },
111
111
  "dependencies": {
112
112
  "@react-router/cloudflare": "^7.13.2",
@@ -0,0 +1,322 @@
1
+ #!/bin/bash
2
+
3
+ escape_for_sed_pattern() {
4
+ printf '%s' "$1" | sed -e 's/[][\\.^$*+?{}|()]/\\&/g'
5
+ }
6
+
7
+ dedupe_env_var_entries() {
8
+ local var_name=$1
9
+ local expected_count=1
10
+ local escaped_var_name
11
+
12
+ escaped_var_name=$(escape_for_sed_pattern "$var_name")
13
+
14
+ if [ -f ".env.example" ]; then
15
+ expected_count=$(grep -c "^$escaped_var_name=" .env.example || true)
16
+
17
+ if [ "$expected_count" -lt 1 ]; then
18
+ expected_count=1
19
+ fi
20
+ fi
21
+
22
+ awk -v key="$var_name" -v keep="$expected_count" '
23
+ BEGIN { seen = 0 }
24
+ {
25
+ if (index($0, key "=") == 1) {
26
+ seen++
27
+
28
+ if (seen > keep) {
29
+ next
30
+ }
31
+ }
32
+ print
33
+ }
34
+ ' .env > .env.tmp && mv .env.tmp .env
35
+ }
36
+
37
+ normalize_domain_value() {
38
+ local domain="$1"
39
+
40
+ domain=$(printf '%s' "$domain" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
41
+ domain="${domain#http://}"
42
+ domain="${domain#https://}"
43
+ domain="${domain%/}"
44
+
45
+ printf '%s' "$domain"
46
+ }
47
+
48
+ normalize_worker_label_value() {
49
+ local label="$1"
50
+
51
+ label=$(normalize_domain_value "$label")
52
+ label="${label#.}"
53
+ label="${label%.}"
54
+ label=$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')
55
+
56
+ printf '%s' "$label"
57
+ }
58
+
59
+ normalize_worker_subdomain_value() {
60
+ local subdomain="$1"
61
+
62
+ subdomain=$(normalize_domain_value "$subdomain")
63
+ subdomain="${subdomain#.}"
64
+ subdomain="${subdomain%.}"
65
+ subdomain=$(printf '%s' "$subdomain" | tr '[:upper:]' '[:lower:]')
66
+
67
+ printf '%s' "$subdomain"
68
+ }
69
+
70
+ is_valid_worker_label() {
71
+ local label="$1"
72
+
73
+ [[ "$label" =~ ^[a-z0-9-]+$ ]]
74
+ }
75
+
76
+ is_valid_worker_subdomain() {
77
+ local subdomain="$1"
78
+
79
+ [[ "$subdomain" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]]
80
+ }
81
+
82
+ strip_carriage_returns() {
83
+ printf '%s' "$1" | tr -d '\r'
84
+ }
85
+
86
+ read_env_var_from_file() {
87
+ local env_file=$1
88
+ local var_name=$2
89
+
90
+ if [ ! -f "$env_file" ]; then
91
+ return 0
92
+ fi
93
+
94
+ awk -v key="$var_name" '
95
+ index($0, key "=") == 1 {
96
+ value = substr($0, length(key) + 2)
97
+ }
98
+ END {
99
+ if (value != "") {
100
+ gsub(/\r/, "", value)
101
+ gsub(/^"/, "", value)
102
+ gsub(/"$/, "", value)
103
+ print value
104
+ }
105
+ }
106
+ ' "$env_file"
107
+ }
108
+
109
+ resolve_existing_domain_value() {
110
+ local var_name=$1
111
+ local current_value=$2
112
+ local preserved_value=""
113
+
114
+ current_value=$(normalize_domain_value "$current_value")
115
+
116
+ if [ "$current_value" = "$var_name" ]; then
117
+ current_value=""
118
+ fi
119
+
120
+ if [ -n "$current_value" ] && ! is_placeholder "$current_value"; then
121
+ printf '%s' "$current_value"
122
+ return 0
123
+ fi
124
+
125
+ if [ -n "$preserved_domain_env_file" ] && [ -f "$preserved_domain_env_file" ]; then
126
+ preserved_value=$(read_env_var_from_file "$preserved_domain_env_file" "$var_name")
127
+ preserved_value=$(normalize_domain_value "$preserved_value")
128
+
129
+ if [ "$preserved_value" = "$var_name" ]; then
130
+ preserved_value=""
131
+ fi
132
+
133
+ if [ -n "$preserved_value" ] && ! is_placeholder "$preserved_value"; then
134
+ printf '%s' "$preserved_value"
135
+ return 0
136
+ fi
137
+ fi
138
+
139
+ printf '%s' "$current_value"
140
+ }
141
+
142
+ restore_env_var_from_backup_if_missing() {
143
+ local var_name=$1
144
+ local current_value="${!var_name}"
145
+ local preserved_value=""
146
+
147
+ if [ "$update_env" != "true" ]; then
148
+ return 0
149
+ fi
150
+
151
+ if [ -z "$preserved_domain_env_file" ] || [ ! -f "$preserved_domain_env_file" ]; then
152
+ return 0
153
+ fi
154
+
155
+ current_value=$(strip_carriage_returns "$current_value")
156
+
157
+ if [ -n "$current_value" ] && ! is_placeholder "$current_value"; then
158
+ return 0
159
+ fi
160
+
161
+ preserved_value=$(read_env_var_from_file "$preserved_domain_env_file" "$var_name")
162
+ preserved_value=$(strip_carriage_returns "$preserved_value")
163
+
164
+ if [ -z "$preserved_value" ] || is_placeholder "$preserved_value"; then
165
+ return 0
166
+ fi
167
+
168
+ printf -v "$var_name" '%s' "$preserved_value"
169
+ export "$var_name"
170
+ write_env_var "$var_name" "$preserved_value"
171
+ }
172
+
173
+ confirm_key_pair_regeneration() {
174
+ local key_pair_label=$1
175
+ local impact_warning=$2
176
+ local regenerate_choice=""
177
+
178
+ if [ "$force_rotate_keys" = "true" ]; then
179
+ echo -e "${YELLOW}⚠️ Auto-confirmed regeneration for $key_pair_label key pair (--force-rotate-keys).${NC}"
180
+ return 0
181
+ fi
182
+
183
+ echo -e "${GREEN}Current $key_pair_label key pair: [HIDDEN]${NC}"
184
+
185
+ if [ -n "$impact_warning" ]; then
186
+ echo -e "${YELLOW}⚠️ $impact_warning${NC}"
187
+ fi
188
+
189
+ read -p "Regenerate $key_pair_label key pair? (press Enter to keep current, or type 'y' to regenerate): " regenerate_choice
190
+ regenerate_choice=$(strip_carriage_returns "$regenerate_choice")
191
+
192
+ if [ "$regenerate_choice" = "y" ] || [ "$regenerate_choice" = "Y" ]; then
193
+ return 0
194
+ fi
195
+
196
+ return 1
197
+ }
198
+
199
+ generate_worker_subdomain_label() {
200
+ node -e "const { randomInt } = require('crypto'); const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789'; let value = ''; for (let index = 0; index < 10; index += 1) { value += alphabet[randomInt(alphabet.length)]; } process.stdout.write(value);" 2>/dev/null
201
+ }
202
+
203
+ compose_worker_domain() {
204
+ local worker_name=$1
205
+ local worker_subdomain=$2
206
+
207
+ worker_name=$(normalize_worker_label_value "$worker_name")
208
+ worker_subdomain=$(normalize_worker_subdomain_value "$worker_subdomain")
209
+
210
+ if [ -z "$worker_name" ] || [ -z "$worker_subdomain" ]; then
211
+ return 1
212
+ fi
213
+
214
+ if ! is_valid_worker_label "$worker_name" || ! is_valid_worker_subdomain "$worker_subdomain"; then
215
+ return 1
216
+ fi
217
+
218
+ printf '%s.%s' "$worker_name" "$worker_subdomain"
219
+ }
220
+
221
+ infer_worker_subdomain_from_domain() {
222
+ local worker_name=$1
223
+ local worker_domain=$2
224
+ local worker_subdomain=""
225
+
226
+ worker_name=$(normalize_worker_label_value "$worker_name")
227
+ worker_domain=$(normalize_domain_value "$worker_domain")
228
+ worker_domain=$(printf '%s' "$worker_domain" | tr '[:upper:]' '[:lower:]')
229
+
230
+ if [ -z "$worker_name" ] || [ -z "$worker_domain" ] || is_placeholder "$worker_name" || is_placeholder "$worker_domain"; then
231
+ printf '%s' ""
232
+ return 0
233
+ fi
234
+
235
+ case "$worker_domain" in
236
+ "$worker_name".*)
237
+ worker_subdomain="${worker_domain#${worker_name}.}"
238
+ worker_subdomain=$(normalize_worker_subdomain_value "$worker_subdomain")
239
+
240
+ if is_valid_worker_subdomain "$worker_subdomain"; then
241
+ printf '%s' "$worker_subdomain"
242
+ return 0
243
+ fi
244
+ ;;
245
+ esac
246
+
247
+ printf '%s' ""
248
+ }
249
+
250
+ write_env_var() {
251
+ local var_name=$1
252
+ local var_value=$2
253
+ local env_file_value="$var_value"
254
+
255
+ var_value=$(strip_carriage_returns "$var_value")
256
+ env_file_value="$var_value"
257
+
258
+ if [ "$var_name" = "FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PRIVATE_KEY" ] || [ "$var_name" = "MANIFEST_SIGNING_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PRIVATE_KEY" ] || [ "$var_name" = "USER_KV_ENCRYPTION_PUBLIC_KEY" ] || [ "$var_name" = "EXPORT_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "DATA_AT_REST_ENCRYPTION_KEYS_JSON" ] || [ "$var_name" = "USER_KV_ENCRYPTION_KEYS_JSON" ]; then
259
+ # Store as a quoted string so sourced .env preserves escaped newline markers (\n)
260
+ env_file_value=${env_file_value//\"/\\\"}
261
+ env_file_value="\"$env_file_value\""
262
+ fi
263
+
264
+ local escaped_var_name
265
+ local replacement_line
266
+ escaped_var_name=$(escape_for_sed_pattern "$var_name")
267
+ replacement_line=$(escape_for_sed_replacement "$var_name=$env_file_value")
268
+
269
+ if grep -q "^$escaped_var_name=" .env; then
270
+ # Replace all occurrences so intentional duplicates in .env.example stay in sync.
271
+ sed -i "s|^$escaped_var_name=.*|$replacement_line|g" .env
272
+ dedupe_env_var_entries "$var_name"
273
+ else
274
+ echo "$var_name=$env_file_value" >> .env
275
+ fi
276
+ }
277
+
278
+ update_private_key_registry() {
279
+ local registry_var_name=$1
280
+ local active_key_var_name=$2
281
+ local current_key_id=$3
282
+ local private_key_value=$4
283
+ local registry_label=$5
284
+ local existing_registry_json=""
285
+ local updated_registry_json=""
286
+ local registry_entry_count=""
287
+
288
+ if [ -z "$registry_var_name" ] || [ -z "$active_key_var_name" ] || [ -z "$current_key_id" ] || [ -z "$private_key_value" ]; then
289
+ echo -e "${YELLOW}⚠️ Skipping $registry_label key registry update due to missing inputs${NC}"
290
+ return 0
291
+ fi
292
+
293
+ existing_registry_json="${!registry_var_name}"
294
+ existing_registry_json=$(strip_carriage_returns "$existing_registry_json")
295
+
296
+ if [ -z "$existing_registry_json" ] || is_placeholder "$existing_registry_json"; then
297
+ existing_registry_json="{}"
298
+ fi
299
+
300
+ updated_registry_json=$(node -e "const raw = process.argv[1] || '{}'; const keyId = process.argv[2] || ''; const privateKey = process.argv[3] || ''; if (!keyId || !privateKey) process.exit(1); const normalized = { activeKeyId: null, keys: {} }; try { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) { if (parsed.keys && typeof parsed.keys === 'object' && !Array.isArray(parsed.keys)) { normalized.keys = Object.fromEntries(Object.entries(parsed.keys).filter(([id, pem]) => typeof id === 'string' && id.trim().length > 0 && typeof pem === 'string' && pem.trim().length > 0)); if (typeof parsed.activeKeyId === 'string' && parsed.activeKeyId.trim().length > 0) normalized.activeKeyId = parsed.activeKeyId.trim(); } else { normalized.keys = Object.fromEntries(Object.entries(parsed).filter(([id, pem]) => id !== 'activeKeyId' && id !== 'keys' && typeof id === 'string' && id.trim().length > 0 && typeof pem === 'string' && pem.trim().length > 0)); if (typeof parsed.activeKeyId === 'string' && parsed.activeKeyId.trim().length > 0) normalized.activeKeyId = parsed.activeKeyId.trim(); } } } catch (_) {} normalized.keys[keyId] = privateKey; normalized.activeKeyId = keyId; process.stdout.write(JSON.stringify(normalized));" "$existing_registry_json" "$current_key_id" "$private_key_value" 2>/dev/null || true)
301
+
302
+ if [ -z "$updated_registry_json" ]; then
303
+ echo -e "${RED}❌ Error: Failed to update $registry_label key registry JSON${NC}"
304
+ exit 1
305
+ fi
306
+
307
+ printf -v "$registry_var_name" '%s' "$updated_registry_json"
308
+ export "$registry_var_name"
309
+ write_env_var "$registry_var_name" "$updated_registry_json"
310
+
311
+ printf -v "$active_key_var_name" '%s' "$current_key_id"
312
+ export "$active_key_var_name"
313
+ write_env_var "$active_key_var_name" "$current_key_id"
314
+
315
+ registry_entry_count=$(node -e "const raw = process.argv[1] || '{}'; try { const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { process.stdout.write('0'); process.exit(0); } const keys = parsed.keys && typeof parsed.keys === 'object' && !Array.isArray(parsed.keys) ? parsed.keys : parsed; const count = Object.entries(keys).filter(([id, pem]) => id !== 'activeKeyId' && id !== 'keys' && typeof id === 'string' && id.trim().length > 0 && typeof pem === 'string' && pem.trim().length > 0).length; process.stdout.write(String(count)); } catch (_) { process.stdout.write('0'); }" "$updated_registry_json")
316
+ echo -e "${GREEN}✅ Updated $registry_label key registry ($registry_entry_count key IDs tracked)${NC}"
317
+ echo -e "${GREEN}✅ $active_key_var_name: $current_key_id${NC}"
318
+ }
319
+
320
+ escape_for_sed_replacement() {
321
+ printf '%s' "$1" | sed -e 's/[&|\\]/\\&/g'
322
+ }