@striae-org/striae 5.1.1 → 5.2.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.
Files changed (30) hide show
  1. package/.env.example +41 -11
  2. package/app/utils/data/permissions.ts +4 -2
  3. package/package.json +5 -5
  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 +344 -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
@@ -34,6 +34,47 @@ PROJECT_ID=your_firebase_project_id_here
34
34
  FIREBASE_SERVICE_ACCOUNT_EMAIL=your_firebase_service_account_email_here
35
35
  FIREBASE_SERVICE_ACCOUNT_PRIVATE_KEY=your_firebase_service_account_private_key_here
36
36
 
37
+ # ================================
38
+ # SIGNING & ENCRYPTION KEYS & REGISTRIES
39
+ # ================================
40
+
41
+ # USER WORKER KV ENCRYPTION CONFIGURATION
42
+ USER_KV_ENCRYPTION_PRIVATE_KEY=your_user_kv_encryption_private_key_here
43
+ USER_KV_ENCRYPTION_KEY_ID=your_user_kv_encryption_key_id_here
44
+ USER_KV_ENCRYPTION_PUBLIC_KEY=your_user_kv_encryption_public_key_here
45
+ # Optional write toggle for USER_DB mutation endpoints.
46
+ # true (default): require USER_KV_ENCRYPTION_PUBLIC_KEY and USER_KV_ENCRYPTION_KEY_ID for encrypt-on-write.
47
+ # false: allow read-only deployments using private key material (legacy key or key registry) without write-path keys.
48
+ USER_KV_WRITE_ENDPOINTS_ENABLED=true
49
+ # Optional key registry for rotation-safe USER_DB reads.
50
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
51
+ USER_KV_ENCRYPTION_KEYS_JSON='{}'
52
+ USER_KV_ENCRYPTION_ACTIVE_KEY_ID=
53
+
54
+ # DATA WORKER MANIFEST SIGNING CONFIGURATION
55
+ MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
56
+ MANIFEST_SIGNING_KEY_ID=your_manifest_signing_key_id_here
57
+ MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
58
+
59
+ # DATA EXPORT ENCRYPTION CONFIGURATION
60
+ EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
61
+ EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
62
+ EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
63
+ # Optional key registry for export decrypt compatibility.
64
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
65
+ EXPORT_ENCRYPTION_KEYS_JSON='{}'
66
+ EXPORT_ENCRYPTION_ACTIVE_KEY_ID=
67
+
68
+ # DATA-AT-REST ENCRYPTION CONFIGURATION
69
+ DATA_AT_REST_ENCRYPTION_ENABLED=true
70
+ DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
71
+ DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
72
+ DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
73
+ # Optional key registry for data/files/audit decryption compatibility.
74
+ # JSON shape: {"activeKeyId":"kid_current","keys":{"kid_current":"-----BEGIN PRIVATE KEY-----\\n...","kid_previous":"-----BEGIN PRIVATE KEY-----\\n..."}}
75
+ DATA_AT_REST_ENCRYPTION_KEYS_JSON='{}'
76
+ DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID=
77
+
37
78
  # ================================
38
79
  # PAGES WORKER ENVIRONMENT VARIABLES
39
80
  # ================================
@@ -62,17 +103,6 @@ DATA_WORKER_NAME=your_data_worker_name_here
62
103
  DATA_BUCKET_NAME=your_data_bucket_name_here
63
104
  FILES_BUCKET_NAME=your_files_bucket_name_here
64
105
  DATA_WORKER_DOMAIN=your_data_worker_domain_here
65
- # Auto-generated by scripts/deploy-config.sh when placeholders are detected.
66
- MANIFEST_SIGNING_PRIVATE_KEY=your_manifest_signing_private_key_here
67
- MANIFEST_SIGNING_KEY_ID=your_manifest_signing_key_id_here
68
- MANIFEST_SIGNING_PUBLIC_KEY=your_manifest_signing_public_key_here
69
- EXPORT_ENCRYPTION_PRIVATE_KEY=your_export_encryption_private_key_here
70
- EXPORT_ENCRYPTION_KEY_ID=your_export_encryption_key_id_here
71
- EXPORT_ENCRYPTION_PUBLIC_KEY=your_export_encryption_public_key_here
72
- DATA_AT_REST_ENCRYPTION_PRIVATE_KEY=your_data_at_rest_encryption_private_key_here
73
- DATA_AT_REST_ENCRYPTION_KEY_ID=your_data_at_rest_encryption_key_id_here
74
- DATA_AT_REST_ENCRYPTION_PUBLIC_KEY=your_data_at_rest_encryption_public_key_here
75
-
76
106
 
77
107
  # ================================
78
108
  # 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.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",
@@ -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",
@@ -135,7 +135,7 @@
135
135
  "typescript": "^5.9.3",
136
136
  "vite": "^6.4.1",
137
137
  "vite-tsconfig-paths": "^6.1.1",
138
- "wrangler": "^4.74.0"
138
+ "wrangler": "^4.77.0"
139
139
  },
140
140
  "overrides": {
141
141
  "tar": "7.5.11",
@@ -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
+ }