@striae-org/striae 5.5.1 → 5.5.2

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 CHANGED
@@ -141,4 +141,12 @@ BROWSER_API_TOKEN=your_cloudflare_browser_rendering_api_token_here
141
141
  # Comma-separated list of email addresses that will receive the primershear PDF format.
142
142
  # Leave empty to disable the feature. Never commit this value to source control.
143
143
  # Example: PRIMERSHEAR_EMAILS=analyst@org.com,user2@org.com
144
- PRIMERSHEAR_EMAILS=
144
+ PRIMERSHEAR_EMAILS=
145
+
146
+ # ================================
147
+ # REGISTRATION EMAIL ALLOWLIST CONFIGURATION
148
+ # ================================
149
+ # Comma-separated list of email addresses that may register an account.
150
+ # Leave empty to disable the feature. Never commit this value to source control.
151
+ # Example: REGISTRATION_EMAILS=analyst@org.com,user2@org.com
152
+ REGISTRATION_EMAILS=
@@ -94,14 +94,26 @@ export const Login = () => {
94
94
  setIsClient(true);
95
95
  }, []);
96
96
 
97
- // Email validation with regex
98
- const validateRegistrationEmail = (email: string): { valid: boolean } => {
97
+ // Email validation with regex and registration gateway allowlist check
98
+ const validateRegistrationEmail = async (email: string): Promise<{ valid: boolean; message?: string }> => {
99
99
  const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
100
-
100
+
101
101
  if (!emailRegex.test(email)) {
102
102
  return { valid: false };
103
103
  }
104
104
 
105
+ try {
106
+ const response = await fetch(`/api/auth/can-register?email=${encodeURIComponent(email)}`);
107
+ if (response.status === 403) {
108
+ return {
109
+ valid: false,
110
+ message: 'Registration is limited to Striae membership. You may join at https://join.striae.org.'
111
+ };
112
+ }
113
+ } catch {
114
+ // Fail open on network error — server-side PUT guard provides defense-in-depth
115
+ }
116
+
105
117
  return { valid: true };
106
118
  };
107
119
 
@@ -282,9 +294,9 @@ export const Login = () => {
282
294
 
283
295
  try {
284
296
  if (!isLogin) {
285
- const emailValidation = validateRegistrationEmail(email);
297
+ const emailValidation = await validateRegistrationEmail(email);
286
298
  if (!emailValidation.valid) {
287
- setError('Please enter a valid email address');
299
+ setError(emailValidation.message ?? 'Please enter a valid email address');
288
300
  setIsLoading(false);
289
301
  return;
290
302
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Checks whether the given email is permitted to register based on the
3
+ * REGISTRATION_EMAILS secret (a comma-separated list of allowed entries).
4
+ *
5
+ * Each entry may be:
6
+ * - An exact email address: user@example.com
7
+ * - A domain wildcard: @example.com (matches any email from that domain)
8
+ *
9
+ * If registrationEmails is empty or unset, all registrations are allowed
10
+ * (backward-compatible — deploys without a members.emails file are unrestricted).
11
+ */
12
+ export function isEmailAllowed(email: string, registrationEmails: string): boolean {
13
+ if (!registrationEmails || registrationEmails.trim().length === 0) {
14
+ return true;
15
+ }
16
+
17
+ const normalizedEmail = email.toLowerCase().trim();
18
+ const entries = registrationEmails
19
+ .split(',')
20
+ .map(e => e.trim().toLowerCase())
21
+ .filter(Boolean);
22
+
23
+ for (const entry of entries) {
24
+ if (entry.startsWith('@')) {
25
+ // Domain wildcard: @example.com matches user@example.com
26
+ if (normalizedEmail.endsWith(entry)) {
27
+ return true;
28
+ }
29
+ } else {
30
+ // Exact email match
31
+ if (normalizedEmail === entry) {
32
+ return true;
33
+ }
34
+ }
35
+ }
36
+
37
+ return false;
38
+ }
@@ -0,0 +1,59 @@
1
+ import { isEmailAllowed } from '../_shared/registration-allowlist';
2
+
3
+ interface CanRegisterContext {
4
+ request: Request;
5
+ env: Env;
6
+ }
7
+
8
+ const SUPPORTED_METHODS = new Set(['GET', 'OPTIONS']);
9
+
10
+ function jsonResponse(payload: Record<string, unknown>, status: number = 200): Response {
11
+ return new Response(JSON.stringify(payload), {
12
+ status,
13
+ headers: {
14
+ 'Cache-Control': 'no-store',
15
+ 'Content-Type': 'application/json; charset=utf-8'
16
+ }
17
+ });
18
+ }
19
+
20
+ function textResponse(message: string, status: number): Response {
21
+ return new Response(message, {
22
+ status,
23
+ headers: {
24
+ 'Cache-Control': 'no-store',
25
+ 'Content-Type': 'text/plain; charset=utf-8'
26
+ }
27
+ });
28
+ }
29
+
30
+ export const onRequest = async ({ request, env }: CanRegisterContext): Promise<Response> => {
31
+ if (!SUPPORTED_METHODS.has(request.method)) {
32
+ return textResponse('Method not allowed', 405);
33
+ }
34
+
35
+ if (request.method === 'OPTIONS') {
36
+ return new Response(null, {
37
+ status: 204,
38
+ headers: {
39
+ 'Allow': 'GET, OPTIONS',
40
+ 'Cache-Control': 'no-store'
41
+ }
42
+ });
43
+ }
44
+
45
+ const url = new URL(request.url);
46
+ const email = url.searchParams.get('email');
47
+
48
+ if (!email || email.trim().length === 0) {
49
+ return textResponse('Missing required parameter: email', 400);
50
+ }
51
+
52
+ const registrationEmails = env.REGISTRATION_EMAILS ?? '';
53
+
54
+ if (isEmailAllowed(email, registrationEmails)) {
55
+ return jsonResponse({ allowed: true });
56
+ }
57
+
58
+ return jsonResponse({ allowed: false }, 403);
59
+ };
@@ -1,4 +1,5 @@
1
1
  import { verifyFirebaseIdentityFromRequest } from '../_shared/firebase-auth';
2
+ import { isEmailAllowed } from '../_shared/registration-allowlist';
2
3
 
3
4
  interface UserProxyContext {
4
5
  request: Request;
@@ -155,6 +156,39 @@ export const onRequest = async ({ request, env }: UserProxyContext): Promise<Res
155
156
  if (requestedUserId !== identity.uid) {
156
157
  return textResponse('Forbidden', 403);
157
158
  }
159
+
160
+ // Registration gateway: for PUT requests, check if this is a new user creation.
161
+ // If REGISTRATION_EMAILS is set and the user record does not yet exist, enforce the allowlist.
162
+ // This is defense-in-depth — the primary check runs client-side in the login flow.
163
+ if (request.method === 'PUT' && env.REGISTRATION_EMAILS && env.REGISTRATION_EMAILS.trim().length > 0) {
164
+ try {
165
+ const existenceCheckUrl = `${userWorkerBaseUrl}/${encodeURIComponent(requestedUserId)}`;
166
+ const existenceResponse = await fetch(existenceCheckUrl, {
167
+ method: 'GET',
168
+ headers: {
169
+ 'Accept': 'application/json',
170
+ 'X-Custom-Auth-Key': env.USER_DB_AUTH
171
+ }
172
+ });
173
+
174
+ if (existenceResponse.status === 404) {
175
+ // User does not exist yet — this is a registration PUT.
176
+ // Enforce the email allowlist.
177
+ if (!isEmailAllowed(identity.email ?? '', env.REGISTRATION_EMAILS)) {
178
+ return textResponse('Registration is not permitted for this email address', 403);
179
+ }
180
+ } else if (!existenceResponse.ok) {
181
+ // Existence check failed (non-404, non-2xx response).
182
+ // Fail closed: reject the registration to prevent allowlist bypass during errors.
183
+ return textResponse('Unable to verify registration eligibility', 502);
184
+ }
185
+ // If user already exists (200), proceed normally.
186
+ } catch {
187
+ // Fail closed: on network error with allowlist active, reject the request.
188
+ return textResponse('Unable to verify registration eligibility', 502);
189
+ }
190
+ }
191
+
158
192
  const upstreamUrl = `${userWorkerBaseUrl}${proxyPath}${requestUrl.search}`;
159
193
 
160
194
  const upstreamHeaders = new Headers();
@@ -0,0 +1,11 @@
1
+ # Registration gateway - authorized email addresses
2
+ # One entry per line. Lines starting with # are ignored.
3
+ # This file is untracked. Run: npm run deploy-members to push changes.
4
+ #
5
+ # Supported formats:
6
+ # Exact email: analyst@organization.com
7
+ # Domain wildcard: @organization.com (allows all emails from that domain)
8
+ #
9
+ # Examples:
10
+ # analyst@organization.com
11
+ # @striae.org
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
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",
@@ -52,11 +52,12 @@
52
52
  "workers/pdf-worker/src/formats/format-striae.ts",
53
53
  ".env.example",
54
54
  "primershear.emails.example",
55
+ "members.emails.example",
55
56
  "firebase.json",
56
57
  "tsconfig.json",
57
58
  "vite.config.ts",
58
59
  "/worker-configuration.d.ts",
59
- "wrangler.toml.example",
60
+ "wrangler.toml.example",
60
61
  "LICENSE"
61
62
  ],
62
63
  "sideEffects": false,
@@ -90,9 +91,10 @@
90
91
  "install-workers": "bash ./scripts/install-workers.sh",
91
92
  "deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
92
93
  "deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
93
- "deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh --production-only",
94
+ "deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
94
95
  "deploy-pages": "bash ./scripts/deploy-pages.sh",
95
- "deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh --production-only",
96
+ "deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
97
+ "deploy-members": "bash ./scripts/deploy-members-emails.sh",
96
98
  "deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
97
99
  "deploy-workers:data": "cd workers/data-worker && npm run deploy",
98
100
  "deploy-workers:image": "cd workers/image-worker && npm run deploy",
@@ -101,7 +103,7 @@
101
103
  },
102
104
  "dependencies": {
103
105
  "@react-router/cloudflare": "^7.14.0",
104
- "firebase": "^12.11.0",
106
+ "firebase": "^12.12.0",
105
107
  "isbot": "^5.1.37",
106
108
  "jszip": "^3.10.1",
107
109
  "qrcode": "^1.5.4",
@@ -123,7 +125,7 @@
123
125
  "eslint-plugin-jsx-a11y": "^6.10.2",
124
126
  "eslint-plugin-react": "^7.37.5",
125
127
  "eslint-plugin-react-hooks": "^7.0.1",
126
- "firebase-admin": "^13.7.0",
128
+ "firebase-admin": "^13.8.0",
127
129
  "modern-normalize": "^3.0.1",
128
130
  "typescript": "^5.9.3",
129
131
  "vite": "^7.3.2",
@@ -131,9 +133,7 @@
131
133
  "wrangler": "^4.81.1"
132
134
  },
133
135
  "overrides": {
134
- "@tootallnate/once": "3.0.1",
135
- "tar": "7.5.11",
136
- "undici": "7.24.1"
136
+ "@tootallnate/once": "3.0.1"
137
137
  },
138
138
  "engines": {
139
139
  "node": ">=20.19.0"
@@ -127,8 +127,8 @@ echo ""
127
127
  # Step 5: Deploy Pages Secrets
128
128
  echo -e "${PURPLE}Step 5/6: Deploying Pages Secrets${NC}"
129
129
  echo "----------------------------------"
130
- echo -e "${YELLOW}🔐 Deploying Pages environment variables to production only...${NC}"
131
- if ! bash "$SCRIPT_DIR/deploy-pages-secrets.sh" --production-only; then
130
+ echo -e "${YELLOW}🔐 Deploying Pages environment variables...${NC}"
131
+ if ! bash "$SCRIPT_DIR/deploy-pages-secrets.sh"; then
132
132
  echo -e "${RED}❌ Pages secrets deployment failed!${NC}"
133
133
  exit 1
134
134
  fi
@@ -0,0 +1,102 @@
1
+ #!/bin/bash
2
+
3
+ # ============================================
4
+ # MEMBERS EMAIL LIST DEPLOYMENT SCRIPT
5
+ # ============================================
6
+ # Reads members.emails, updates REGISTRATION_EMAILS in .env,
7
+ # then deploys that secret directly to Cloudflare Pages (production).
8
+
9
+ set -e
10
+ set -o pipefail
11
+
12
+ RED='\033[0;31m'
13
+ GREEN='\033[0;32m'
14
+ YELLOW='\033[1;33m'
15
+ BLUE='\033[0;34m'
16
+ NC='\033[0m'
17
+
18
+ echo -e "${BLUE}👥 Members Email List Deployment${NC}"
19
+ echo "=================================="
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
23
+ cd "$PROJECT_ROOT"
24
+
25
+ trap 'echo -e "\n${RED}❌ deploy-members-emails.sh failed near line ${LINENO}${NC}"' ERR
26
+
27
+ # ── Read emails file ──────────────────────────────────────────────────────────
28
+
29
+ EMAILS_FILE="$PROJECT_ROOT/members.emails"
30
+
31
+ if [ ! -f "$EMAILS_FILE" ]; then
32
+ echo -e "${RED}❌ members.emails not found at: $EMAILS_FILE${NC}"
33
+ echo -e "${YELLOW} Create it with one email address or @domain.com wildcard per line.${NC}"
34
+ echo -e "${YELLOW} See members.emails.example for the format.${NC}"
35
+ exit 1
36
+ fi
37
+
38
+ # Strip comment lines and blank lines, then join with commas
39
+ # Use || true to avoid failure if paste gets no input (handles empty file gracefully)
40
+ REGISTRATION_EMAILS=$(grep -v '^[[:space:]]*#' "$EMAILS_FILE" | grep -v '^[[:space:]]*$' | paste -sd ',' - || true)
41
+
42
+ if [ -z "$REGISTRATION_EMAILS" ]; then
43
+ echo -e "${YELLOW}⚠️ members.emails contains no active entries.${NC}"
44
+ echo -e "${YELLOW} The secret will be set to an empty string, disabling the gateway (open registration).${NC}"
45
+ fi
46
+
47
+ ENTRY_COUNT=$(echo "$REGISTRATION_EMAILS" | tr ',' '\n' | grep -c '[^[:space:]]' || true)
48
+ echo -e "${GREEN}✅ Loaded $ENTRY_COUNT entry(ies) from members.emails${NC}"
49
+
50
+ # ── Update .env ───────────────────────────────────────────────────────────────
51
+
52
+ ENV_FILE="$PROJECT_ROOT/.env"
53
+
54
+ if [ ! -f "$ENV_FILE" ]; then
55
+ echo -e "${RED}❌ .env not found. Run deploy-config first.${NC}"
56
+ exit 1
57
+ fi
58
+
59
+ # Replace the REGISTRATION_EMAILS= line in .env (handles both empty and populated values)
60
+ if grep -q '^REGISTRATION_EMAILS=' "$ENV_FILE"; then
61
+ # Use a temp file to avoid sed -i portability issues across macOS/Linux
62
+ local_tmp=$(mktemp)
63
+ sed "s|^REGISTRATION_EMAILS=.*|REGISTRATION_EMAILS=${REGISTRATION_EMAILS}|" "$ENV_FILE" > "$local_tmp"
64
+ mv "$local_tmp" "$ENV_FILE"
65
+ echo -e "${GREEN}✅ Updated REGISTRATION_EMAILS in .env${NC}"
66
+ else
67
+ echo "" >> "$ENV_FILE"
68
+ echo "REGISTRATION_EMAILS=${REGISTRATION_EMAILS}" >> "$ENV_FILE"
69
+ echo -e "${GREEN}✅ Appended REGISTRATION_EMAILS to .env${NC}"
70
+ fi
71
+
72
+ # ── Deploy to Cloudflare Pages ────────────────────────────────────────────────
73
+
74
+ if ! command -v wrangler > /dev/null 2>&1; then
75
+ echo -e "${RED}❌ wrangler is not installed or not in PATH${NC}"
76
+ exit 1
77
+ fi
78
+
79
+ source "$ENV_FILE"
80
+
81
+ PAGES_PROJECT_NAME=$(echo "$PAGES_PROJECT_NAME" | tr -d '\r')
82
+ if [ -z "$PAGES_PROJECT_NAME" ]; then
83
+ echo -e "${RED}❌ PAGES_PROJECT_NAME is missing from .env${NC}"
84
+ exit 1
85
+ fi
86
+
87
+ echo -e "${YELLOW} Setting REGISTRATION_EMAILS for production...${NC}"
88
+ printf '%s' "$REGISTRATION_EMAILS" | wrangler pages secret put REGISTRATION_EMAILS \
89
+ --project-name "$PAGES_PROJECT_NAME"
90
+
91
+ echo -e "${GREEN}✅ REGISTRATION_EMAILS deployed to production${NC}"
92
+
93
+ # Deploy Pages so the new secret takes effect immediately
94
+ echo -e "\n${YELLOW}🚀 Building and deploying Pages to activate new secret...${NC}"
95
+
96
+ if ! npm run deploy-pages; then
97
+ echo -e "${RED}❌ Pages deployment failed${NC}"
98
+ exit 1
99
+ fi
100
+ echo -e "${GREEN}✅ Pages deployment complete${NC}"
101
+
102
+ echo -e "\n${GREEN}🎉 Members email list deployment complete!${NC}"
@@ -24,46 +24,6 @@ cd "$PROJECT_ROOT"
24
24
 
25
25
  trap 'echo -e "\n${RED}❌ deploy-pages-secrets.sh failed near line ${LINENO}${NC}"' ERR
26
26
 
27
- show_help=false
28
- deploy_production=true
29
- deploy_preview=true
30
-
31
- for arg in "$@"; do
32
- case "$arg" in
33
- -h|--help)
34
- show_help=true
35
- ;;
36
- --production-only)
37
- deploy_production=true
38
- deploy_preview=false
39
- ;;
40
- --preview-only)
41
- deploy_production=false
42
- deploy_preview=true
43
- ;;
44
- *)
45
- echo -e "${RED}❌ Unknown option: $arg${NC}"
46
- echo "Use --help to see supported options."
47
- exit 1
48
- ;;
49
- esac
50
- done
51
-
52
- if [ "$show_help" = "true" ]; then
53
- echo "Usage: bash ./scripts/deploy-pages-secrets.sh [--production-only|--preview-only]"
54
- echo ""
55
- echo "Options:"
56
- echo " --production-only Deploy secrets only to the production Pages environment"
57
- echo " --preview-only Deploy secrets only to the preview Pages environment"
58
- echo " -h, --help Show this help message"
59
- exit 0
60
- fi
61
-
62
- if [ "$deploy_production" != "true" ] && [ "$deploy_preview" != "true" ]; then
63
- echo -e "${RED}❌ No target environment selected${NC}"
64
- exit 1
65
- fi
66
-
67
27
  require_command() {
68
28
  local cmd=$1
69
29
  if ! command -v "$cmd" > /dev/null 2>&1; then
@@ -145,40 +105,29 @@ get_optional_value() {
145
105
  printf '%s' "$value"
146
106
  }
147
107
 
148
- set_pages_secret() {
149
- local secret_name=$1
150
- local secret_value=$2
151
- local pages_env=$3
152
-
153
- echo -e "${YELLOW} Setting $secret_name for $pages_env...${NC}"
154
-
155
- if [ "$pages_env" = "production" ]; then
156
- printf '%s' "$secret_value" | wrangler pages secret put "$secret_name" --project-name "$PAGES_PROJECT_NAME"
157
- return 0
158
- fi
159
-
160
- printf '%s' "$secret_value" | wrangler pages secret put "$secret_name" --project-name "$PAGES_PROJECT_NAME" --env "$pages_env"
161
- }
162
-
163
- deploy_pages_environment_secrets() {
164
- local pages_env=$1
108
+ deploy_pages_secrets() {
165
109
  local secret
166
110
  local secret_value
167
111
 
168
- echo -e "\n${BLUE}🔧 Deploying Pages secrets to $pages_env...${NC}"
112
+ echo -e "\n${BLUE}🔧 Deploying Pages secrets to production...${NC}"
169
113
 
170
114
  for secret in "${required_pages_secrets[@]}"; do
171
115
  secret_value=$(get_required_value "$secret")
172
- set_pages_secret "$secret" "$secret_value" "$pages_env"
116
+ echo -e "${YELLOW} Setting $secret...${NC}"
117
+ printf '%s' "$secret_value" | wrangler pages secret put "$secret" --project-name "$PAGES_PROJECT_NAME"
173
118
  done
174
119
 
175
120
  local optional_primershear_emails
176
121
  optional_primershear_emails=$(get_optional_value "PRIMERSHEAR_EMAILS")
177
- if [ -n "$optional_primershear_emails" ]; then
178
- set_pages_secret "PRIMERSHEAR_EMAILS" "$optional_primershear_emails" "$pages_env"
179
- fi
122
+ echo -e "${YELLOW} Setting PRIMERSHEAR_EMAILS...${NC}"
123
+ printf '%s' "$optional_primershear_emails" | wrangler pages secret put "PRIMERSHEAR_EMAILS" --project-name "$PAGES_PROJECT_NAME"
180
124
 
181
- echo -e "${GREEN}✅ Pages secrets deployed to $pages_env${NC}"
125
+ local optional_registration_emails
126
+ optional_registration_emails=$(get_optional_value "REGISTRATION_EMAILS")
127
+ echo -e "${YELLOW} Setting REGISTRATION_EMAILS...${NC}"
128
+ printf '%s' "$optional_registration_emails" | wrangler pages secret put "REGISTRATION_EMAILS" --project-name "$PAGES_PROJECT_NAME"
129
+
130
+ echo -e "${GREEN}✅ Pages secrets deployed to production${NC}"
182
131
  }
183
132
 
184
133
  require_command wrangler
@@ -220,12 +169,6 @@ for secret in "${required_pages_secrets[@]}"; do
220
169
  done
221
170
  echo -e "${GREEN}✅ Required Pages secret values found${NC}"
222
171
 
223
- if [ "$deploy_production" = "true" ]; then
224
- deploy_pages_environment_secrets "production"
225
- fi
226
-
227
- if [ "$deploy_preview" = "true" ]; then
228
- deploy_pages_environment_secrets "preview"
229
- fi
172
+ deploy_pages_secrets
230
173
 
231
174
  echo -e "\n${GREEN}🎉 Pages secrets deployment completed!${NC}"
@@ -4,16 +4,7 @@
4
4
  # PRIMERSHEAR EMAIL LIST DEPLOYMENT SCRIPT
5
5
  # ============================================
6
6
  # Reads primershear.emails, updates PRIMERSHEAR_EMAILS in .env,
7
- # then deploys that secret directly to Cloudflare Pages.
8
- #
9
- # Usage:
10
- # bash ./scripts/deploy-primershear-emails.sh [--production-only|--preview-only|--env-only]
11
- #
12
- # Options:
13
- # --production-only Deploy to production Pages environment only
14
- # --preview-only Deploy to preview Pages environment only
15
- # --env-only Update .env only; do not deploy to Cloudflare
16
- # -h, --help Show this help message
7
+ # then deploys that secret directly to Cloudflare Pages (production).
17
8
 
18
9
  set -e
19
10
  set -o pipefail
@@ -33,43 +24,6 @@ cd "$PROJECT_ROOT"
33
24
 
34
25
  trap 'echo -e "\n${RED}❌ deploy-primershear-emails.sh failed near line ${LINENO}${NC}"' ERR
35
26
 
36
- # ── Argument parsing ─────────────────────────────────────────────────────────
37
-
38
- deploy_production=true
39
- deploy_preview=true
40
- env_only=false
41
-
42
- for arg in "$@"; do
43
- case "$arg" in
44
- -h|--help)
45
- echo "Usage: bash ./scripts/deploy-primershear-emails.sh [--production-only|--preview-only|--env-only]"
46
- echo ""
47
- echo "Options:"
48
- echo " --production-only Deploy to production Pages environment only"
49
- echo " --preview-only Deploy to preview Pages environment only"
50
- echo " --env-only Update .env only; do not deploy to Cloudflare"
51
- echo " -h, --help Show this help message"
52
- exit 0
53
- ;;
54
- --production-only)
55
- deploy_production=true
56
- deploy_preview=false
57
- ;;
58
- --preview-only)
59
- deploy_production=false
60
- deploy_preview=true
61
- ;;
62
- --env-only)
63
- env_only=true
64
- ;;
65
- *)
66
- echo -e "${RED}❌ Unknown option: $arg${NC}"
67
- echo "Use --help to see supported options."
68
- exit 1
69
- ;;
70
- esac
71
- done
72
-
73
27
  # ── Read emails file ──────────────────────────────────────────────────────────
74
28
 
75
29
  EMAILS_FILE="$PROJECT_ROOT/primershear.emails"
@@ -114,11 +68,6 @@ else
114
68
  echo -e "${GREEN}✅ Appended PRIMERSHEAR_EMAILS to .env${NC}"
115
69
  fi
116
70
 
117
- if [ "$env_only" = "true" ]; then
118
- echo -e "\n${GREEN}🎉 .env updated. Skipping Cloudflare deployment (--env-only).${NC}"
119
- exit 0
120
- fi
121
-
122
71
  # ── Deploy to Cloudflare Pages ────────────────────────────────────────────────
123
72
 
124
73
  if ! command -v wrangler > /dev/null 2>&1; then
@@ -134,31 +83,16 @@ if [ -z "$PAGES_PROJECT_NAME" ]; then
134
83
  exit 1
135
84
  fi
136
85
 
137
- set_secret() {
138
- local pages_env=$1
139
- echo -e "${YELLOW} Setting PRIMERSHEAR_EMAILS for $pages_env...${NC}"
140
- if [ "$pages_env" = "production" ]; then
141
- printf '%s' "$PRIMERSHEAR_EMAILS" | wrangler pages secret put PRIMERSHEAR_EMAILS \
142
- --project-name "$PAGES_PROJECT_NAME"
143
- else
144
- printf '%s' "$PRIMERSHEAR_EMAILS" | wrangler pages secret put PRIMERSHEAR_EMAILS \
145
- --project-name "$PAGES_PROJECT_NAME" --env "$pages_env"
146
- fi
147
- }
148
-
149
- if [ "$deploy_production" = "true" ]; then
150
- set_secret "production"
151
- echo -e "${GREEN}✅ PRIMERSHEAR_EMAILS deployed to production${NC}"
152
- fi
86
+ echo -e "${YELLOW} Setting PRIMERSHEAR_EMAILS for production...${NC}"
87
+ printf '%s' "$PRIMERSHEAR_EMAILS" | wrangler pages secret put PRIMERSHEAR_EMAILS \
88
+ --project-name "$PAGES_PROJECT_NAME"
153
89
 
154
- if [ "$deploy_preview" = "true" ]; then
155
- set_secret "preview"
156
- echo -e "${GREEN}✅ PRIMERSHEAR_EMAILS deployed to preview${NC}"
157
- fi
90
+ echo -e "${GREEN}✅ PRIMERSHEAR_EMAILS deployed to production${NC}"
158
91
 
159
92
  # Deploy Pages so the new secret takes effect immediately
160
93
  echo -e "\n${YELLOW}🚀 Building and deploying Pages to activate new secret...${NC}"
161
- if ! npm run deploy; then
94
+
95
+ if ! npm run deploy-pages; then
162
96
  echo -e "${RED}❌ Pages deployment failed${NC}"
163
97
  exit 1
164
98
  fi
@@ -58,6 +58,7 @@ declare namespace Cloudflare {
58
58
  PDF_WORKER_AUTH: string;
59
59
  BROWSER_API_TOKEN: string;
60
60
  PRIMERSHEAR_EMAILS: string;
61
+ REGISTRATION_EMAILS: string;
61
62
  }
62
63
  }
63
64
  interface Env extends Cloudflare.Env {}
@@ -65,7 +66,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
65
66
  [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
66
67
  };
67
68
  declare namespace NodeJS {
68
- 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" | "USER_KV_ENCRYPTION_PRIVATE_KEY" | "USER_KV_ENCRYPTION_KEY_ID" | "USER_KV_ENCRYPTION_PUBLIC_KEY" | "USER_KV_WRITE_ENDPOINTS_ENABLED" | "USER_KV_ENCRYPTION_KEYS_JSON" | "USER_KV_ENCRYPTION_ACTIVE_KEY_ID" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "EXPORT_ENCRYPTION_KEYS_JSON" | "EXPORT_ENCRYPTION_ACTIVE_KEY_ID" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_KEYS_JSON" | "DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "IMAGE_SIGNED_URL_BASE_URL" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS">> {}
69
+ 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" | "USER_KV_ENCRYPTION_PRIVATE_KEY" | "USER_KV_ENCRYPTION_KEY_ID" | "USER_KV_ENCRYPTION_PUBLIC_KEY" | "USER_KV_WRITE_ENDPOINTS_ENABLED" | "USER_KV_ENCRYPTION_KEYS_JSON" | "USER_KV_ENCRYPTION_ACTIVE_KEY_ID" | "MANIFEST_SIGNING_PRIVATE_KEY" | "MANIFEST_SIGNING_KEY_ID" | "MANIFEST_SIGNING_PUBLIC_KEY" | "EXPORT_ENCRYPTION_PRIVATE_KEY" | "EXPORT_ENCRYPTION_KEY_ID" | "EXPORT_ENCRYPTION_PUBLIC_KEY" | "EXPORT_ENCRYPTION_KEYS_JSON" | "EXPORT_ENCRYPTION_ACTIVE_KEY_ID" | "DATA_AT_REST_ENCRYPTION_ENABLED" | "DATA_AT_REST_ENCRYPTION_PRIVATE_KEY" | "DATA_AT_REST_ENCRYPTION_KEY_ID" | "DATA_AT_REST_ENCRYPTION_PUBLIC_KEY" | "DATA_AT_REST_ENCRYPTION_KEYS_JSON" | "DATA_AT_REST_ENCRYPTION_ACTIVE_KEY_ID" | "PAGES_PROJECT_NAME" | "PAGES_CUSTOM_DOMAIN" | "USER_WORKER_NAME" | "USER_WORKER_DOMAIN" | "KV_STORE_ID" | "DATA_WORKER_NAME" | "DATA_BUCKET_NAME" | "FILES_BUCKET_NAME" | "DATA_WORKER_DOMAIN" | "AUDIT_WORKER_NAME" | "AUDIT_BUCKET_NAME" | "AUDIT_WORKER_DOMAIN" | "IMAGES_WORKER_NAME" | "IMAGES_WORKER_DOMAIN" | "IMAGE_SIGNED_URL_SECRET" | "IMAGE_SIGNED_URL_TTL_SECONDS" | "IMAGE_SIGNED_URL_BASE_URL" | "PDF_WORKER_NAME" | "PDF_WORKER_DOMAIN" | "PDF_WORKER_AUTH" | "BROWSER_API_TOKEN" | "PRIMERSHEAR_EMAILS" | "REGISTRATION_EMAILS">> {}
69
70
  }
70
71
 
71
72
  // Begin runtime types
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "audit-worker",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -9,9 +9,5 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "wrangler": "^4.81.1"
12
- },
13
- "overrides": {
14
- "undici": "7.24.1",
15
- "yauzl": "3.2.1"
16
12
  }
17
13
  }
@@ -7,7 +7,7 @@
7
7
  "name": "AUDIT_WORKER_NAME",
8
8
  "account_id": "ACCOUNT_ID",
9
9
  "main": "src/audit-worker.ts",
10
- "compatibility_date": "2026-04-09",
10
+ "compatibility_date": "2026-04-10",
11
11
  "compatibility_flags": [
12
12
  "nodejs_compat"
13
13
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "data-worker",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -9,9 +9,5 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "wrangler": "^4.81.1"
12
- },
13
- "overrides": {
14
- "undici": "7.24.1",
15
- "yauzl": "3.2.1"
16
12
  }
17
13
  }
@@ -5,7 +5,7 @@
5
5
  "name": "DATA_WORKER_NAME",
6
6
  "account_id": "ACCOUNT_ID",
7
7
  "main": "src/data-worker.ts",
8
- "compatibility_date": "2026-04-09",
8
+ "compatibility_date": "2026-04-10",
9
9
  "compatibility_flags": [
10
10
  "nodejs_compat"
11
11
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "image-worker",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -9,9 +9,5 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "wrangler": "^4.81.1"
12
- },
13
- "overrides": {
14
- "undici": "7.24.1",
15
- "yauzl": "3.2.1"
16
12
  }
17
13
  }
@@ -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-04-09",
5
+ "compatibility_date": "2026-04-10",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-worker",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "generate:assets": "node scripts/generate-assets.js",
@@ -10,9 +10,5 @@
10
10
  },
11
11
  "devDependencies": {
12
12
  "wrangler": "^4.81.1"
13
- },
14
- "overrides": {
15
- "undici": "7.24.1",
16
- "yauzl": "3.2.1"
17
13
  }
18
14
  }
@@ -2,7 +2,7 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-04-09",
5
+ "compatibility_date": "2026-04-10",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-worker",
3
- "version": "5.5.1",
3
+ "version": "5.5.2",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "deploy": "wrangler deploy",
@@ -9,9 +9,5 @@
9
9
  },
10
10
  "devDependencies": {
11
11
  "wrangler": "^4.81.1"
12
- },
13
- "overrides": {
14
- "undici": "7.24.1",
15
- "yauzl": "3.2.1"
16
12
  }
17
13
  }
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-04-09",
5
+ "compatibility_date": "2026-04-10",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-04-09"
3
+ compatibility_date = "2026-04-10"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6