@nexartis/sentinel-sdk 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +215 -0
- package/dist/core/auth-utils.d.ts +31 -0
- package/dist/core/auth-utils.d.ts.map +1 -0
- package/dist/core/auth-utils.js +128 -0
- package/dist/core/auth-utils.js.map +1 -0
- package/dist/core/client.d.ts +22 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +137 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/errors.d.ts +47 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +69 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +30 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +54 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/types.d.ts +269 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/sveltekit/client/index.d.ts +36 -0
- package/dist/sveltekit/client/index.d.ts.map +1 -0
- package/dist/sveltekit/client/index.js +121 -0
- package/dist/sveltekit/client/index.js.map +1 -0
- package/dist/sveltekit/cookies.d.ts +54 -0
- package/dist/sveltekit/cookies.d.ts.map +1 -0
- package/dist/sveltekit/cookies.js +64 -0
- package/dist/sveltekit/cookies.js.map +1 -0
- package/dist/sveltekit/handle.d.ts +20 -0
- package/dist/sveltekit/handle.d.ts.map +1 -0
- package/dist/sveltekit/handle.js +146 -0
- package/dist/sveltekit/handle.js.map +1 -0
- package/dist/sveltekit/handlers/index.d.ts +29 -0
- package/dist/sveltekit/handlers/index.d.ts.map +1 -0
- package/dist/sveltekit/handlers/index.js +536 -0
- package/dist/sveltekit/handlers/index.js.map +1 -0
- package/dist/sveltekit/index.d.ts +6 -0
- package/dist/sveltekit/index.d.ts.map +1 -0
- package/dist/sveltekit/index.js +5 -0
- package/dist/sveltekit/index.js.map +1 -0
- package/dist/sveltekit/pages/index.d.ts +68 -0
- package/dist/sveltekit/pages/index.d.ts.map +1 -0
- package/dist/sveltekit/pages/index.js +207 -0
- package/dist/sveltekit/pages/index.js.map +1 -0
- package/dist/sveltekit/utils.d.ts +17 -0
- package/dist/sveltekit/utils.d.ts.map +1 -0
- package/dist/sveltekit/utils.js +34 -0
- package/dist/sveltekit/utils.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
// src/sveltekit/handlers/index.ts
|
|
2
|
+
import { json, redirect } from '@sveltejs/kit';
|
|
3
|
+
import { SentinelClient } from '../../core/client.js';
|
|
4
|
+
import { decodeTokenLocally } from '../../core/auth-utils.js';
|
|
5
|
+
import { createLogger } from '../../core/logger.js';
|
|
6
|
+
import { setSessionCookies, clearAuthCookies, COOKIE_NAMES, COOKIE_OPTIONS } from '../cookies.js';
|
|
7
|
+
import { getConfigFromPlatform, clearLoginCookies } from '../utils.js';
|
|
8
|
+
const DEFAULT_LOGIN_COOKIE_TTL_SECONDS = 300; // 5 minutes
|
|
9
|
+
const DEFAULT_LOGIN_REDIRECT = '/auth';
|
|
10
|
+
const MAX_EMAIL_LENGTH = 254; // RFC 5321
|
|
11
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
12
|
+
/**
|
|
13
|
+
* True if `path` is a safe relative path: starts with `/`, is not `//x` or
|
|
14
|
+
* `/\x` (protocol-relative), carries no scheme prefix (http:, javascript:, data:),
|
|
15
|
+
* and contains no whitespace or control characters (defence-in-depth against
|
|
16
|
+
* header-injection via CR/LF/tab smuggled through a `Location` header).
|
|
17
|
+
*/
|
|
18
|
+
function isSafeRelativePath(path) {
|
|
19
|
+
if (typeof path !== 'string' || path.length === 0)
|
|
20
|
+
return false;
|
|
21
|
+
if (!path.startsWith('/'))
|
|
22
|
+
return false;
|
|
23
|
+
if (/^[/\\]{2}/.test(path))
|
|
24
|
+
return false;
|
|
25
|
+
if (/^\/[a-z][a-z0-9+.\-]*:/i.test(path))
|
|
26
|
+
return false;
|
|
27
|
+
// Reject any ASCII control char (incl. CR/LF/TAB) or whitespace.
|
|
28
|
+
if (/[\s\u0000-\u001f\u007f]/.test(path))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/** Sanitise a redirect path to prevent open-redirect via env misconfiguration. */
|
|
33
|
+
function safeRedirectPath(raw) {
|
|
34
|
+
const path = (raw || '/').trim();
|
|
35
|
+
if (!isSafeRelativePath(path))
|
|
36
|
+
return '/';
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
/** Build UserLocals from a freshly-issued access token, or null if the token won't decode. */
|
|
40
|
+
function userLocalsFromAccessToken(accessToken) {
|
|
41
|
+
const decoded = decodeTokenLocally(accessToken);
|
|
42
|
+
if (!decoded)
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
isAuthenticated: true,
|
|
46
|
+
id: decoded.sub,
|
|
47
|
+
email: decoded.email,
|
|
48
|
+
roles: decoded.roles || []
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function buildLoginCookieOptions(ttlSeconds) {
|
|
52
|
+
return {
|
|
53
|
+
path: '/',
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
secure: true,
|
|
56
|
+
sameSite: 'lax',
|
|
57
|
+
maxAge: ttlSeconds
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Validate email: present, non-empty string, max 254 chars, basic format. */
|
|
61
|
+
function validateEmail(email) {
|
|
62
|
+
if (!email || typeof email !== 'string')
|
|
63
|
+
return 'Email is required';
|
|
64
|
+
const trimmed = email.trim();
|
|
65
|
+
if (!trimmed)
|
|
66
|
+
return 'Email is required';
|
|
67
|
+
if (trimmed.length > MAX_EMAIL_LENGTH)
|
|
68
|
+
return 'Email exceeds maximum length';
|
|
69
|
+
if (!EMAIL_REGEX.test(trimmed))
|
|
70
|
+
return 'Invalid email format';
|
|
71
|
+
return null; // valid
|
|
72
|
+
}
|
|
73
|
+
// Start Login Handler
|
|
74
|
+
async function handleStartLogin(event, resolved) {
|
|
75
|
+
const { loginCookieOptions, log, opts } = resolved;
|
|
76
|
+
try {
|
|
77
|
+
const body = await event.request.json();
|
|
78
|
+
const { email, rememberMe, redirectTo } = body;
|
|
79
|
+
// Built-in basic email validation
|
|
80
|
+
const emailError = validateEmail(email);
|
|
81
|
+
if (emailError) {
|
|
82
|
+
return json({ success: false, error: emailError }, { status: 400 });
|
|
83
|
+
}
|
|
84
|
+
const trimmedEmail = email.trim();
|
|
85
|
+
// onBeforeLogin hook — custom validation, rate limiting, etc.
|
|
86
|
+
if (opts.onBeforeLogin) {
|
|
87
|
+
const rejection = await opts.onBeforeLogin(event, {
|
|
88
|
+
email: trimmedEmail,
|
|
89
|
+
rememberMe: !!rememberMe,
|
|
90
|
+
redirectTo
|
|
91
|
+
});
|
|
92
|
+
if (rejection) {
|
|
93
|
+
// 1.5.0: if the rejection carries a safe relative `redirect`, 303
|
|
94
|
+
// to that branded destination instead of returning a JSON error.
|
|
95
|
+
// 303 (See Other) converts the POST into a GET on the client.
|
|
96
|
+
if (rejection.redirect && isSafeRelativePath(rejection.redirect)) {
|
|
97
|
+
clearLoginCookies(event.cookies);
|
|
98
|
+
return new Response(null, {
|
|
99
|
+
status: 303,
|
|
100
|
+
headers: { Location: rejection.redirect }
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (rejection.redirect) {
|
|
104
|
+
log.warn('onBeforeLogin returned unsafe redirect — falling back to JSON', {
|
|
105
|
+
redirect: rejection.redirect
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// Clear stale login cookies on JSON reject so a rejected login never
|
|
109
|
+
// leaves a half-open transaction behind. Mirrors the 303 path above.
|
|
110
|
+
clearLoginCookies(event.cookies);
|
|
111
|
+
return json({ success: false, error: rejection.error }, { status: rejection.status ?? 400 });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const config = getConfigFromPlatform(event.platform);
|
|
115
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
116
|
+
const result = await sentinel.startLogin(trimmedEmail);
|
|
117
|
+
// cookiesSet requires both a success signal AND valid tokens to prevent
|
|
118
|
+
// setting undefined/null cookie values if the API response is malformed.
|
|
119
|
+
const cookiesSet = Boolean((result.code === 0 || result.success) &&
|
|
120
|
+
result.transactionId &&
|
|
121
|
+
result.clientToken);
|
|
122
|
+
if (cookiesSet) {
|
|
123
|
+
event.cookies.set('login_transaction_id', result.transactionId, loginCookieOptions);
|
|
124
|
+
event.cookies.set('login_client_token', result.clientToken, loginCookieOptions);
|
|
125
|
+
event.cookies.set('login_remember_me', rememberMe ? 'true' : 'false', loginCookieOptions);
|
|
126
|
+
// Built-in redirectTo cookie — stores where to send user after login
|
|
127
|
+
if (resolved.redirectCookie && redirectTo) {
|
|
128
|
+
const safeTarget = safeRedirectPath(redirectTo);
|
|
129
|
+
if (safeTarget !== '/') {
|
|
130
|
+
event.cookies.set('login_redirect_to', safeTarget, loginCookieOptions);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// onAfterLogin hook — audit logging, analytics, etc.
|
|
134
|
+
if (opts.onAfterLogin) {
|
|
135
|
+
await opts.onAfterLogin(event, {
|
|
136
|
+
email: trimmedEmail,
|
|
137
|
+
transactionId: result.transactionId
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
log.error('Start login issue', { code: result.code, error: result.error });
|
|
143
|
+
clearLoginCookies(event.cookies);
|
|
144
|
+
}
|
|
145
|
+
// Always return same message to prevent user enumeration.
|
|
146
|
+
return json({
|
|
147
|
+
success: true,
|
|
148
|
+
message: 'If this email is registered, you will receive a magic link shortly.',
|
|
149
|
+
pollReady: cookiesSet
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
log.error('Start login error', { error: String(error) });
|
|
154
|
+
if (opts.onError)
|
|
155
|
+
await opts.onError(event, { handler: 'start-login', error });
|
|
156
|
+
clearLoginCookies(event.cookies);
|
|
157
|
+
return json({
|
|
158
|
+
success: true,
|
|
159
|
+
message: 'If this email is registered, you will receive a magic link shortly.',
|
|
160
|
+
pollReady: false
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Poll Handler
|
|
165
|
+
async function handlePoll(event, resolved) {
|
|
166
|
+
const { loginCookieOptions, log, opts } = resolved;
|
|
167
|
+
try {
|
|
168
|
+
const transactionId = event.cookies.get('login_transaction_id');
|
|
169
|
+
if (!transactionId) {
|
|
170
|
+
return json({ status: 'expired' }, { status: 400 });
|
|
171
|
+
}
|
|
172
|
+
const config = getConfigFromPlatform(event.platform);
|
|
173
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
174
|
+
const result = await sentinel.pollStatus(transactionId);
|
|
175
|
+
if (result.code === 0 || result.success) {
|
|
176
|
+
if (result.status === 'completed') {
|
|
177
|
+
event.cookies.set('claim_transaction_id', transactionId, {
|
|
178
|
+
...loginCookieOptions,
|
|
179
|
+
maxAge: 60 // 1 minute
|
|
180
|
+
});
|
|
181
|
+
return json({ status: 'completed', redirectTo: '/api/auth/claim' });
|
|
182
|
+
}
|
|
183
|
+
else if (result.status === 'pending') {
|
|
184
|
+
return json({ status: 'pending' });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
return json({ status: 'expired' });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
return json({ status: 'error' }, { status: 400 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
log.error('Poll status error', { error: String(error) });
|
|
196
|
+
if (opts.onError)
|
|
197
|
+
await opts.onError(event, { handler: 'poll', error });
|
|
198
|
+
return json({ status: 'error' }, { status: 500 });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Compute the built-in redirect candidate (before `resolveRedirect` runs).
|
|
203
|
+
* Order: login_redirect_to cookie → SENTINEL_POST_AUTH_REDIRECT env → '/'.
|
|
204
|
+
*
|
|
205
|
+
* Separated from `finalizePostClaimRedirect` so the `ResolveRedirectContext`
|
|
206
|
+
* can surface both the `requestedRedirect` and the `defaultRedirect` cleanly.
|
|
207
|
+
*/
|
|
208
|
+
function computeBuiltinClaimRedirect(event, resolved) {
|
|
209
|
+
let requested;
|
|
210
|
+
if (resolved.redirectCookie) {
|
|
211
|
+
const redirectTo = event.cookies.get('login_redirect_to');
|
|
212
|
+
if (redirectTo) {
|
|
213
|
+
// Clear it — single use, regardless of which redirect path wins.
|
|
214
|
+
event.cookies.delete('login_redirect_to', { path: '/' });
|
|
215
|
+
requested = safeRedirectPath(redirectTo);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const envRedirect = event.platform?.env;
|
|
219
|
+
const fallback = envRedirect?.SENTINEL_POST_AUTH_REDIRECT
|
|
220
|
+
? safeRedirectPath(envRedirect.SENTINEL_POST_AUTH_REDIRECT)
|
|
221
|
+
: '/';
|
|
222
|
+
return { requested, fallback };
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Finalise the post-claim destination:
|
|
226
|
+
* 1. Compute built-in requested + fallback candidates.
|
|
227
|
+
* 2. If `opts.resolveRedirect` is configured, call it; use the returned path
|
|
228
|
+
* when it is a safe relative path. Throws / empty / unsafe → fallback chain.
|
|
229
|
+
* 3. Otherwise prefer `requested` then `fallback`.
|
|
230
|
+
*/
|
|
231
|
+
async function finalizePostClaimRedirect(event, resolved, accessToken, rememberMe) {
|
|
232
|
+
const { requested, fallback } = computeBuiltinClaimRedirect(event, resolved);
|
|
233
|
+
const defaultRedirect = requested || fallback;
|
|
234
|
+
if (!resolved.opts.resolveRedirect) {
|
|
235
|
+
return defaultRedirect;
|
|
236
|
+
}
|
|
237
|
+
const user = userLocalsFromAccessToken(accessToken);
|
|
238
|
+
if (!user) {
|
|
239
|
+
resolved.log.warn('resolveRedirect skipped — access token failed to decode');
|
|
240
|
+
return defaultRedirect;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const candidate = await resolved.opts.resolveRedirect(event, {
|
|
244
|
+
user,
|
|
245
|
+
accessToken,
|
|
246
|
+
rememberMe,
|
|
247
|
+
requestedRedirect: requested,
|
|
248
|
+
defaultRedirect
|
|
249
|
+
});
|
|
250
|
+
if (typeof candidate === 'string' && isSafeRelativePath(candidate)) {
|
|
251
|
+
return candidate;
|
|
252
|
+
}
|
|
253
|
+
resolved.log.warn('resolveRedirect returned invalid path — using default', {
|
|
254
|
+
candidate: typeof candidate === 'string' ? candidate : typeof candidate
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
resolved.log.error('resolveRedirect threw — using default', { error: String(err) });
|
|
259
|
+
}
|
|
260
|
+
return defaultRedirect;
|
|
261
|
+
}
|
|
262
|
+
// Claim Handler (POST)
|
|
263
|
+
async function handleClaimPost(event, resolved) {
|
|
264
|
+
const { log, opts } = resolved;
|
|
265
|
+
try {
|
|
266
|
+
const transactionId = event.cookies.get('claim_transaction_id');
|
|
267
|
+
const clientToken = event.cookies.get('login_client_token');
|
|
268
|
+
const rememberMe = event.cookies.get('login_remember_me') === 'true';
|
|
269
|
+
if (!transactionId) {
|
|
270
|
+
return json({ success: false, error: 'No transaction ID' }, { status: 400 });
|
|
271
|
+
}
|
|
272
|
+
if (!clientToken) {
|
|
273
|
+
return json({ success: false, error: 'No client token' }, { status: 400 });
|
|
274
|
+
}
|
|
275
|
+
const config = getConfigFromPlatform(event.platform);
|
|
276
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
277
|
+
const result = await sentinel.claimTokens(transactionId, clientToken);
|
|
278
|
+
if ((result.code === 0 || result.success) && result.access_token && result.refresh_token) {
|
|
279
|
+
const postAuthRedirect = await finalizePostClaimRedirect(event, resolved, result.access_token, rememberMe);
|
|
280
|
+
setSessionCookies(event.cookies, result.access_token, result.refresh_token, rememberMe);
|
|
281
|
+
clearLoginCookies(event.cookies);
|
|
282
|
+
// onAfterClaim hook — user creation, profile sync, analytics
|
|
283
|
+
if (opts.onAfterClaim) {
|
|
284
|
+
await opts.onAfterClaim(event, {
|
|
285
|
+
accessToken: result.access_token,
|
|
286
|
+
refreshToken: result.refresh_token,
|
|
287
|
+
redirectTo: postAuthRedirect
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return json({ success: true, redirectTo: postAuthRedirect });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
return json({ success: false, error: result.error || 'Failed to claim tokens' }, { status: 400 });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
log.error('Claim tokens error', { error: String(error) });
|
|
298
|
+
if (opts.onError)
|
|
299
|
+
await opts.onError(event, { handler: 'claim', error });
|
|
300
|
+
return json({ success: false, error: 'Internal server error' }, { status: 500 });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Claim Handler (GET)
|
|
304
|
+
async function handleClaimGet(event, resolved) {
|
|
305
|
+
const { loginRedirect, opts } = resolved;
|
|
306
|
+
const transactionId = event.cookies.get('claim_transaction_id');
|
|
307
|
+
const clientToken = event.cookies.get('login_client_token');
|
|
308
|
+
const rememberMe = event.cookies.get('login_remember_me') === 'true';
|
|
309
|
+
if (!transactionId || !clientToken) {
|
|
310
|
+
throw redirect(302, `${loginRedirect}?error=no_transaction`);
|
|
311
|
+
}
|
|
312
|
+
const config = getConfigFromPlatform(event.platform);
|
|
313
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
314
|
+
const result = await sentinel.claimTokens(transactionId, clientToken);
|
|
315
|
+
if ((result.code === 0 || result.success) && result.access_token && result.refresh_token) {
|
|
316
|
+
const postAuthRedirect = await finalizePostClaimRedirect(event, resolved, result.access_token, rememberMe);
|
|
317
|
+
setSessionCookies(event.cookies, result.access_token, result.refresh_token, rememberMe);
|
|
318
|
+
clearLoginCookies(event.cookies);
|
|
319
|
+
if (opts.onAfterClaim) {
|
|
320
|
+
await opts.onAfterClaim(event, {
|
|
321
|
+
accessToken: result.access_token,
|
|
322
|
+
refreshToken: result.refresh_token,
|
|
323
|
+
redirectTo: postAuthRedirect
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
throw redirect(303, postAuthRedirect);
|
|
327
|
+
}
|
|
328
|
+
throw redirect(302, `${loginRedirect}?error=claim_failed`);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Resolve the post-logout destination.
|
|
332
|
+
*
|
|
333
|
+
* - `logoutRedirect` absent → fall back to `loginRedirect` (preserves 1.4.x behaviour).
|
|
334
|
+
* - `logoutRedirect` string → validate; unsafe → fall back to `loginRedirect`.
|
|
335
|
+
* - `logoutRedirect` function → await; validate; throw/empty/unsafe → fall back to `loginRedirect`.
|
|
336
|
+
*/
|
|
337
|
+
async function resolveLogoutDestination(event, resolved) {
|
|
338
|
+
const fallback = resolved.loginRedirect;
|
|
339
|
+
const opt = resolved.opts.logoutRedirect;
|
|
340
|
+
if (opt === undefined)
|
|
341
|
+
return fallback;
|
|
342
|
+
if (typeof opt === 'string') {
|
|
343
|
+
return isSafeRelativePath(opt) ? opt : fallback;
|
|
344
|
+
}
|
|
345
|
+
if (typeof opt === 'function') {
|
|
346
|
+
// Best-effort user context: decode the still-present access token before
|
|
347
|
+
// cookies get cleared. Callers typing user as possibly null lines up with
|
|
348
|
+
// the reality that the token may be missing or malformed.
|
|
349
|
+
const accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);
|
|
350
|
+
const user = accessToken ? userLocalsFromAccessToken(accessToken) : null;
|
|
351
|
+
try {
|
|
352
|
+
const candidate = await opt(event, { user });
|
|
353
|
+
if (typeof candidate === 'string' && isSafeRelativePath(candidate)) {
|
|
354
|
+
return candidate;
|
|
355
|
+
}
|
|
356
|
+
resolved.log.warn('logoutRedirect returned invalid path — using fallback', {
|
|
357
|
+
candidate: typeof candidate === 'string' ? candidate : typeof candidate
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
resolved.log.error('logoutRedirect threw — using fallback', { error: String(err) });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return fallback;
|
|
365
|
+
}
|
|
366
|
+
// Logout Handler
|
|
367
|
+
async function handleLogout(event, resolved) {
|
|
368
|
+
const destination = await resolveLogoutDestination(event, resolved);
|
|
369
|
+
clearAuthCookies(event.cookies);
|
|
370
|
+
throw redirect(303, destination);
|
|
371
|
+
}
|
|
372
|
+
// Refresh Handler
|
|
373
|
+
async function handleRefresh(event, resolved) {
|
|
374
|
+
const { log, opts } = resolved;
|
|
375
|
+
try {
|
|
376
|
+
const refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);
|
|
377
|
+
if (!refreshToken) {
|
|
378
|
+
return json({ success: false, error: 'No refresh token' }, { status: 401 });
|
|
379
|
+
}
|
|
380
|
+
const config = getConfigFromPlatform(event.platform);
|
|
381
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
382
|
+
const result = await sentinel.refreshToken(refreshToken);
|
|
383
|
+
if (result.code === 0 || result.success) {
|
|
384
|
+
if (result.access_token) {
|
|
385
|
+
event.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, result.access_token, COOKIE_OPTIONS.ACCESS_TOKEN);
|
|
386
|
+
if (result.refresh_token) {
|
|
387
|
+
event.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, result.refresh_token, COOKIE_OPTIONS.REFRESH_TOKEN_LONG);
|
|
388
|
+
}
|
|
389
|
+
return json({ success: true });
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
return json({ success: false, error: 'No access token returned' }, { status: 400 });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
log.warn('Token refresh failed - returning error without clearing cookies');
|
|
397
|
+
return json({ success: false, error: 'Refresh token expired' }, { status: 401 });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
log.error('Refresh token error', { error: String(error) });
|
|
402
|
+
if (opts.onError)
|
|
403
|
+
await opts.onError(event, { handler: 'refresh', error });
|
|
404
|
+
return json({ success: false, error: 'Internal server error' }, { status: 500 });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Register Handler
|
|
408
|
+
async function handleRegister(event, resolved) {
|
|
409
|
+
const { log, opts } = resolved;
|
|
410
|
+
try {
|
|
411
|
+
let email;
|
|
412
|
+
const contentType = event.request.headers.get('content-type') || '';
|
|
413
|
+
if (contentType.includes('application/json')) {
|
|
414
|
+
const body = await event.request.json();
|
|
415
|
+
email = body.email;
|
|
416
|
+
}
|
|
417
|
+
else if (contentType.includes('application/x-www-form-urlencoded') || contentType.includes('multipart/form-data')) {
|
|
418
|
+
const formData = await event.request.formData();
|
|
419
|
+
email = formData.get('email');
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const body = await event.request.json();
|
|
423
|
+
email = body.email;
|
|
424
|
+
}
|
|
425
|
+
// Use shared email validation
|
|
426
|
+
const emailError = validateEmail(email);
|
|
427
|
+
if (emailError) {
|
|
428
|
+
return json({ success: false, error: emailError }, { status: 400 });
|
|
429
|
+
}
|
|
430
|
+
const config = getConfigFromPlatform(event.platform);
|
|
431
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
432
|
+
const result = await sentinel.register(email.trim());
|
|
433
|
+
if (result.code !== 0 && !result.success) {
|
|
434
|
+
log.error('Registration issue', { code: result.code, error: result.error });
|
|
435
|
+
}
|
|
436
|
+
// Always return same message to prevent user enumeration
|
|
437
|
+
return json({
|
|
438
|
+
success: true,
|
|
439
|
+
message: 'If this email is not already registered, you will receive a verification link shortly.'
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
log.error('Registration error', { error: String(error) });
|
|
444
|
+
if (opts.onError)
|
|
445
|
+
await opts.onError(event, { handler: 'register', error });
|
|
446
|
+
return json({
|
|
447
|
+
success: true,
|
|
448
|
+
message: 'If this email is not already registered, you will receive a verification link shortly.'
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// Verify Email Handler
|
|
453
|
+
async function handleVerify(event, resolved) {
|
|
454
|
+
const { log, opts } = resolved;
|
|
455
|
+
try {
|
|
456
|
+
const { token } = await event.request.json();
|
|
457
|
+
if (!token) {
|
|
458
|
+
return json({ success: false, error: 'Verification token is required' }, { status: 400 });
|
|
459
|
+
}
|
|
460
|
+
const config = getConfigFromPlatform(event.platform);
|
|
461
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
462
|
+
const result = await sentinel.verifyEmail(token);
|
|
463
|
+
return json(result);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
log.error('Verify email error', { error: String(error) });
|
|
467
|
+
if (opts.onError)
|
|
468
|
+
await opts.onError(event, { handler: 'verify', error });
|
|
469
|
+
return json({ success: false, error: 'Internal server error' }, { status: 500 });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Creates all auth API route handlers for catch-all route pattern.
|
|
474
|
+
*
|
|
475
|
+
* @param options - Optional configuration for auth handlers.
|
|
476
|
+
*
|
|
477
|
+
* Usage:
|
|
478
|
+
* ```typescript
|
|
479
|
+
* // src/routes/api/auth/[...path]/+server.ts
|
|
480
|
+
* import { createAuthHandlers } from '@nexartis/sentinel-sdk/sveltekit/handlers';
|
|
481
|
+
* export const { GET, POST } = createAuthHandlers({
|
|
482
|
+
* loginCookieTtlSeconds: 1800,
|
|
483
|
+
* loginRedirect: '/auth',
|
|
484
|
+
* redirectCookie: true,
|
|
485
|
+
* onBeforeLogin: async (event, ctx) => {
|
|
486
|
+
* // custom rate limiting, validation, etc.
|
|
487
|
+
* },
|
|
488
|
+
* onAfterClaim: async (event, ctx) => {
|
|
489
|
+
* // create user record, sync profile, etc.
|
|
490
|
+
* }
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
export function createAuthHandlers(options = {}) {
|
|
495
|
+
const resolved = {
|
|
496
|
+
loginCookieOptions: buildLoginCookieOptions(options.loginCookieTtlSeconds ?? DEFAULT_LOGIN_COOKIE_TTL_SECONDS),
|
|
497
|
+
loginRedirect: options.loginRedirect ?? DEFAULT_LOGIN_REDIRECT,
|
|
498
|
+
redirectCookie: options.redirectCookie ?? true,
|
|
499
|
+
log: options.logger ?? createLogger('sentinel-handlers'),
|
|
500
|
+
opts: options
|
|
501
|
+
};
|
|
502
|
+
const POST = async (event) => {
|
|
503
|
+
const path = event.params.path || '';
|
|
504
|
+
switch (path) {
|
|
505
|
+
case 'start-login':
|
|
506
|
+
return handleStartLogin(event, resolved);
|
|
507
|
+
case 'poll':
|
|
508
|
+
return handlePoll(event, resolved);
|
|
509
|
+
case 'claim':
|
|
510
|
+
return handleClaimPost(event, resolved);
|
|
511
|
+
case 'logout':
|
|
512
|
+
return handleLogout(event, resolved);
|
|
513
|
+
case 'refresh':
|
|
514
|
+
return handleRefresh(event, resolved);
|
|
515
|
+
case 'register':
|
|
516
|
+
return handleRegister(event, resolved);
|
|
517
|
+
case 'verify':
|
|
518
|
+
return handleVerify(event, resolved);
|
|
519
|
+
default:
|
|
520
|
+
return json({ error: 'Not found' }, { status: 404 });
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const GET = async (event) => {
|
|
524
|
+
const path = event.params.path || '';
|
|
525
|
+
switch (path) {
|
|
526
|
+
case 'claim':
|
|
527
|
+
return handleClaimGet(event, resolved);
|
|
528
|
+
case 'logout':
|
|
529
|
+
return handleLogout(event, resolved);
|
|
530
|
+
default:
|
|
531
|
+
return json({ error: 'Not found' }, { status: 404 });
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
return { GET, POST };
|
|
535
|
+
}
|
|
536
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sveltekit/handlers/index.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/handlers/index.ts\nimport { json, redirect } from '@sveltejs/kit';\nimport type { RequestHandler, RequestEvent } from '@sveltejs/kit';\nimport { SentinelClient } from '../../core/client.js';\nimport type {\n\tAuthHandlerOptions,\n\tDecodedToken,\n\tUserLocals\n} from '../../core/types.js';\nimport { decodeTokenLocally } from '../../core/auth-utils.js';\nimport { createLogger, type SentinelLogger } from '../../core/logger.js';\nimport { setSessionCookies, clearAuthCookies, COOKIE_NAMES, COOKIE_OPTIONS } from '../cookies.js';\nimport { getConfigFromPlatform, clearLoginCookies } from '../utils.js';\n\nconst DEFAULT_LOGIN_COOKIE_TTL_SECONDS = 300; // 5 minutes\nconst DEFAULT_LOGIN_REDIRECT = '/auth';\nconst MAX_EMAIL_LENGTH = 254; // RFC 5321\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Resolved options with defaults applied. Computed once in createAuthHandlers. */\ninterface ResolvedOptions {\n\tloginCookieOptions: ReturnType<typeof buildLoginCookieOptions>;\n\tloginRedirect: string;\n\tredirectCookie: boolean;\n\tlog: SentinelLogger;\n\topts: AuthHandlerOptions;\n}\n\n/**\n * True if `path` is a safe relative path: starts with `/`, is not `//x` or\n * `/\\x` (protocol-relative), carries no scheme prefix (http:, javascript:, data:),\n * and contains no whitespace or control characters (defence-in-depth against\n * header-injection via CR/LF/tab smuggled through a `Location` header).\n */\nfunction isSafeRelativePath(path: string): boolean {\n\tif (typeof path !== 'string' || path.length === 0) return false;\n\tif (!path.startsWith('/')) return false;\n\tif (/^[/\\\\]{2}/.test(path)) return false;\n\tif (/^\\/[a-z][a-z0-9+.\\-]*:/i.test(path)) return false;\n\t// Reject any ASCII control char (incl. CR/LF/TAB) or whitespace.\n\tif (/[\\s\\u0000-\\u001f\\u007f]/.test(path)) return false;\n\treturn true;\n}\n\n/** Sanitise a redirect path to prevent open-redirect via env misconfiguration. */\nfunction safeRedirectPath(raw: string | undefined): string {\n\tconst path = (raw || '/').trim();\n\tif (!isSafeRelativePath(path)) return '/';\n\treturn path;\n}\n\n/** Build UserLocals from a freshly-issued access token, or null if the token won't decode. */\nfunction userLocalsFromAccessToken(accessToken: string): UserLocals | null {\n\tconst decoded: DecodedToken | null = decodeTokenLocally(accessToken);\n\tif (!decoded) return null;\n\treturn {\n\t\tisAuthenticated: true,\n\t\tid: decoded.sub,\n\t\temail: decoded.email,\n\t\troles: decoded.roles || []\n\t};\n}\n\nfunction buildLoginCookieOptions(ttlSeconds: number) {\n\treturn {\n\t\tpath: '/',\n\t\thttpOnly: true,\n\t\tsecure: true,\n\t\tsameSite: 'lax' as const,\n\t\tmaxAge: ttlSeconds\n\t};\n}\n\n/** Validate email: present, non-empty string, max 254 chars, basic format. */\nfunction validateEmail(email: unknown): string | null {\n\tif (!email || typeof email !== 'string') return 'Email is required';\n\tconst trimmed = email.trim();\n\tif (!trimmed) return 'Email is required';\n\tif (trimmed.length > MAX_EMAIL_LENGTH) return 'Email exceeds maximum length';\n\tif (!EMAIL_REGEX.test(trimmed)) return 'Invalid email format';\n\treturn null; // valid\n}\n\n// Start Login Handler\nasync function handleStartLogin(event: RequestEvent, resolved: ResolvedOptions): Promise<Response> {\n\tconst { loginCookieOptions, log, opts } = resolved;\n\ttry {\n\t\tconst body = await event.request.json();\n\t\tconst { email, rememberMe, redirectTo } = body;\n\n\t\t// Built-in basic email validation\n\t\tconst emailError = validateEmail(email);\n\t\tif (emailError) {\n\t\t\treturn json({ success: false, error: emailError }, { status: 400 });\n\t\t}\n\t\tconst trimmedEmail = (email as string).trim();\n\n\t\t// onBeforeLogin hook — custom validation, rate limiting, etc.\n\t\tif (opts.onBeforeLogin) {\n\t\t\tconst rejection = await opts.onBeforeLogin(event, {\n\t\t\t\temail: trimmedEmail,\n\t\t\t\trememberMe: !!rememberMe,\n\t\t\t\tredirectTo\n\t\t\t});\n\t\t\tif (rejection) {\n\t\t\t\t// 1.5.0: if the rejection carries a safe relative `redirect`, 303\n\t\t\t\t// to that branded destination instead of returning a JSON error.\n\t\t\t\t// 303 (See Other) converts the POST into a GET on the client.\n\t\t\t\tif (rejection.redirect && isSafeRelativePath(rejection.redirect)) {\n\t\t\t\t\tclearLoginCookies(event.cookies);\n\t\t\t\t\treturn new Response(null, {\n\t\t\t\t\t\tstatus: 303,\n\t\t\t\t\t\theaders: { Location: rejection.redirect }\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (rejection.redirect) {\n\t\t\t\t\tlog.warn('onBeforeLogin returned unsafe redirect — falling back to JSON', {\n\t\t\t\t\t\tredirect: rejection.redirect\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\t// Clear stale login cookies on JSON reject so a rejected login never\n\t\t\t\t// leaves a half-open transaction behind. Mirrors the 303 path above.\n\t\t\t\tclearLoginCookies(event.cookies);\n\t\t\t\treturn json({ success: false, error: rejection.error }, { status: rejection.status ?? 400 });\n\t\t\t}\n\t\t}\n\n\t\tconst config = getConfigFromPlatform(event.platform);\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.startLogin(trimmedEmail);\n\n\t\t// cookiesSet requires both a success signal AND valid tokens to prevent\n\t\t// setting undefined/null cookie values if the API response is malformed.\n\t\tconst cookiesSet = Boolean(\n\t\t\t(result.code === 0 || result.success) &&\n\t\t\tresult.transactionId &&\n\t\t\tresult.clientToken\n\t\t);\n\n\t\tif (cookiesSet) {\n\t\t\tevent.cookies.set('login_transaction_id', result.transactionId!, loginCookieOptions);\n\t\t\tevent.cookies.set('login_client_token', result.clientToken!, loginCookieOptions);\n\t\t\tevent.cookies.set('login_remember_me', rememberMe ? 'true' : 'false', loginCookieOptions);\n\n\t\t\t// Built-in redirectTo cookie — stores where to send user after login\n\t\t\tif (resolved.redirectCookie && redirectTo) {\n\t\t\t\tconst safeTarget = safeRedirectPath(redirectTo);\n\t\t\t\tif (safeTarget !== '/') {\n\t\t\t\t\tevent.cookies.set('login_redirect_to', safeTarget, loginCookieOptions);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// onAfterLogin hook — audit logging, analytics, etc.\n\t\t\tif (opts.onAfterLogin) {\n\t\t\t\tawait opts.onAfterLogin(event, {\n\t\t\t\t\temail: trimmedEmail,\n\t\t\t\t\ttransactionId: result.transactionId!\n\t\t\t\t});\n\t\t\t}\n\t\t} else {\n\t\t\tlog.error('Start login issue', { code: result.code, error: result.error });\n\t\t\tclearLoginCookies(event.cookies);\n\t\t}\n\n\t\t// Always return same message to prevent user enumeration.\n\t\treturn json({\n\t\t\tsuccess: true,\n\t\t\tmessage: 'If this email is registered, you will receive a magic link shortly.',\n\t\t\tpollReady: cookiesSet\n\t\t});\n\t} catch (error) {\n\t\tlog.error('Start login error', { error: String(error) });\n\t\tif (opts.onError) await opts.onError(event, { handler: 'start-login', error });\n\t\tclearLoginCookies(event.cookies);\n\t\treturn json({\n\t\t\tsuccess: true,\n\t\t\tmessage: 'If this email is registered, you will receive a magic link shortly.',\n\t\t\tpollReady: false\n\t\t});\n\t}\n}\n\n// Poll Handler\nasync function handlePoll(event: RequestEvent, resolved: ResolvedOptions): Promise<Response> {\n\tconst { loginCookieOptions, log, opts } = resolved;\n\ttry {\n\t\tconst transactionId = event.cookies.get('login_transaction_id');\n\n\t\tif (!transactionId) {\n\t\t\treturn json({ status: 'expired' }, { status: 400 });\n\t\t}\n\n\t\tconst config = getConfigFromPlatform(event.platform);\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.pollStatus(transactionId);\n\n\t\tif (result.code === 0 || result.success) {\n\t\t\tif (result.status === 'completed') {\n\t\t\t\tevent.cookies.set('claim_transaction_id', transactionId, {\n\t\t\t\t\t...loginCookieOptions,\n\t\t\t\t\tmaxAge: 60 // 1 minute\n\t\t\t\t});\n\n\t\t\t\treturn json({ status: 'completed', redirectTo: '/api/auth/claim' });\n\t\t\t} else if (result.status === 'pending') {\n\t\t\t\treturn json({ status: 'pending' });\n\t\t\t} else {\n\t\t\t\treturn json({ status: 'expired' });\n\t\t\t}\n\t\t} else {\n\t\t\treturn json({ status: 'error' }, { status: 400 });\n\t\t}\n\t} catch (error) {\n\t\tlog.error('Poll status error', { error: String(error) });\n\t\tif (opts.onError) await opts.onError(event, { handler: 'poll', error });\n\t\treturn json({ status: 'error' }, { status: 500 });\n\t}\n}\n\n/**\n * Compute the built-in redirect candidate (before `resolveRedirect` runs).\n * Order: login_redirect_to cookie → SENTINEL_POST_AUTH_REDIRECT env → '/'.\n *\n * Separated from `finalizePostClaimRedirect` so the `ResolveRedirectContext`\n * can surface both the `requestedRedirect` and the `defaultRedirect` cleanly.\n */\nfunction computeBuiltinClaimRedirect(\n\tevent: RequestEvent,\n\tresolved: ResolvedOptions\n): { requested?: string; fallback: string } {\n\tlet requested: string | undefined;\n\tif (resolved.redirectCookie) {\n\t\tconst redirectTo = event.cookies.get('login_redirect_to');\n\t\tif (redirectTo) {\n\t\t\t// Clear it — single use, regardless of which redirect path wins.\n\t\t\tevent.cookies.delete('login_redirect_to', { path: '/' });\n\t\t\trequested = safeRedirectPath(redirectTo);\n\t\t}\n\t}\n\tconst envRedirect = (event.platform as Record<string, unknown>)?.env as\n\t\t| Record<string, string>\n\t\t| undefined;\n\tconst fallback = envRedirect?.SENTINEL_POST_AUTH_REDIRECT\n\t\t? safeRedirectPath(envRedirect.SENTINEL_POST_AUTH_REDIRECT)\n\t\t: '/';\n\treturn { requested, fallback };\n}\n\n/**\n * Finalise the post-claim destination:\n * 1. Compute built-in requested + fallback candidates.\n * 2. If `opts.resolveRedirect` is configured, call it; use the returned path\n * when it is a safe relative path. Throws / empty / unsafe → fallback chain.\n * 3. Otherwise prefer `requested` then `fallback`.\n */\nasync function finalizePostClaimRedirect(\n\tevent: RequestEvent,\n\tresolved: ResolvedOptions,\n\taccessToken: string,\n\trememberMe: boolean\n): Promise<string> {\n\tconst { requested, fallback } = computeBuiltinClaimRedirect(event, resolved);\n\tconst defaultRedirect = requested || fallback;\n\n\tif (!resolved.opts.resolveRedirect) {\n\t\treturn defaultRedirect;\n\t}\n\n\tconst user = userLocalsFromAccessToken(accessToken);\n\tif (!user) {\n\t\tresolved.log.warn('resolveRedirect skipped — access token failed to decode');\n\t\treturn defaultRedirect;\n\t}\n\n\ttry {\n\t\tconst candidate = await resolved.opts.resolveRedirect(event, {\n\t\t\tuser,\n\t\t\taccessToken,\n\t\t\trememberMe,\n\t\t\trequestedRedirect: requested,\n\t\t\tdefaultRedirect\n\t\t});\n\t\tif (typeof candidate === 'string' && isSafeRelativePath(candidate)) {\n\t\t\treturn candidate;\n\t\t}\n\t\tresolved.log.warn('resolveRedirect returned invalid path — using default', {\n\t\t\tcandidate: typeof candidate === 'string' ? candidate : typeof candidate\n\t\t});\n\t} catch (err) {\n\t\tresolved.log.error('resolveRedirect threw — using default', { error: String(err) });\n\t}\n\treturn defaultRedirect;\n}\n\n// Claim Handler (POST)\nasync function handleClaimPost(event: RequestEvent, resolved: ResolvedOptions): Promise<Response> {\n\tconst { log, opts } = resolved;\n\ttry {\n\t\tconst transactionId = event.cookies.get('claim_transaction_id');\n\t\tconst clientToken = event.cookies.get('login_client_token');\n\t\tconst rememberMe = event.cookies.get('login_remember_me') === 'true';\n\n\t\tif (!transactionId) {\n\t\t\treturn json({ success: false, error: 'No transaction ID' }, { status: 400 });\n\t\t}\n\t\tif (!clientToken) {\n\t\t\treturn json({ success: false, error: 'No client token' }, { status: 400 });\n\t\t}\n\n\t\tconst config = getConfigFromPlatform(event.platform);\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.claimTokens(transactionId, clientToken);\n\n\t\tif ((result.code === 0 || result.success) && result.access_token && result.refresh_token) {\n\t\t\tconst postAuthRedirect = await finalizePostClaimRedirect(\n\t\t\t\tevent,\n\t\t\t\tresolved,\n\t\t\t\tresult.access_token,\n\t\t\t\trememberMe\n\t\t\t);\n\t\t\tsetSessionCookies(event.cookies, result.access_token, result.refresh_token, rememberMe);\n\t\t\tclearLoginCookies(event.cookies);\n\n\t\t\t// onAfterClaim hook — user creation, profile sync, analytics\n\t\t\tif (opts.onAfterClaim) {\n\t\t\t\tawait opts.onAfterClaim(event, {\n\t\t\t\t\taccessToken: result.access_token,\n\t\t\t\t\trefreshToken: result.refresh_token,\n\t\t\t\t\tredirectTo: postAuthRedirect\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn json({ success: true, redirectTo: postAuthRedirect });\n\t\t} else {\n\t\t\treturn json({ success: false, error: result.error || 'Failed to claim tokens' }, { status: 400 });\n\t\t}\n\t} catch (error) {\n\t\tlog.error('Claim tokens error', { error: String(error) });\n\t\tif (opts.onError) await opts.onError(event, { handler: 'claim', error });\n\t\treturn json({ success: false, error: 'Internal server error' }, { status: 500 });\n\t}\n}\n\n// Claim Handler (GET)\nasync function handleClaimGet(event: RequestEvent, resolved: ResolvedOptions): Promise<never> {\n\tconst { loginRedirect, opts } = resolved;\n\tconst transactionId = event.cookies.get('claim_transaction_id');\n\tconst clientToken = event.cookies.get('login_client_token');\n\tconst rememberMe = event.cookies.get('login_remember_me') === 'true';\n\n\tif (!transactionId || !clientToken) {\n\t\tthrow redirect(302, `${loginRedirect}?error=no_transaction`);\n\t}\n\n\tconst config = getConfigFromPlatform(event.platform);\n\tconst sentinel = new SentinelClient(config, event.fetch);\n\tconst result = await sentinel.claimTokens(transactionId, clientToken);\n\n\tif ((result.code === 0 || result.success) && result.access_token && result.refresh_token) {\n\t\tconst postAuthRedirect = await finalizePostClaimRedirect(\n\t\t\tevent,\n\t\t\tresolved,\n\t\t\tresult.access_token,\n\t\t\trememberMe\n\t\t);\n\t\tsetSessionCookies(event.cookies, result.access_token, result.refresh_token, rememberMe);\n\t\tclearLoginCookies(event.cookies);\n\n\t\tif (opts.onAfterClaim) {\n\t\t\tawait opts.onAfterClaim(event, {\n\t\t\t\taccessToken: result.access_token,\n\t\t\t\trefreshToken: result.refresh_token,\n\t\t\t\tredirectTo: postAuthRedirect\n\t\t\t});\n\t\t}\n\n\t\tthrow redirect(303, postAuthRedirect);\n\t}\n\n\tthrow redirect(302, `${loginRedirect}?error=claim_failed`);\n}\n\n/**\n * Resolve the post-logout destination.\n *\n * - `logoutRedirect` absent → fall back to `loginRedirect` (preserves 1.4.x behaviour).\n * - `logoutRedirect` string → validate; unsafe → fall back to `loginRedirect`.\n * - `logoutRedirect` function → await; validate; throw/empty/unsafe → fall back to `loginRedirect`.\n */\nasync function resolveLogoutDestination(\n\tevent: RequestEvent,\n\tresolved: ResolvedOptions\n): Promise<string> {\n\tconst fallback = resolved.loginRedirect;\n\tconst opt = resolved.opts.logoutRedirect;\n\tif (opt === undefined) return fallback;\n\n\tif (typeof opt === 'string') {\n\t\treturn isSafeRelativePath(opt) ? opt : fallback;\n\t}\n\n\tif (typeof opt === 'function') {\n\t\t// Best-effort user context: decode the still-present access token before\n\t\t// cookies get cleared. Callers typing user as possibly null lines up with\n\t\t// the reality that the token may be missing or malformed.\n\t\tconst accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);\n\t\tconst user = accessToken ? userLocalsFromAccessToken(accessToken) : null;\n\t\ttry {\n\t\t\tconst candidate = await opt(event, { user });\n\t\t\tif (typeof candidate === 'string' && isSafeRelativePath(candidate)) {\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t\tresolved.log.warn('logoutRedirect returned invalid path — using fallback', {\n\t\t\t\tcandidate: typeof candidate === 'string' ? candidate : typeof candidate\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tresolved.log.error('logoutRedirect threw — using fallback', { error: String(err) });\n\t\t}\n\t}\n\treturn fallback;\n}\n\n// Logout Handler\nasync function handleLogout(event: RequestEvent, resolved: ResolvedOptions): Promise<never> {\n\tconst destination = await resolveLogoutDestination(event, resolved);\n\tclearAuthCookies(event.cookies);\n\tthrow redirect(303, destination);\n}\n\n// Refresh Handler\nasync function handleRefresh(event: RequestEvent, resolved: ResolvedOptions): Promise<Response> {\n\tconst { log, opts } = resolved;\n\ttry {\n\t\tconst refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);\n\n\t\tif (!refreshToken) {\n\t\t\treturn json({ success: false, error: 'No refresh token' }, { status: 401 });\n\t\t}\n\n\t\tconst config = getConfigFromPlatform(event.platform);\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.refreshToken(refreshToken);\n\n\t\tif (result.code === 0 || result.success) {\n\t\t\tif (result.access_token) {\n\t\t\t\tevent.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, result.access_token, COOKIE_OPTIONS.ACCESS_TOKEN);\n\n\t\t\t\tif (result.refresh_token) {\n\t\t\t\t\tevent.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, result.refresh_token, COOKIE_OPTIONS.REFRESH_TOKEN_LONG);\n\t\t\t\t}\n\n\t\t\t\treturn json({ success: true });\n\t\t\t} else {\n\t\t\t\treturn json({ success: false, error: 'No access token returned' }, { status: 400 });\n\t\t\t}\n\t\t} else {\n\t\t\tlog.warn('Token refresh failed - returning error without clearing cookies');\n\t\t\treturn json({ success: false, error: 'Refresh token expired' }, { status: 401 });\n\t\t}\n\t} catch (error) {\n\t\tlog.error('Refresh token error', { error: String(error) });\n\t\tif (opts.onError) await opts.onError(event, { handler: 'refresh', error });\n\t\treturn json({ success: false, error: 'Internal server error' }, { status: 500 });\n\t}\n}\n\n// Register Handler\nasync function handleRegister(event: RequestEvent, resolved: ResolvedOptions): Promise<Response> {\n\tconst { log, opts } = resolved;\n\ttry {\n\t\tlet email: string;\n\t\tconst contentType = event.request.headers.get('content-type') || '';\n\n\t\tif (contentType.includes('application/json')) {\n\t\t\tconst body = await event.request.json();\n\t\t\temail = body.email;\n\t\t} else if (contentType.includes('application/x-www-form-urlencoded') || contentType.includes('multipart/form-data')) {\n\t\t\tconst formData = await event.request.formData();\n\t\t\temail = formData.get('email') as string;\n\t\t} else {\n\t\t\tconst body = await event.request.json();\n\t\t\temail = body.email;\n\t\t}\n\n\t\t// Use shared email validation\n\t\tconst emailError = validateEmail(email);\n\t\tif (emailError) {\n\t\t\treturn json({ success: false, error: emailError }, { status: 400 });\n\t\t}\n\n\t\tconst config = getConfigFromPlatform(event.platform);\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.register((email as string).trim());\n\n\t\tif (result.code !== 0 && !result.success) {\n\t\t\tlog.error('Registration issue', { code: result.code, error: result.error });\n\t\t}\n\n\t\t// Always return same message to prevent user enumeration\n\t\treturn json({\n\t\t\tsuccess: true,\n\t\t\tmessage: 'If this email is not already registered, you will receive a verification link shortly.'\n\t\t});\n\t} catch (error) {\n\t\tlog.error('Registration error', { error: String(error) });\n\t\tif (opts.onError) await opts.onError(event, { handler: 'register', error });\n\t\treturn json({\n\t\t\tsuccess: true,\n\t\t\tmessage: 'If this email is not already registered, you will receive a verification link shortly.'\n\t\t});\n\t}\n}\n\n// Verify Email Handler\nasync function handleVerify(event: RequestEvent, resolved: ResolvedOptions): Promise<Response> {\n\tconst { log, opts } = resolved;\n\ttry {\n\t\tconst { token } = await event.request.json();\n\n\t\tif (!token) {\n\t\t\treturn json({ success: false, error: 'Verification token is required' }, { status: 400 });\n\t\t}\n\n\t\tconst config = getConfigFromPlatform(event.platform);\n\t\tconst sentinel = new SentinelClient(config, event.fetch);\n\t\tconst result = await sentinel.verifyEmail(token);\n\n\t\treturn json(result);\n\t} catch (error) {\n\t\tlog.error('Verify email error', { error: String(error) });\n\t\tif (opts.onError) await opts.onError(event, { handler: 'verify', error });\n\t\treturn json({ success: false, error: 'Internal server error' }, { status: 500 });\n\t}\n}\n\n/**\n * Creates all auth API route handlers for catch-all route pattern.\n *\n * @param options - Optional configuration for auth handlers.\n *\n * Usage:\n * ```typescript\n * // src/routes/api/auth/[...path]/+server.ts\n * import { createAuthHandlers } from '@nexartis/sentinel-sdk/sveltekit/handlers';\n * export const { GET, POST } = createAuthHandlers({\n * loginCookieTtlSeconds: 1800,\n * loginRedirect: '/auth',\n * redirectCookie: true,\n * onBeforeLogin: async (event, ctx) => {\n * // custom rate limiting, validation, etc.\n * },\n * onAfterClaim: async (event, ctx) => {\n * // create user record, sync profile, etc.\n * }\n * });\n * ```\n */\nexport function createAuthHandlers(options: AuthHandlerOptions = {}): { GET: RequestHandler; POST: RequestHandler } {\n\tconst resolved: ResolvedOptions = {\n\t\tloginCookieOptions: buildLoginCookieOptions(options.loginCookieTtlSeconds ?? DEFAULT_LOGIN_COOKIE_TTL_SECONDS),\n\t\tloginRedirect: options.loginRedirect ?? DEFAULT_LOGIN_REDIRECT,\n\t\tredirectCookie: options.redirectCookie ?? true,\n\t\tlog: options.logger ?? createLogger('sentinel-handlers'),\n\t\topts: options\n\t};\n\n\tconst POST: RequestHandler = async (event) => {\n\t\tconst path = event.params.path || '';\n\n\t\tswitch (path) {\n\t\t\tcase 'start-login':\n\t\t\t\treturn handleStartLogin(event, resolved);\n\t\t\tcase 'poll':\n\t\t\t\treturn handlePoll(event, resolved);\n\t\t\tcase 'claim':\n\t\t\t\treturn handleClaimPost(event, resolved);\n\t\t\tcase 'logout':\n\t\t\t\treturn handleLogout(event, resolved);\n\t\t\tcase 'refresh':\n\t\t\t\treturn handleRefresh(event, resolved);\n\t\t\tcase 'register':\n\t\t\t\treturn handleRegister(event, resolved);\n\t\t\tcase 'verify':\n\t\t\t\treturn handleVerify(event, resolved);\n\t\t\tdefault:\n\t\t\t\treturn json({ error: 'Not found' }, { status: 404 });\n\t\t}\n\t};\n\n\tconst GET: RequestHandler = async (event) => {\n\t\tconst path = event.params.path || '';\n\n\t\tswitch (path) {\n\t\t\tcase 'claim':\n\t\t\t\treturn handleClaimGet(event, resolved);\n\t\t\tcase 'logout':\n\t\t\t\treturn handleLogout(event, resolved);\n\t\t\tdefault:\n\t\t\t\treturn json({ error: 'Not found' }, { status: 404 });\n\t\t}\n\t};\n\n\treturn { GET, POST };\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createSentinelHandle } from './handle.js';
|
|
2
|
+
export type { SentinelLocals } from './handle.js';
|
|
3
|
+
export { setSessionCookies, getSessionCookies, clearAuthCookies, COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';
|
|
4
|
+
export { handleMagicLink, handleVerifyEmail, handleVerified } from './pages/index.js';
|
|
5
|
+
export type { SentinelConfig, SentinelResponse, DecodedToken, UserLocals, SentinelHandleOptions, AuthHandlerOptions } from '../core/types.js';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sveltekit/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACnD,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACN,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EACX,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,qBAAqB,EACrB,kBAAkB,EAClB,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// src/sveltekit/index.ts
|
|
2
|
+
export { createSentinelHandle } from './handle.js';
|
|
3
|
+
export { setSessionCookies, getSessionCookies, clearAuthCookies, COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';
|
|
4
|
+
export { handleMagicLink, handleVerifyEmail, handleVerified } from './pages/index.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sveltekit/index.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EACN,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,MAAM,kBAAkB,CAAC","sourcesContent":["// src/sveltekit/index.ts\nexport { createSentinelHandle } from './handle.js';\nexport type { SentinelLocals } from './handle.js';\nexport {\n\tsetSessionCookies,\n\tgetSessionCookies,\n\tclearAuthCookies,\n\tCOOKIE_NAMES,\n\tCOOKIE_OPTIONS\n} from './cookies.js';\nexport {\n\thandleMagicLink,\n\thandleVerifyEmail,\n\thandleVerified\n} from './pages/index.js';\n\n// Re-export core types for convenience\nexport type {\n\tSentinelConfig,\n\tSentinelResponse,\n\tDecodedToken,\n\tUserLocals,\n\tSentinelHandleOptions,\n\tAuthHandlerOptions\n} from '../core/types.js';\n\n"]}
|