@striae-org/striae 5.1.0 → 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 (40) hide show
  1. package/.env.example +22 -2
  2. package/app/components/actions/case-export/download-handlers.ts +18 -1
  3. package/app/components/actions/case-manage.ts +17 -1
  4. package/app/components/actions/generate-pdf.ts +9 -3
  5. package/app/components/actions/image-manage.ts +43 -11
  6. package/app/routes/striae/striae.tsx +1 -0
  7. package/app/types/file.ts +18 -2
  8. package/app/utils/api/image-api-client.ts +49 -1
  9. package/app/utils/data/permissions.ts +4 -2
  10. package/functions/api/image/[[path]].ts +2 -1
  11. package/package.json +4 -4
  12. package/scripts/deploy-config/modules/env-utils.sh +322 -0
  13. package/scripts/deploy-config/modules/keys.sh +404 -0
  14. package/scripts/deploy-config/modules/prompt.sh +372 -0
  15. package/scripts/deploy-config/modules/scaffolding.sh +336 -0
  16. package/scripts/deploy-config/modules/validation.sh +365 -0
  17. package/scripts/deploy-config.sh +59 -1556
  18. package/scripts/deploy-worker-secrets.sh +101 -6
  19. package/worker-configuration.d.ts +9 -4
  20. package/workers/audit-worker/package.json +1 -1
  21. package/workers/audit-worker/src/audit-worker.example.ts +188 -6
  22. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  23. package/workers/data-worker/package.json +1 -1
  24. package/workers/data-worker/src/data-worker.example.ts +344 -32
  25. package/workers/data-worker/wrangler.jsonc.example +2 -4
  26. package/workers/image-worker/package.json +1 -1
  27. package/workers/image-worker/src/image-worker.example.ts +456 -20
  28. package/workers/image-worker/worker-configuration.d.ts +3 -2
  29. package/workers/image-worker/wrangler.jsonc.example +1 -1
  30. package/workers/keys-worker/package.json +1 -1
  31. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  32. package/workers/pdf-worker/package.json +1 -1
  33. package/workers/pdf-worker/src/pdf-worker.example.ts +0 -1
  34. package/workers/pdf-worker/wrangler.jsonc.example +1 -5
  35. package/workers/user-worker/package.json +17 -17
  36. package/workers/user-worker/src/encryption-utils.ts +244 -0
  37. package/workers/user-worker/src/user-worker.example.ts +333 -31
  38. package/workers/user-worker/wrangler.jsonc.example +1 -1
  39. package/wrangler.toml.example +1 -1
  40. package/scripts/encrypt-r2-backfill.mjs +0 -376
@@ -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
+ }