@mindfulauth/core 2.0.0-beta.5 → 2.0.0-beta.7
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/dist/astro/ForgotPasswordScript.astro +64 -0
- package/dist/astro/LoginScript.astro +209 -0
- package/dist/astro/MagicLoginScript.astro +62 -0
- package/dist/astro/MagicRegisterScript.astro +73 -0
- package/dist/astro/MainScript.astro +236 -0
- package/dist/astro/RegisterPasswordScript.astro +118 -0
- package/dist/astro/ResendVerificationScript.astro +51 -0
- package/dist/astro/ResetPasswordScript.astro +155 -0
- package/dist/astro/SecurityScript.astro +490 -0
- package/dist/astro/TurnstileInit.astro +112 -0
- package/dist/astro/VerifyEmailScript.astro +72 -0
- package/dist/astro/VerifyMagicLinkScript.astro +195 -0
- package/dist/core/csp.d.ts +19 -0
- package/dist/core/csp.d.ts.map +1 -0
- package/dist/core/csp.js +36 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/package.json +23 -13
- package/dist/auth-handler.d.ts +0 -5
- package/dist/auth-handler.d.ts.map +0 -1
- package/dist/auth-handler.js +0 -154
- package/dist/auth.d.ts +0 -9
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -56
- package/dist/config.d.ts +0 -49
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -95
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -13
- package/dist/middleware.d.ts +0 -2
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -108
- package/dist/security.d.ts +0 -5
- package/dist/security.d.ts.map +0 -1
- package/dist/security.js +0 -31
- package/dist/types.d.ts +0 -22
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
// Mindful Auth - Verify Magic Link Script Component
|
|
3
|
+
// Provides: Magic link verification with 2FA support
|
|
4
|
+
---
|
|
5
|
+
<script is:inline>
|
|
6
|
+
// Verify Magic Link Script - Astro Optimized
|
|
7
|
+
|
|
8
|
+
function getPathParams() {
|
|
9
|
+
const pathParts = window.location.pathname.split('/');
|
|
10
|
+
return {
|
|
11
|
+
recordid: pathParts[2],
|
|
12
|
+
token: pathParts[3]
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function handleVerifyMagicLinkSubmit(event) {
|
|
17
|
+
if (event) event.preventDefault();
|
|
18
|
+
|
|
19
|
+
const form = document.querySelector('[data-mindfulauth-form="verify-magic-link"]');
|
|
20
|
+
if (!form) return;
|
|
21
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
|
|
22
|
+
if (!messageEl) return;
|
|
23
|
+
const twoFACodeEl = form.querySelector('[data-mindfulauth-field="twofa-code"]');
|
|
24
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
25
|
+
|
|
26
|
+
// Skip API call on localhost to prevent production logs pollution
|
|
27
|
+
const hostname = window.location.hostname;
|
|
28
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local')) {
|
|
29
|
+
messageEl.textContent = 'Magic link verification skipped on localhost.';
|
|
30
|
+
console.log('[Verify Magic Link] Skipping API call on localhost');
|
|
31
|
+
if (form) form.style.display = 'none';
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { recordid, token } = getPathParams();
|
|
36
|
+
|
|
37
|
+
if (!recordid || !token) {
|
|
38
|
+
messageEl.textContent = "Invalid or expired magic link. Please request a new one.";
|
|
39
|
+
if (form) form.style.display = 'none';
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get Turnstile token
|
|
44
|
+
const turnstileToken = form.querySelector('[name="cf-turnstile-response"]')?.value;
|
|
45
|
+
if (!turnstileToken) {
|
|
46
|
+
messageEl.textContent = "Bot protection validation required.";
|
|
47
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
messageEl.textContent = "Verifying magic link...";
|
|
52
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const requestBody = {
|
|
56
|
+
'cf-turnstile-response': turnstileToken
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Include 2FA code if provided
|
|
60
|
+
if (twoFACodeEl && twoFACodeEl.value) {
|
|
61
|
+
requestBody.twoFACode = twoFACodeEl.value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const endpoint = `/auth/verify-magic-link/${recordid}/${token}`;
|
|
65
|
+
console.log('[Verify Magic Link] Making request to:', endpoint);
|
|
66
|
+
console.log('[Verify Magic Link] Request body:', requestBody);
|
|
67
|
+
|
|
68
|
+
const response = await window.apiFetch(endpoint, {
|
|
69
|
+
body: JSON.stringify(requestBody)
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log('[Verify Magic Link] Response status:', response.status);
|
|
73
|
+
const result = await response.json();
|
|
74
|
+
console.log('[Verify Magic Link] Response body:', result);
|
|
75
|
+
|
|
76
|
+
// Check if 2FA is required
|
|
77
|
+
if (result.requires2FA) {
|
|
78
|
+
// Reset Turnstile to generate a fresh token for the 2FA submission
|
|
79
|
+
// (Turnstile tokens are single-use; first request consumed it)
|
|
80
|
+
window.turnstile?.reset();
|
|
81
|
+
|
|
82
|
+
// Show the 2FA input field
|
|
83
|
+
const twoFAContainer = form.querySelector('[data-mindfulauth-field="twofa-code-container"]');
|
|
84
|
+
if (twoFAContainer) {
|
|
85
|
+
twoFAContainer.removeAttribute('hidden');
|
|
86
|
+
twoFAContainer.classList && twoFAContainer.classList.remove('hidden');
|
|
87
|
+
twoFAContainer.style.setProperty('display', 'block', 'important');
|
|
88
|
+
// Try common display values
|
|
89
|
+
if (window.getComputedStyle(twoFAContainer).display === 'none') {
|
|
90
|
+
twoFAContainer.style.setProperty('display', 'flex', 'important');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (twoFACodeEl) {
|
|
94
|
+
twoFACodeEl.focus();
|
|
95
|
+
}
|
|
96
|
+
messageEl.textContent = '2FA code is required to complete login.';
|
|
97
|
+
if (submitBtn) {
|
|
98
|
+
submitBtn.textContent = 'Verify 2FA Code';
|
|
99
|
+
submitBtn.disabled = false;
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (result.success) {
|
|
105
|
+
console.log('[Verify Magic Link] Verification successful');
|
|
106
|
+
messageEl.textContent = result.message || 'Login successful! Redirecting...';
|
|
107
|
+
if (form) form.style.display = 'none';
|
|
108
|
+
|
|
109
|
+
// Redirect to secure area (match login.js pattern)
|
|
110
|
+
if (result.redirect) {
|
|
111
|
+
console.log('[Verify Magic Link] Redirecting to:', result.redirect);
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
window.location.assign(result.redirect);
|
|
114
|
+
}, 200);
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
console.error('[Verify Magic Link] Verification failed:', result.message);
|
|
118
|
+
throw new Error(result.message || 'Verification failed.');
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('[Verify Magic Link] Error:', error);
|
|
122
|
+
messageEl.textContent = `Error: ${error.message}`;
|
|
123
|
+
if (submitBtn) submitBtn.disabled = false;
|
|
124
|
+
window.turnstile?.reset();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// --- MAIN EXECUTION ---
|
|
129
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
130
|
+
const form = document.querySelector('[data-mindfulauth-form="verify-magic-link"]');
|
|
131
|
+
if (!form) return;
|
|
132
|
+
|
|
133
|
+
// Initialize message and disable button until Turnstile is ready
|
|
134
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
|
|
135
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
136
|
+
if (messageEl) {
|
|
137
|
+
messageEl.textContent = 'Loading bot protection...';
|
|
138
|
+
}
|
|
139
|
+
if (submitBtn) {
|
|
140
|
+
submitBtn.disabled = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Initially hide 2FA field - it will be shown if needed
|
|
144
|
+
const twoFAContainer = form.querySelector('[data-mindfulauth-field="twofa-code-container"]');
|
|
145
|
+
if (twoFAContainer) {
|
|
146
|
+
twoFAContainer.style.display = 'none';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Wait for Turnstile to be ready
|
|
150
|
+
let turnstileReady = false;
|
|
151
|
+
const checkTurnstile = setInterval(() => {
|
|
152
|
+
const turnstileToken = form.querySelector('[name="cf-turnstile-response"]')?.value;
|
|
153
|
+
if (turnstileToken) {
|
|
154
|
+
console.log('[Verify Magic Link] Turnstile token loaded - ready for user verification');
|
|
155
|
+
clearInterval(checkTurnstile);
|
|
156
|
+
turnstileReady = true;
|
|
157
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
|
|
158
|
+
if (messageEl) {
|
|
159
|
+
messageEl.textContent = 'Ready to verify. Click the button below to sign in.';
|
|
160
|
+
}
|
|
161
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
162
|
+
if (submitBtn) {
|
|
163
|
+
submitBtn.disabled = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}, 100);
|
|
167
|
+
|
|
168
|
+
// Check for Turnstile load every 500ms for up to 30 seconds
|
|
169
|
+
// (don't show error proactively - only if user tries to submit)
|
|
170
|
+
let turnstileCheckCount = 0;
|
|
171
|
+
const extendedCheckTurnstile = setInterval(() => {
|
|
172
|
+
const turnstileToken = form.querySelector('[name="cf-turnstile-response"]')?.value;
|
|
173
|
+
if (turnstileToken && !turnstileReady) {
|
|
174
|
+
clearInterval(extendedCheckTurnstile);
|
|
175
|
+
turnstileReady = true;
|
|
176
|
+
const messageEl = document.querySelector('[data-mindfulauth-field="message"]');
|
|
177
|
+
if (messageEl) {
|
|
178
|
+
messageEl.textContent = 'Ready to verify. Click the button below to sign in.';
|
|
179
|
+
}
|
|
180
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
181
|
+
if (submitBtn) {
|
|
182
|
+
submitBtn.disabled = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
turnstileCheckCount++;
|
|
186
|
+
// Stop checking after 30 seconds
|
|
187
|
+
if (turnstileCheckCount > 60) {
|
|
188
|
+
clearInterval(extendedCheckTurnstile);
|
|
189
|
+
}
|
|
190
|
+
}, 500);
|
|
191
|
+
|
|
192
|
+
// Also handle explicit form submission (for 2FA code entry)
|
|
193
|
+
form.addEventListener('submit', handleVerifyMagicLinkSubmit);
|
|
194
|
+
});
|
|
195
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scans the mindfulauth/astro/ directory at build time and returns SHA-384
|
|
3
|
+
* hashes for all <script is:inline> blocks found in .astro component files.
|
|
4
|
+
*
|
|
5
|
+
* Astro's static CSP analysis cannot resolve dynamically rendered components,
|
|
6
|
+
* so hashes must be declared manually in astro.config.mjs. This function
|
|
7
|
+
* computes them automatically so no manual maintenance is needed.
|
|
8
|
+
*
|
|
9
|
+
* When published as a package, this function resolves the astro/ directory
|
|
10
|
+
* relative to its own location — no consumer configuration required.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // astro.config.mjs
|
|
14
|
+
* // import { getScriptHashes } from '@mindfulauth/core';
|
|
15
|
+
*
|
|
16
|
+
* scriptDirective: { hashes: getScriptHashes() }
|
|
17
|
+
*/
|
|
18
|
+
export declare function getScriptHashes(): string[];
|
|
19
|
+
//# sourceMappingURL=csp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csp.d.ts","sourceRoot":"","sources":["../../src/core/csp.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAS1C"}
|
package/dist/core/csp.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Build-time CSP utilities for Mindful Auth
|
|
3
|
+
// Import this in astro.config.mjs only — not at SSR runtime.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/**
|
|
11
|
+
* Scans the mindfulauth/astro/ directory at build time and returns SHA-384
|
|
12
|
+
* hashes for all <script is:inline> blocks found in .astro component files.
|
|
13
|
+
*
|
|
14
|
+
* Astro's static CSP analysis cannot resolve dynamically rendered components,
|
|
15
|
+
* so hashes must be declared manually in astro.config.mjs. This function
|
|
16
|
+
* computes them automatically so no manual maintenance is needed.
|
|
17
|
+
*
|
|
18
|
+
* When published as a package, this function resolves the astro/ directory
|
|
19
|
+
* relative to its own location — no consumer configuration required.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // astro.config.mjs
|
|
23
|
+
* // import { getScriptHashes } from '@mindfulauth/core';
|
|
24
|
+
*
|
|
25
|
+
* scriptDirective: { hashes: getScriptHashes() }
|
|
26
|
+
*/
|
|
27
|
+
export function getScriptHashes() {
|
|
28
|
+
const dir = join(__dirname, '../astro');
|
|
29
|
+
return readdirSync(dir)
|
|
30
|
+
.filter(f => f.endsWith('.astro'))
|
|
31
|
+
.flatMap(file => {
|
|
32
|
+
const content = readFileSync(join(dir, file), 'utf8');
|
|
33
|
+
return [...content.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)]
|
|
34
|
+
.map((m) => 'sha384-' + createHash('sha384').update(m[1], 'utf8').digest('base64'));
|
|
35
|
+
});
|
|
36
|
+
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAGA,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,QAAQ,CAAC;AAGvB,cAAc,gBAAgB,CAAC;AAG/B,cAAc,YAAY,CAAC;AAG3B,cAAc,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAGA,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,QAAQ,CAAC;AAGvB,cAAc,gBAAgB,CAAC;AAG/B,cAAc,YAAY,CAAC;AAG3B,cAAc,cAAc,CAAC;AAG7B,cAAc,OAAO,CAAC"}
|
package/dist/core/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindfulauth/core",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.7",
|
|
4
4
|
"description": "Mindful Auth core authentication library for Astro 6",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/index.js",
|
|
7
|
-
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./dist/core/index.js",
|
|
7
|
+
"types": "./dist/core/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
10
|
+
"types": "./dist/core/index.d.ts",
|
|
11
|
+
"import": "./dist/core/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./astro": {
|
|
14
|
+
"types": "./dist/astro/index.d.ts",
|
|
15
|
+
"import": "./dist/astro/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./astro/*.astro": "./dist/astro/*.astro",
|
|
13
18
|
"./middleware": {
|
|
14
|
-
"types": "./dist/middleware.d.ts",
|
|
15
|
-
"import": "./dist/middleware.js"
|
|
19
|
+
"types": "./dist/core/middleware.d.ts",
|
|
20
|
+
"import": "./dist/core/middleware.js"
|
|
16
21
|
},
|
|
17
22
|
"./auth-handler": {
|
|
18
|
-
"types": "./dist/auth-handler.d.ts",
|
|
19
|
-
"import": "./dist/auth-handler.js"
|
|
23
|
+
"types": "./dist/core/auth-handler.d.ts",
|
|
24
|
+
"import": "./dist/core/auth-handler.js"
|
|
20
25
|
},
|
|
21
26
|
"./config": {
|
|
22
|
-
"types": "./dist/config.d.ts",
|
|
23
|
-
"import": "./dist/config.js"
|
|
27
|
+
"types": "./dist/core/config.d.ts",
|
|
28
|
+
"import": "./dist/core/config.js"
|
|
29
|
+
},
|
|
30
|
+
"./csp": {
|
|
31
|
+
"types": "./dist/core/csp.d.ts",
|
|
32
|
+
"import": "./dist/core/csp.js"
|
|
24
33
|
}
|
|
25
34
|
},
|
|
26
35
|
"files": [
|
|
@@ -28,7 +37,7 @@
|
|
|
28
37
|
"README.md"
|
|
29
38
|
],
|
|
30
39
|
"scripts": {
|
|
31
|
-
"build": "tsc",
|
|
40
|
+
"build": "tsc && cp src/astro/*.astro dist/astro/",
|
|
32
41
|
"dev": "tsc --watch",
|
|
33
42
|
"prepublishOnly": "npm run build"
|
|
34
43
|
},
|
|
@@ -46,7 +55,8 @@
|
|
|
46
55
|
},
|
|
47
56
|
"devDependencies": {
|
|
48
57
|
"@cloudflare/workers-types": "^4.20260307.1",
|
|
58
|
+
"@types/node": "^25.3.5",
|
|
49
59
|
"astro": "^6.0.0-beta.20",
|
|
50
60
|
"typescript": "^5.9.3"
|
|
51
61
|
}
|
|
52
|
-
}
|
|
62
|
+
}
|
package/dist/auth-handler.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** Handle GET requests (email verification, password reset links, etc.) */
|
|
2
|
-
export declare function handleAuthGet(rawEndpoint: string, request: Request, url: URL, locals?: any): Promise<Response>;
|
|
3
|
-
/** Handle POST requests (login, 2FA, password changes, etc.) */
|
|
4
|
-
export declare function handleAuthPost(rawEndpoint: string, request: Request, url: URL, locals?: any): Promise<Response>;
|
|
5
|
-
//# sourceMappingURL=auth-handler.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth-handler.d.ts","sourceRoot":"","sources":["../src/auth-handler.ts"],"names":[],"mappings":"AAwEA,2EAA2E;AAC3E,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CA6BpH;AAED,gEAAgE;AAChE,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyDrH"}
|
package/dist/auth-handler.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// Auth proxy handler for Mindful Auth
|
|
2
|
-
// Forwards authentication requests to the central Mindful Auth service
|
|
3
|
-
//
|
|
4
|
-
// ASTRO 6 MIGRATION:
|
|
5
|
-
// - Astro v6 removed context.locals.runtime.env. Env vars now import from 'cloudflare:workers'.
|
|
6
|
-
// - Note: @cloudflare/workers-types must be installed and referenced in env.d.ts.
|
|
7
|
-
import { env } from 'cloudflare:workers';
|
|
8
|
-
import { CENTRAL_AUTH_ORIGIN, ALLOWED_AUTH_METHODS, MAX_BODY_SIZE_BYTES, AUTH_PROXY_TIMEOUT_MS } from './config';
|
|
9
|
-
import { sanitizeEndpoint } from './security';
|
|
10
|
-
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
11
|
-
const jsonError = (error, status) => new Response(JSON.stringify({ error }), { status, headers: JSON_HEADERS });
|
|
12
|
-
/** Build proxy headers from incoming request */
|
|
13
|
-
function buildProxyHeaders(request, tenantDomain, apiKey) {
|
|
14
|
-
const headers = {
|
|
15
|
-
'Content-Type': 'application/json',
|
|
16
|
-
'X-Tenant-Domain': tenantDomain,
|
|
17
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
18
|
-
};
|
|
19
|
-
if (apiKey)
|
|
20
|
-
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
21
|
-
const cookie = request.headers.get('Cookie');
|
|
22
|
-
if (cookie)
|
|
23
|
-
headers['Cookie'] = cookie;
|
|
24
|
-
const clientIp = request.headers.get('cf-connecting-ip') ||
|
|
25
|
-
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
26
|
-
request.headers.get('x-real-ip');
|
|
27
|
-
if (clientIp)
|
|
28
|
-
headers['X-Original-Client-IP'] = clientIp;
|
|
29
|
-
const userAgent = request.headers.get('user-agent');
|
|
30
|
-
if (userAgent)
|
|
31
|
-
headers['User-Agent'] = userAgent;
|
|
32
|
-
return headers;
|
|
33
|
-
}
|
|
34
|
-
/** Fetch with timeout */
|
|
35
|
-
async function fetchWithTimeout(url, options) {
|
|
36
|
-
const controller = new AbortController();
|
|
37
|
-
const timeoutId = setTimeout(() => controller.abort(), AUTH_PROXY_TIMEOUT_MS);
|
|
38
|
-
try {
|
|
39
|
-
return await fetch(url, { ...options, signal: controller.signal });
|
|
40
|
-
}
|
|
41
|
-
finally {
|
|
42
|
-
clearTimeout(timeoutId);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/** Extract mirrored session cookie (removes Domain to use tenant domain) */
|
|
46
|
-
function getMirroredCookie(resp) {
|
|
47
|
-
const setCookie = resp.headers.get('set-cookie');
|
|
48
|
-
return setCookie?.includes('session_id=')
|
|
49
|
-
? setCookie.replace(/Domain=[^;]+;?\s*/i, '')
|
|
50
|
-
: null;
|
|
51
|
-
}
|
|
52
|
-
/** Check if redirect is safe (relative, no open redirect) */
|
|
53
|
-
function isSafeRedirect(url) {
|
|
54
|
-
return url.startsWith('/') && !url.startsWith('//');
|
|
55
|
-
}
|
|
56
|
-
/** Build response with standard headers */
|
|
57
|
-
function buildResponse(data, status, cookie) {
|
|
58
|
-
const headers = new Headers({
|
|
59
|
-
'Content-Type': 'application/json',
|
|
60
|
-
'Cache-Control': 'no-store, no-cache, must-revalidate, private'
|
|
61
|
-
});
|
|
62
|
-
if (cookie)
|
|
63
|
-
headers.set('Set-Cookie', cookie);
|
|
64
|
-
return new Response(JSON.stringify(data), { status, headers });
|
|
65
|
-
}
|
|
66
|
-
/** Handle GET requests (email verification, password reset links, etc.) */
|
|
67
|
-
export async function handleAuthGet(rawEndpoint, request, url, locals) {
|
|
68
|
-
// ASTRO 6 CHANGE: Environment variables accessed from 'cloudflare:workers' env directly.
|
|
69
|
-
const internalApiKey = env.INTERNAL_API_KEY || import.meta.env.INTERNAL_API_KEY;
|
|
70
|
-
if (!internalApiKey)
|
|
71
|
-
console.error('[auth-handler] INTERNAL_API_KEY not configured');
|
|
72
|
-
const endpoint = sanitizeEndpoint(rawEndpoint);
|
|
73
|
-
if (!endpoint)
|
|
74
|
-
return jsonError('Bad request', 400);
|
|
75
|
-
const apiUrl = new URL(`${CENTRAL_AUTH_ORIGIN}/auth/${endpoint}`);
|
|
76
|
-
url.searchParams.forEach((v, k) => apiUrl.searchParams.set(k, v));
|
|
77
|
-
let resp;
|
|
78
|
-
try {
|
|
79
|
-
resp = await fetchWithTimeout(apiUrl.toString(), {
|
|
80
|
-
method: 'GET',
|
|
81
|
-
headers: buildProxyHeaders(request, url.hostname, internalApiKey)
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
catch (e) {
|
|
85
|
-
if (e.name === 'AbortError')
|
|
86
|
-
return jsonError('Request timeout', 504);
|
|
87
|
-
throw e;
|
|
88
|
-
}
|
|
89
|
-
// Handle redirect
|
|
90
|
-
const location = resp.headers.get('Location');
|
|
91
|
-
if (resp.status === 302 && location && isSafeRedirect(location)) {
|
|
92
|
-
return new Response(null, { status: 302, headers: { Location: location } });
|
|
93
|
-
}
|
|
94
|
-
return buildResponse(await resp.json(), resp.status, getMirroredCookie(resp));
|
|
95
|
-
}
|
|
96
|
-
/** Handle POST requests (login, 2FA, password changes, etc.) */
|
|
97
|
-
export async function handleAuthPost(rawEndpoint, request, url, locals) {
|
|
98
|
-
if (!ALLOWED_AUTH_METHODS.includes(request.method))
|
|
99
|
-
return jsonError('Method not allowed', 405);
|
|
100
|
-
const contentLength = request.headers.get('content-length');
|
|
101
|
-
if (contentLength && parseInt(contentLength) > MAX_BODY_SIZE_BYTES)
|
|
102
|
-
return jsonError('Payload too large', 413);
|
|
103
|
-
// ASTRO 6 CHANGE: Environment variables accessed from 'cloudflare:workers' env directly.
|
|
104
|
-
const internalApiKey = env.INTERNAL_API_KEY || import.meta.env.INTERNAL_API_KEY;
|
|
105
|
-
if (!internalApiKey)
|
|
106
|
-
console.error('[auth-handler] INTERNAL_API_KEY not configured');
|
|
107
|
-
const endpoint = sanitizeEndpoint(rawEndpoint);
|
|
108
|
-
if (!endpoint)
|
|
109
|
-
return jsonError('Bad request', 400);
|
|
110
|
-
// Parse request body
|
|
111
|
-
let body = null;
|
|
112
|
-
try {
|
|
113
|
-
const text = await request.text();
|
|
114
|
-
if (text) {
|
|
115
|
-
JSON.parse(text);
|
|
116
|
-
body = text;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
return jsonError('Invalid JSON', 400);
|
|
121
|
-
}
|
|
122
|
-
let resp;
|
|
123
|
-
try {
|
|
124
|
-
resp = await fetchWithTimeout(`${CENTRAL_AUTH_ORIGIN}/auth/${endpoint}`, {
|
|
125
|
-
method: 'POST',
|
|
126
|
-
headers: buildProxyHeaders(request, url.hostname, internalApiKey),
|
|
127
|
-
body
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
if (e.name === 'AbortError')
|
|
132
|
-
return jsonError('Request timeout', 504);
|
|
133
|
-
throw e;
|
|
134
|
-
}
|
|
135
|
-
const data = await resp.json();
|
|
136
|
-
const cookie = getMirroredCookie(resp);
|
|
137
|
-
// Handle HTTP redirect
|
|
138
|
-
const location = resp.headers.get('Location');
|
|
139
|
-
if (resp.status === 302 && location && isSafeRedirect(location)) {
|
|
140
|
-
return new Response(null, { status: 302, headers: { Location: location } });
|
|
141
|
-
}
|
|
142
|
-
// Handle JSON redirect (loginsuccessredirect)
|
|
143
|
-
const redirectUrl = data.loginsuccessredirect;
|
|
144
|
-
if (typeof redirectUrl === 'string' && isSafeRedirect(redirectUrl)) {
|
|
145
|
-
return new Response(null, {
|
|
146
|
-
status: 302,
|
|
147
|
-
headers: { Location: redirectUrl, ...(cookie && { 'Set-Cookie': cookie }) }
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
// Remove sensitive data
|
|
151
|
-
delete data.sessionToken;
|
|
152
|
-
delete data.maxAgeSeconds;
|
|
153
|
-
return buildResponse(data, resp.status, cookie);
|
|
154
|
-
}
|
package/dist/auth.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { SessionValidationResult } from './types';
|
|
2
|
-
/** Validate session with Mindful Auth central service */
|
|
3
|
-
export declare function validateSession(request: Request, tenantDomain: string, pathname: string, internalApiKey: string): Promise<SessionValidationResult>;
|
|
4
|
-
/** Validate memberid in URL matches session (or just check structure if sessionRecordId is null) */
|
|
5
|
-
export declare function validateMemberIdInUrl(pathname: string, sessionRecordId: string | null): {
|
|
6
|
-
valid: boolean;
|
|
7
|
-
strippedPathname?: string;
|
|
8
|
-
};
|
|
9
|
-
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD,yDAAyD;AACzD,wBAAsB,eAAe,CACjC,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,uBAAuB,CAAC,CAsClC;AAED,oGAAoG;AACpG,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,CAerI"}
|
package/dist/auth.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// Authentication and session validation for Mindful Auth
|
|
2
|
-
import { CENTRAL_AUTH_ORIGIN, SESSION_VALIDATION_TIMEOUT_MS } from './config';
|
|
3
|
-
/** Validate session with Mindful Auth central service */
|
|
4
|
-
export async function validateSession(request, tenantDomain, pathname, internalApiKey) {
|
|
5
|
-
const sessionId = request.headers.get('Cookie')?.match(/session_id=([^;]+)/)?.[1];
|
|
6
|
-
if (!sessionId)
|
|
7
|
-
return { valid: false, reason: 'no-session' };
|
|
8
|
-
const controller = new AbortController();
|
|
9
|
-
const timeoutId = setTimeout(() => controller.abort(), SESSION_VALIDATION_TIMEOUT_MS);
|
|
10
|
-
try {
|
|
11
|
-
const clientIp = request.headers.get('cf-connecting-ip') ||
|
|
12
|
-
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
13
|
-
request.headers.get('x-real-ip');
|
|
14
|
-
const resp = await fetch(`${CENTRAL_AUTH_ORIGIN}/auth/validate-session`, {
|
|
15
|
-
method: 'POST',
|
|
16
|
-
headers: {
|
|
17
|
-
'Content-Type': 'application/json',
|
|
18
|
-
'X-Tenant-Domain': tenantDomain,
|
|
19
|
-
'X-Internal-Api-Key': internalApiKey,
|
|
20
|
-
...(clientIp && { 'X-Original-Client-IP': clientIp }),
|
|
21
|
-
...(request.headers.get('user-agent') && { 'User-Agent': request.headers.get('user-agent') })
|
|
22
|
-
},
|
|
23
|
-
body: JSON.stringify({ sessionId, requestedUrl: pathname }),
|
|
24
|
-
signal: controller.signal
|
|
25
|
-
});
|
|
26
|
-
const result = await resp.json();
|
|
27
|
-
if (!result.valid)
|
|
28
|
-
return { valid: false, reason: 'invalid-session' };
|
|
29
|
-
const recordId = result.data?.sub || result.recordId;
|
|
30
|
-
if (!recordId)
|
|
31
|
-
return { valid: false, reason: 'invalid-record-id' };
|
|
32
|
-
return { valid: true, recordId };
|
|
33
|
-
}
|
|
34
|
-
catch (e) {
|
|
35
|
-
console.error('[auth] Session validation failed:', e.message);
|
|
36
|
-
return { valid: false, reason: 'error' };
|
|
37
|
-
}
|
|
38
|
-
finally {
|
|
39
|
-
clearTimeout(timeoutId);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/** Validate memberid in URL matches session (or just check structure if sessionRecordId is null) */
|
|
43
|
-
export function validateMemberIdInUrl(pathname, sessionRecordId) {
|
|
44
|
-
const segments = pathname.split('/').filter(Boolean);
|
|
45
|
-
if (segments.length === 0)
|
|
46
|
-
return { valid: false };
|
|
47
|
-
const urlMemberId = segments[0];
|
|
48
|
-
// Pre-check mode: just validate URL has memberid
|
|
49
|
-
if (sessionRecordId === null) {
|
|
50
|
-
return { valid: !!urlMemberId, strippedPathname: pathname };
|
|
51
|
-
}
|
|
52
|
-
// Validate memberid matches session
|
|
53
|
-
if (urlMemberId !== sessionRecordId)
|
|
54
|
-
return { valid: false };
|
|
55
|
-
return { valid: true, strippedPathname: '/' + segments.slice(1).join('/') };
|
|
56
|
-
}
|
package/dist/config.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export declare const CENTRAL_AUTH_ORIGIN = "https://api.mindfulauth.com";
|
|
2
|
-
export declare const CDN_ORIGIN = "https://cdn.mindfulauth.com";
|
|
3
|
-
export declare const ALLOWED_AUTH_METHODS: string[];
|
|
4
|
-
export declare const MAX_BODY_SIZE_BYTES = 1048576;
|
|
5
|
-
export declare const AUTH_PROXY_TIMEOUT_MS = 15000;
|
|
6
|
-
export declare const SESSION_VALIDATION_TIMEOUT_MS = 10000;
|
|
7
|
-
/**
|
|
8
|
-
* Returns the combined list of assets to skip session validation for.
|
|
9
|
-
* Merges defaults with any custom assets configured via mauthSecurityConfig() in astro.config.mjs.
|
|
10
|
-
*/
|
|
11
|
-
export declare function GET_SKIP_ASSETS(): string[];
|
|
12
|
-
/**
|
|
13
|
-
* Configure Mindful Auth security settings including custom skip assets.
|
|
14
|
-
* Call in astro.config.mjs and pass the result to getMauthViteDefines().
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* // astro.config.mjs
|
|
18
|
-
* const mauthCfg = mauthSecurityConfig({
|
|
19
|
-
* skipAssets: ['/sitemap.xml', '/manifest.webmanifest']
|
|
20
|
-
* });
|
|
21
|
-
*
|
|
22
|
-
* export default defineConfig({
|
|
23
|
-
* vite: { define: getMauthViteDefines(mauthCfg) }
|
|
24
|
-
* });
|
|
25
|
-
*/
|
|
26
|
-
export declare function mauthSecurityConfig(options?: {
|
|
27
|
-
skipAssets?: string[];
|
|
28
|
-
}): typeof options;
|
|
29
|
-
/**
|
|
30
|
-
* Converts the result of mauthSecurityConfig() into Vite define entries.
|
|
31
|
-
* Pass the output to vite.define in astro.config.mjs so custom values are
|
|
32
|
-
* baked into the SSR bundle at build time.
|
|
33
|
-
*/
|
|
34
|
-
export declare function getMauthViteDefines(options?: {
|
|
35
|
-
skipAssets?: string[];
|
|
36
|
-
}): Record<string, string>;
|
|
37
|
-
export declare const PUBLIC_ROUTES: string[];
|
|
38
|
-
export declare const PUBLIC_PREFIXES: string[];
|
|
39
|
-
/**
|
|
40
|
-
* Returns security headers to be added to every SSR response.
|
|
41
|
-
* CSP (Content-Security-Policy) for script-src and style-src is handled by
|
|
42
|
-
* Astro 6's native security.csp in astro.config.mjs using hashes.
|
|
43
|
-
* The remaining headers here cover transport security, framing, and permissions.
|
|
44
|
-
*
|
|
45
|
-
* Note: X-Frame-Options: SAMEORIGIN covers clickjacking protection
|
|
46
|
-
* (equivalent to CSP frame-ancestors 'self', which cannot be set via meta tag).
|
|
47
|
-
*/
|
|
48
|
-
export declare function GET_SECURITY_HEADERS(): Record<string, string>;
|
|
49
|
-
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AACjE,eAAO,MAAM,UAAU,gCAAgC,CAAC;AAGxD,eAAO,MAAM,oBAAoB,UAAkB,CAAC;AACpD,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAC3C,eAAO,MAAM,6BAA6B,QAAQ,CAAC;AAenD;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,EAAE,CAE1C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAC1C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,OAAO,OAAO,CAEjB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,CAAC,EAAE;IAC1C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIzB;AAID,eAAO,MAAM,aAAa,UAQzB,CAAC;AAIF,eAAO,MAAM,eAAe,UAO3B,CAAC;AAMF;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ7D"}
|