@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 +9 -1
- package/app/routes/auth/login.example.tsx +17 -5
- package/functions/api/_shared/registration-allowlist.ts +38 -0
- package/functions/api/auth/can-register.ts +59 -0
- package/functions/api/user/[[path]].ts +34 -0
- package/members.emails.example +11 -0
- package/package.json +9 -9
- package/scripts/deploy-all.sh +2 -2
- package/scripts/deploy-members-emails.sh +102 -0
- package/scripts/deploy-pages-secrets.sh +13 -70
- package/scripts/deploy-primershear-emails.sh +7 -73
- package/worker-configuration.d.ts +2 -1
- package/workers/audit-worker/package.json +1 -5
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -5
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -5
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -5
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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"
|
package/scripts/deploy-all.sh
CHANGED
|
@@ -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
|
|
131
|
-
if ! bash "$SCRIPT_DIR/deploy-pages-secrets.sh"
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-worker",
|
|
3
|
-
"version": "5.5.
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-worker",
|
|
3
|
-
"version": "5.5.
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdf-worker",
|
|
3
|
-
"version": "5.5.
|
|
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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-worker",
|
|
3
|
-
"version": "5.5.
|
|
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
|
}
|
package/wrangler.toml.example
CHANGED