@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,269 @@
|
|
|
1
|
+
import type { SentinelLogger } from './logger.js';
|
|
2
|
+
export interface SentinelConfig {
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
/**
|
|
6
|
+
* When true, validateToken() performs a second GET /users/me request after
|
|
7
|
+
* POST /auth/validate to refresh DB-backed profile fields such as roles.
|
|
8
|
+
* Default: false. Keep disabled for high-volume per-request validation paths.
|
|
9
|
+
*/
|
|
10
|
+
hydrateUserProfile?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Timeout in milliseconds for each individual fetch call to the Sentinel API.
|
|
13
|
+
* Default: 8000 (8 seconds).
|
|
14
|
+
*/
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of retries for retryable errors (network timeout, HTTP 502/503/504).
|
|
18
|
+
* Non-retryable errors (400, 401, 403) are never retried.
|
|
19
|
+
* Default: 1.
|
|
20
|
+
*/
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
/** Optional structured logger. Falls back to console if not provided. */
|
|
23
|
+
logger?: SentinelLogger;
|
|
24
|
+
}
|
|
25
|
+
/** Base fields shared by all Sentinel API responses. */
|
|
26
|
+
export interface SentinelResponseBase {
|
|
27
|
+
success?: boolean;
|
|
28
|
+
code?: number;
|
|
29
|
+
message?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Response from POST /auth/login-passwordless */
|
|
33
|
+
export interface StartLoginResponse extends SentinelResponseBase {
|
|
34
|
+
transactionId?: string;
|
|
35
|
+
clientToken?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Response from POST /auth/poll */
|
|
38
|
+
export interface PollResponse extends SentinelResponseBase {
|
|
39
|
+
status?: 'pending' | 'completed' | 'expired';
|
|
40
|
+
}
|
|
41
|
+
/** Response from POST /auth/claim */
|
|
42
|
+
export interface ClaimResponse extends SentinelResponseBase {
|
|
43
|
+
access_token?: string;
|
|
44
|
+
refresh_token?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Response from POST /auth/refresh */
|
|
47
|
+
export interface RefreshResponse extends SentinelResponseBase {
|
|
48
|
+
access_token?: string;
|
|
49
|
+
refresh_token?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Response from POST /auth/validate */
|
|
52
|
+
export interface ValidateResponse extends SentinelResponseBase {
|
|
53
|
+
valid?: boolean;
|
|
54
|
+
payload?: DecodedToken;
|
|
55
|
+
user?: {
|
|
56
|
+
sub: string;
|
|
57
|
+
email: string;
|
|
58
|
+
roles: string[];
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Response from GET /users/me. */
|
|
62
|
+
export interface CurrentUserResponse extends SentinelResponseBase {
|
|
63
|
+
id?: string;
|
|
64
|
+
email?: string;
|
|
65
|
+
role?: string;
|
|
66
|
+
roles?: string[];
|
|
67
|
+
status?: string;
|
|
68
|
+
legalName?: string;
|
|
69
|
+
ocmeMemberNumber?: string;
|
|
70
|
+
walletAddress?: string;
|
|
71
|
+
aliasName?: string;
|
|
72
|
+
twitterAccount?: string;
|
|
73
|
+
youtubeAccount?: string;
|
|
74
|
+
cubicubeAddress?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Response from POST /auth/register */
|
|
77
|
+
export interface RegisterResponse extends SentinelResponseBase {
|
|
78
|
+
}
|
|
79
|
+
/** Response from POST /auth/verify */
|
|
80
|
+
export interface VerifyResponse extends SentinelResponseBase {
|
|
81
|
+
access_token?: string;
|
|
82
|
+
refresh_token?: string;
|
|
83
|
+
}
|
|
84
|
+
/** Response from POST /auth/complete-passwordless */
|
|
85
|
+
export interface CompletePasswordlessResponse extends SentinelResponseBase {
|
|
86
|
+
access_token?: string;
|
|
87
|
+
refresh_token?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @deprecated Use the specific typed response interfaces instead.
|
|
91
|
+
* Kept for backward compatibility — will be removed in v2.0.0.
|
|
92
|
+
*/
|
|
93
|
+
export interface SentinelResponse {
|
|
94
|
+
success?: boolean;
|
|
95
|
+
code?: number;
|
|
96
|
+
message?: string;
|
|
97
|
+
error?: string;
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
}
|
|
100
|
+
export interface DecodedToken {
|
|
101
|
+
sub: string;
|
|
102
|
+
email: string;
|
|
103
|
+
roles: string[];
|
|
104
|
+
exp: number;
|
|
105
|
+
iat: number;
|
|
106
|
+
}
|
|
107
|
+
export interface UserLocals {
|
|
108
|
+
isAuthenticated: boolean;
|
|
109
|
+
id: string;
|
|
110
|
+
email: string;
|
|
111
|
+
roles: string[];
|
|
112
|
+
}
|
|
113
|
+
export interface SentinelHandleOptions {
|
|
114
|
+
protectedRoutes?: string[];
|
|
115
|
+
loginRedirect?: string;
|
|
116
|
+
apiUrl?: string;
|
|
117
|
+
baseUrl?: string;
|
|
118
|
+
/** Optional structured logger for auth middleware. */
|
|
119
|
+
logger?: SentinelLogger;
|
|
120
|
+
}
|
|
121
|
+
/** Data passed to the onBeforeLogin hook. */
|
|
122
|
+
export interface BeforeLoginContext {
|
|
123
|
+
email: string;
|
|
124
|
+
rememberMe: boolean;
|
|
125
|
+
redirectTo?: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Return this from onBeforeLogin to reject the request.
|
|
129
|
+
*
|
|
130
|
+
* When `redirect` is set and is a safe relative path (starts with `/`, no
|
|
131
|
+
* protocol, no `//` / `\\` prefix), the SDK issues a `303 See Other` to that
|
|
132
|
+
* path instead of returning the JSON error body. Unsafe redirects are ignored
|
|
133
|
+
* and the JSON-reject path is used as a fallback.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* onBeforeLogin: async (_event, { email }) => {
|
|
138
|
+
* const gate = await isLoginAllowed(email);
|
|
139
|
+
* if (!gate.ok) {
|
|
140
|
+
* return {
|
|
141
|
+
* error: gate.reason,
|
|
142
|
+
* status: 403,
|
|
143
|
+
* redirect: gate.reason === 'waitlisted' ? '/waitlist' : '/unauthorized'
|
|
144
|
+
* };
|
|
145
|
+
* }
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export interface BeforeLoginReject {
|
|
150
|
+
error: string;
|
|
151
|
+
status?: number;
|
|
152
|
+
/**
|
|
153
|
+
* Optional branded destination the SDK will 303-redirect to instead of
|
|
154
|
+
* returning a JSON error. Must be a safe relative path (starts with `/`,
|
|
155
|
+
* no protocol, no `//` / `\\` prefix). Unsafe values fall back to the
|
|
156
|
+
* JSON-reject behaviour.
|
|
157
|
+
*/
|
|
158
|
+
redirect?: string;
|
|
159
|
+
}
|
|
160
|
+
/** Data passed to the onAfterLogin hook. */
|
|
161
|
+
export interface AfterLoginContext {
|
|
162
|
+
email: string;
|
|
163
|
+
transactionId: string;
|
|
164
|
+
}
|
|
165
|
+
/** Data passed to the onAfterClaim hook. */
|
|
166
|
+
export interface AfterClaimContext {
|
|
167
|
+
accessToken: string;
|
|
168
|
+
refreshToken: string;
|
|
169
|
+
redirectTo?: string;
|
|
170
|
+
}
|
|
171
|
+
/** Data passed to the onError hook. */
|
|
172
|
+
export interface ErrorContext {
|
|
173
|
+
handler: 'start-login' | 'poll' | 'claim' | 'logout' | 'refresh' | 'register' | 'verify';
|
|
174
|
+
error: unknown;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Context passed to the `resolveRedirect` hook after a successful token claim.
|
|
178
|
+
* Hosts use this to pick a post-login destination based on the user's roles,
|
|
179
|
+
* a requested redirect, or fall back to the configured default.
|
|
180
|
+
*/
|
|
181
|
+
export interface ResolveRedirectContext {
|
|
182
|
+
/** Newly authenticated user, decoded from the access token. */
|
|
183
|
+
user: UserLocals;
|
|
184
|
+
/** Access token just issued for this user. */
|
|
185
|
+
accessToken: string;
|
|
186
|
+
/** Whether the user opted into a longer session at login time. */
|
|
187
|
+
rememberMe: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* The redirect the client originally requested (from the `login_redirect_to`
|
|
190
|
+
* cookie, `?redirect=` on `/auth`, or the `redirectTo` body field).
|
|
191
|
+
* Already sanitised to a safe relative path by the SDK.
|
|
192
|
+
*/
|
|
193
|
+
requestedRedirect?: string;
|
|
194
|
+
/** The redirect that would be used if the resolver is absent. Always safe. */
|
|
195
|
+
defaultRedirect: string;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Context passed to the `logoutRedirect` function form, if the host wants to
|
|
199
|
+
* decide the post-logout landing at request time (e.g. send authenticated
|
|
200
|
+
* users to `/` and anonymous users to `/auth`).
|
|
201
|
+
*/
|
|
202
|
+
export interface LogoutRedirectContext {
|
|
203
|
+
/** User present when logout was invoked, if any (may be null for stale sessions). */
|
|
204
|
+
user: UserLocals | null;
|
|
205
|
+
}
|
|
206
|
+
export interface AuthHandlerOptions {
|
|
207
|
+
/**
|
|
208
|
+
* Login cookie TTL in seconds. Controls how long the login transaction
|
|
209
|
+
* cookies (transaction ID, client token, remember-me) persist.
|
|
210
|
+
* Default: 300 (5 minutes).
|
|
211
|
+
*/
|
|
212
|
+
loginCookieTtlSeconds?: number;
|
|
213
|
+
/**
|
|
214
|
+
* Path to redirect unauthenticated users (logout, claim errors).
|
|
215
|
+
* Default: '/auth'.
|
|
216
|
+
*/
|
|
217
|
+
loginRedirect?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Store a `login_redirect_to` cookie from the request body's `redirectTo`
|
|
220
|
+
* field, then use it on claim to redirect the user back to their original page.
|
|
221
|
+
* Default: true.
|
|
222
|
+
*/
|
|
223
|
+
redirectCookie?: boolean;
|
|
224
|
+
/** Optional structured logger for auth handlers. */
|
|
225
|
+
logger?: SentinelLogger;
|
|
226
|
+
/**
|
|
227
|
+
* Runs before the startLogin API call.
|
|
228
|
+
* Return `{ error, status? }` to reject the request.
|
|
229
|
+
*/
|
|
230
|
+
onBeforeLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: BeforeLoginContext) => Promise<BeforeLoginReject | void> | BeforeLoginReject | void;
|
|
231
|
+
/** Runs after a successful startLogin (transaction cookies set). */
|
|
232
|
+
onAfterLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterLoginContext) => Promise<void> | void;
|
|
233
|
+
/** Runs after successful token claim (session cookies set). */
|
|
234
|
+
onAfterClaim?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterClaimContext) => Promise<void> | void;
|
|
235
|
+
/** Runs on any handler error. */
|
|
236
|
+
onError?: (event: import('@sveltejs/kit').RequestEvent, ctx: ErrorContext) => Promise<void> | void;
|
|
237
|
+
/**
|
|
238
|
+
* Called after a successful token claim to decide the post-login destination.
|
|
239
|
+
* Return a safe relative path (starts with `/`, no protocol, no `//` prefix).
|
|
240
|
+
* If the resolver throws, returns a non-string, an empty string, or an unsafe
|
|
241
|
+
* path, the SDK falls back to `ctx.defaultRedirect`.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* resolveRedirect: async ({ user, requestedRedirect, defaultRedirect }) => {
|
|
246
|
+
* if (user.roles.includes('admin')) return '/admin';
|
|
247
|
+
* return requestedRedirect || defaultRedirect;
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
resolveRedirect?: (event: import('@sveltejs/kit').RequestEvent, ctx: ResolveRedirectContext) => Promise<string> | string;
|
|
252
|
+
/**
|
|
253
|
+
* Where `POST /logout` and `GET /logout` redirect after clearing cookies.
|
|
254
|
+
*
|
|
255
|
+
* - Omitted: falls back to `loginRedirect` (current behaviour, `/auth` by default).
|
|
256
|
+
* - String: treated as a literal safe relative path. Unsafe values fall back to `loginRedirect`.
|
|
257
|
+
* - Function: called with the pre-logout user context; return a safe relative
|
|
258
|
+
* path (async permitted). Throws / empty / unsafe → falls back to `loginRedirect`.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* ```ts
|
|
262
|
+
* logoutRedirect: '/'
|
|
263
|
+
* // or
|
|
264
|
+
* logoutRedirect: ({ user }) => user ? '/' : '/auth'
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
logoutRedirect?: string | ((event: import('@sveltejs/kit').RequestEvent, ctx: LogoutRedirectContext) => Promise<string> | string);
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"","sourcesContent":["// src/core/types.ts\nimport type { SentinelLogger } from './logger.js';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nexport interface SentinelConfig {\n\tapiUrl: string;\n\tbaseUrl: string;\n\t/**\n\t * When true, validateToken() performs a second GET /users/me request after\n\t * POST /auth/validate to refresh DB-backed profile fields such as roles.\n\t * Default: false. Keep disabled for high-volume per-request validation paths.\n\t */\n\thydrateUserProfile?: boolean;\n\t/**\n\t * Timeout in milliseconds for each individual fetch call to the Sentinel API.\n\t * Default: 8000 (8 seconds).\n\t */\n\ttimeoutMs?: number;\n\t/**\n\t * Maximum number of retries for retryable errors (network timeout, HTTP 502/503/504).\n\t * Non-retryable errors (400, 401, 403) are never retried.\n\t * Default: 1.\n\t */\n\tmaxRetries?: number;\n\t/** Optional structured logger. Falls back to console if not provided. */\n\tlogger?: SentinelLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Typed API Responses (replaces the old untyped SentinelResponse bag)\n// ---------------------------------------------------------------------------\n\n/** Base fields shared by all Sentinel API responses. */\nexport interface SentinelResponseBase {\n\tsuccess?: boolean;\n\tcode?: number;\n\tmessage?: string;\n\terror?: string;\n}\n\n/** Response from POST /auth/login-passwordless */\nexport interface StartLoginResponse extends SentinelResponseBase {\n\ttransactionId?: string;\n\tclientToken?: string;\n}\n\n/** Response from POST /auth/poll */\nexport interface PollResponse extends SentinelResponseBase {\n\tstatus?: 'pending' | 'completed' | 'expired';\n}\n\n/** Response from POST /auth/claim */\nexport interface ClaimResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/refresh */\nexport interface RefreshResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/validate */\nexport interface ValidateResponse extends SentinelResponseBase {\n\tvalid?: boolean;\n\tpayload?: DecodedToken;\n\tuser?: {\n\t\tsub: string;\n\t\temail: string;\n\t\troles: string[];\n\t};\n}\n\n/** Response from GET /users/me. */\nexport interface CurrentUserResponse extends SentinelResponseBase {\n\tid?: string;\n\temail?: string;\n\trole?: string;\n\troles?: string[];\n\tstatus?: string;\n\tlegalName?: string;\n\tocmeMemberNumber?: string;\n\twalletAddress?: string;\n\taliasName?: string;\n\ttwitterAccount?: string;\n\tyoutubeAccount?: string;\n\tcubicubeAddress?: string;\n}\n\n/** Response from POST /auth/register */\nexport interface RegisterResponse extends SentinelResponseBase {}\n\n/** Response from POST /auth/verify */\nexport interface VerifyResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/** Response from POST /auth/complete-passwordless */\nexport interface CompletePasswordlessResponse extends SentinelResponseBase {\n\taccess_token?: string;\n\trefresh_token?: string;\n}\n\n/**\n * @deprecated Use the specific typed response interfaces instead.\n * Kept for backward compatibility — will be removed in v2.0.0.\n */\nexport interface SentinelResponse {\n\tsuccess?: boolean;\n\tcode?: number;\n\tmessage?: string;\n\terror?: string;\n\t[key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// JWT / Auth\n// ---------------------------------------------------------------------------\n\nexport interface DecodedToken {\n\tsub: string;\n\temail: string;\n\troles: string[];\n\texp: number;\n\tiat: number;\n}\n\nexport interface UserLocals {\n\tisAuthenticated: boolean;\n\tid: string;\n\temail: string;\n\troles: string[];\n}\n\n// ---------------------------------------------------------------------------\n// SvelteKit Handle Options\n// ---------------------------------------------------------------------------\n\nexport interface SentinelHandleOptions {\n\tprotectedRoutes?: string[];\n\tloginRedirect?: string;\n\tapiUrl?: string;\n\tbaseUrl?: string;\n\t/** Optional structured logger for auth middleware. */\n\tlogger?: SentinelLogger;\n}\n\n// ---------------------------------------------------------------------------\n// Auth Handler Options & Lifecycle Hooks\n// ---------------------------------------------------------------------------\n\n/** Data passed to the onBeforeLogin hook. */\nexport interface BeforeLoginContext {\n\temail: string;\n\trememberMe: boolean;\n\tredirectTo?: string;\n}\n\n/**\n * Return this from onBeforeLogin to reject the request.\n *\n * When `redirect` is set and is a safe relative path (starts with `/`, no\n * protocol, no `//` / `\\\\` prefix), the SDK issues a `303 See Other` to that\n * path instead of returning the JSON error body. Unsafe redirects are ignored\n * and the JSON-reject path is used as a fallback.\n *\n * @example\n * ```ts\n * onBeforeLogin: async (_event, { email }) => {\n * const gate = await isLoginAllowed(email);\n * if (!gate.ok) {\n * return {\n * error: gate.reason,\n * status: 403,\n * redirect: gate.reason === 'waitlisted' ? '/waitlist' : '/unauthorized'\n * };\n * }\n * }\n * ```\n */\nexport interface BeforeLoginReject {\n\terror: string;\n\tstatus?: number;\n\t/**\n\t * Optional branded destination the SDK will 303-redirect to instead of\n\t * returning a JSON error. Must be a safe relative path (starts with `/`,\n\t * no protocol, no `//` / `\\\\` prefix). Unsafe values fall back to the\n\t * JSON-reject behaviour.\n\t */\n\tredirect?: string;\n}\n\n/** Data passed to the onAfterLogin hook. */\nexport interface AfterLoginContext {\n\temail: string;\n\ttransactionId: string;\n}\n\n/** Data passed to the onAfterClaim hook. */\nexport interface AfterClaimContext {\n\taccessToken: string;\n\trefreshToken: string;\n\tredirectTo?: string;\n}\n\n/** Data passed to the onError hook. */\nexport interface ErrorContext {\n\thandler: 'start-login' | 'poll' | 'claim' | 'logout' | 'refresh' | 'register' | 'verify';\n\terror: unknown;\n}\n\n/**\n * Context passed to the `resolveRedirect` hook after a successful token claim.\n * Hosts use this to pick a post-login destination based on the user's roles,\n * a requested redirect, or fall back to the configured default.\n */\nexport interface ResolveRedirectContext {\n\t/** Newly authenticated user, decoded from the access token. */\n\tuser: UserLocals;\n\t/** Access token just issued for this user. */\n\taccessToken: string;\n\t/** Whether the user opted into a longer session at login time. */\n\trememberMe: boolean;\n\t/**\n\t * The redirect the client originally requested (from the `login_redirect_to`\n\t * cookie, `?redirect=` on `/auth`, or the `redirectTo` body field).\n\t * Already sanitised to a safe relative path by the SDK.\n\t */\n\trequestedRedirect?: string;\n\t/** The redirect that would be used if the resolver is absent. Always safe. */\n\tdefaultRedirect: string;\n}\n\n/**\n * Context passed to the `logoutRedirect` function form, if the host wants to\n * decide the post-logout landing at request time (e.g. send authenticated\n * users to `/` and anonymous users to `/auth`).\n */\nexport interface LogoutRedirectContext {\n\t/** User present when logout was invoked, if any (may be null for stale sessions). */\n\tuser: UserLocals | null;\n}\n\nexport interface AuthHandlerOptions {\n\t/**\n\t * Login cookie TTL in seconds. Controls how long the login transaction\n\t * cookies (transaction ID, client token, remember-me) persist.\n\t * Default: 300 (5 minutes).\n\t */\n\tloginCookieTtlSeconds?: number;\n\n\t/**\n\t * Path to redirect unauthenticated users (logout, claim errors).\n\t * Default: '/auth'.\n\t */\n\tloginRedirect?: string;\n\n\t/**\n\t * Store a `login_redirect_to` cookie from the request body's `redirectTo`\n\t * field, then use it on claim to redirect the user back to their original page.\n\t * Default: true.\n\t */\n\tredirectCookie?: boolean;\n\n\t/** Optional structured logger for auth handlers. */\n\tlogger?: SentinelLogger;\n\n\t/**\n\t * Runs before the startLogin API call.\n\t * Return `{ error, status? }` to reject the request.\n\t */\n\tonBeforeLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: BeforeLoginContext) => Promise<BeforeLoginReject | void> | BeforeLoginReject | void;\n\n\t/** Runs after a successful startLogin (transaction cookies set). */\n\tonAfterLogin?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterLoginContext) => Promise<void> | void;\n\n\t/** Runs after successful token claim (session cookies set). */\n\tonAfterClaim?: (event: import('@sveltejs/kit').RequestEvent, ctx: AfterClaimContext) => Promise<void> | void;\n\n\t/** Runs on any handler error. */\n\tonError?: (event: import('@sveltejs/kit').RequestEvent, ctx: ErrorContext) => Promise<void> | void;\n\n\t/**\n\t * Called after a successful token claim to decide the post-login destination.\n\t * Return a safe relative path (starts with `/`, no protocol, no `//` prefix).\n\t * If the resolver throws, returns a non-string, an empty string, or an unsafe\n\t * path, the SDK falls back to `ctx.defaultRedirect`.\n\t *\n\t * @example\n\t * ```ts\n\t * resolveRedirect: async ({ user, requestedRedirect, defaultRedirect }) => {\n\t * if (user.roles.includes('admin')) return '/admin';\n\t * return requestedRedirect || defaultRedirect;\n\t * }\n\t * ```\n\t */\n\tresolveRedirect?: (\n\t\tevent: import('@sveltejs/kit').RequestEvent,\n\t\tctx: ResolveRedirectContext\n\t) => Promise<string> | string;\n\n\t/**\n\t * Where `POST /logout` and `GET /logout` redirect after clearing cookies.\n\t *\n\t * - Omitted: falls back to `loginRedirect` (current behaviour, `/auth` by default).\n\t * - String: treated as a literal safe relative path. Unsafe values fall back to `loginRedirect`.\n\t * - Function: called with the pre-logout user context; return a safe relative\n\t * path (async permitted). Throws / empty / unsafe → falls back to `loginRedirect`.\n\t *\n\t * @example\n\t * ```ts\n\t * logoutRedirect: '/'\n\t * // or\n\t * logoutRedirect: ({ user }) => user ? '/' : '/auth'\n\t * ```\n\t */\n\tlogoutRedirect?:\n\t\t| string\n\t\t| ((\n\t\t\t\tevent: import('@sveltejs/kit').RequestEvent,\n\t\t\t\tctx: LogoutRedirectContext\n\t\t ) => Promise<string> | string);\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended options for {@link authFetch} / {@link authFetchJson}.
|
|
3
|
+
*
|
|
4
|
+
* Adds a per-attempt request timeout on top of the standard `RequestInit`.
|
|
5
|
+
* The timeout is applied INDEPENDENTLY to the initial request and to the
|
|
6
|
+
* post-401-refresh retry, so a slow first attempt does not steal budget from
|
|
7
|
+
* the retry.
|
|
8
|
+
*/
|
|
9
|
+
export interface AuthFetchOptions extends RequestInit {
|
|
10
|
+
/**
|
|
11
|
+
* Per-attempt timeout in milliseconds.
|
|
12
|
+
*
|
|
13
|
+
* - Defaults to `60_000` (60 s) when omitted.
|
|
14
|
+
* - `0` or `Infinity` disables the internal timeout entirely (caller's
|
|
15
|
+
* own `signal` still applies).
|
|
16
|
+
* - The timeout is applied SEPARATELY to attempt #1 and to the post-401
|
|
17
|
+
* refresh retry — each gets a fresh budget.
|
|
18
|
+
* - Composed with `options.signal` via `AbortSignal.any([...])` so caller
|
|
19
|
+
* cancellation still works and a tighter caller signal wins.
|
|
20
|
+
* - Does NOT apply to the internal `/api/auth/refresh` sub-call.
|
|
21
|
+
*/
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Authenticated fetch wrapper with automatic token refresh.
|
|
26
|
+
*
|
|
27
|
+
* Applies a per-attempt timeout (default 60 s) to attempt #1 and to the
|
|
28
|
+
* post-401-refresh retry. Each attempt gets its OWN fresh timeout budget so
|
|
29
|
+
* a slow first attempt cannot starve the retry. See {@link AuthFetchOptions}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function authFetch(url: string | URL, options?: AuthFetchOptions): Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Authenticated fetch wrapper with JSON response parsing
|
|
34
|
+
*/
|
|
35
|
+
export declare function authFetchJson<T = any>(url: string | URL, options?: AuthFetchOptions): Promise<T>;
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sveltekit/client/index.ts"],"names":[],"mappings":"AAkDA;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACpD;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAgCD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC9B,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAqCnB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EAC1C,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,CAAC,CAAC,CAQZ"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/sveltekit/client/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Authenticated fetch wrapper with automatic token refresh.
|
|
4
|
+
*
|
|
5
|
+
* Use for:
|
|
6
|
+
* - Multipart/chunked file uploads
|
|
7
|
+
* - Sequential API calls with delays
|
|
8
|
+
* - Any scenario where token might expire between requests
|
|
9
|
+
*/
|
|
10
|
+
let isRefreshing = false;
|
|
11
|
+
let refreshPromise = null;
|
|
12
|
+
async function refreshAccessToken() {
|
|
13
|
+
if (isRefreshing && refreshPromise) {
|
|
14
|
+
return refreshPromise;
|
|
15
|
+
}
|
|
16
|
+
isRefreshing = true;
|
|
17
|
+
refreshPromise = (async () => {
|
|
18
|
+
try {
|
|
19
|
+
// The refresh sub-call is intentionally NOT wrapped in the caller's
|
|
20
|
+
// timeoutMs. It has its own well-behaved backend contract and we do
|
|
21
|
+
// not want a mid-refresh abort to leave the client with partially
|
|
22
|
+
// rotated tokens. If refresh itself hangs pathologically the
|
|
23
|
+
// browser/network stack will eventually fail it.
|
|
24
|
+
const response = await fetch('/api/auth/refresh', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
credentials: 'include'
|
|
27
|
+
});
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
const result = await response.json();
|
|
30
|
+
return result.success === true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Token refresh error:', error);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
isRefreshing = false;
|
|
40
|
+
refreshPromise = null;
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
return refreshPromise;
|
|
44
|
+
}
|
|
45
|
+
/** Default per-attempt timeout when `timeoutMs` is not provided. */
|
|
46
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
47
|
+
/**
|
|
48
|
+
* Build the effective per-attempt AbortSignal for a single fetch attempt.
|
|
49
|
+
*
|
|
50
|
+
* - When `timeoutMs` disables the timeout, we pass through the caller signal
|
|
51
|
+
* (or `undefined`).
|
|
52
|
+
* - When timeout is enabled and the caller supplied a signal, we compose them
|
|
53
|
+
* with `AbortSignal.any` so whichever aborts first wins.
|
|
54
|
+
*
|
|
55
|
+
* Node engine is >=20 so `AbortSignal.any` / `AbortSignal.timeout` are
|
|
56
|
+
* available; modern browsers (Chrome 116+, Firefox 124+, Safari 17.4+)
|
|
57
|
+
* also ship them.
|
|
58
|
+
*/
|
|
59
|
+
function buildAttemptSignal(callerSignal, timeoutMs) {
|
|
60
|
+
const timeoutDisabled = timeoutMs === 0 || !Number.isFinite(timeoutMs);
|
|
61
|
+
if (timeoutDisabled) {
|
|
62
|
+
return callerSignal ?? undefined;
|
|
63
|
+
}
|
|
64
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
65
|
+
if (!callerSignal) {
|
|
66
|
+
return timeoutSignal;
|
|
67
|
+
}
|
|
68
|
+
return AbortSignal.any([callerSignal, timeoutSignal]);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Authenticated fetch wrapper with automatic token refresh.
|
|
72
|
+
*
|
|
73
|
+
* Applies a per-attempt timeout (default 60 s) to attempt #1 and to the
|
|
74
|
+
* post-401-refresh retry. Each attempt gets its OWN fresh timeout budget so
|
|
75
|
+
* a slow first attempt cannot starve the retry. See {@link AuthFetchOptions}.
|
|
76
|
+
*/
|
|
77
|
+
export async function authFetch(url, options) {
|
|
78
|
+
const { timeoutMs, signal: callerSignal, ...restOptions } = options ?? {};
|
|
79
|
+
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
80
|
+
// Attempt #1 — fresh timeout budget composed with caller signal.
|
|
81
|
+
let response = await fetch(url, {
|
|
82
|
+
...restOptions,
|
|
83
|
+
credentials: 'include',
|
|
84
|
+
signal: buildAttemptSignal(callerSignal, effectiveTimeout)
|
|
85
|
+
});
|
|
86
|
+
if (response.status === 401) {
|
|
87
|
+
const refreshed = await refreshAccessToken();
|
|
88
|
+
if (refreshed) {
|
|
89
|
+
// Attempt #2 — brand new timeout budget. Do NOT reuse attempt #1's
|
|
90
|
+
// signal (that budget may already be nearly exhausted); build a
|
|
91
|
+
// fresh one instead. The caller's signal is still honored via
|
|
92
|
+
// AbortSignal.any so external cancellation continues to work.
|
|
93
|
+
response = await fetch(url, {
|
|
94
|
+
...restOptions,
|
|
95
|
+
credentials: 'include',
|
|
96
|
+
signal: buildAttemptSignal(callerSignal, effectiveTimeout)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// If refresh failed, do NOT hard-redirect to /auth.
|
|
100
|
+
// With Sentinel's rotating refresh tokens, concurrent requests can
|
|
101
|
+
// race: one wins the refresh (R1→R2) while the others fail because
|
|
102
|
+
// R1 is already invalidated. A hard redirect would log the user out
|
|
103
|
+
// even though the browser already has fresh cookies from the winning
|
|
104
|
+
// response. Instead, return the original 401 response and let the
|
|
105
|
+
// caller handle it — polling callers will silently retry on the next
|
|
106
|
+
// interval with the updated cookies, and SvelteKit's protected-route
|
|
107
|
+
// redirect handles truly expired sessions.
|
|
108
|
+
}
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Authenticated fetch wrapper with JSON response parsing
|
|
113
|
+
*/
|
|
114
|
+
export async function authFetchJson(url, options) {
|
|
115
|
+
const response = await authFetch(url, options);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
118
|
+
}
|
|
119
|
+
return response.json();
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sveltekit/client/index.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/client/index.ts\n\n/**\n * Authenticated fetch wrapper with automatic token refresh.\n *\n * Use for:\n * - Multipart/chunked file uploads\n * - Sequential API calls with delays\n * - Any scenario where token might expire between requests\n */\n\nlet isRefreshing = false;\nlet refreshPromise: Promise<boolean> | null = null;\n\nasync function refreshAccessToken(): Promise<boolean> {\n\tif (isRefreshing && refreshPromise) {\n\t\treturn refreshPromise;\n\t}\n\n\tisRefreshing = true;\n\trefreshPromise = (async () => {\n\t\ttry {\n\t\t\t// The refresh sub-call is intentionally NOT wrapped in the caller's\n\t\t\t// timeoutMs. It has its own well-behaved backend contract and we do\n\t\t\t// not want a mid-refresh abort to leave the client with partially\n\t\t\t// rotated tokens. If refresh itself hangs pathologically the\n\t\t\t// browser/network stack will eventually fail it.\n\t\t\tconst response = await fetch('/api/auth/refresh', {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tcredentials: 'include'\n\t\t\t});\n\n\t\t\tif (response.ok) {\n\t\t\t\tconst result = await response.json();\n\t\t\t\treturn result.success === true;\n\t\t\t}\n\n\t\t\treturn false;\n\t\t} catch (error) {\n\t\t\tconsole.error('Token refresh error:', error);\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tisRefreshing = false;\n\t\t\trefreshPromise = null;\n\t\t}\n\t})();\n\n\treturn refreshPromise;\n}\n\n/**\n * Extended options for {@link authFetch} / {@link authFetchJson}.\n *\n * Adds a per-attempt request timeout on top of the standard `RequestInit`.\n * The timeout is applied INDEPENDENTLY to the initial request and to the\n * post-401-refresh retry, so a slow first attempt does not steal budget from\n * the retry.\n */\nexport interface AuthFetchOptions extends RequestInit {\n\t/**\n\t * Per-attempt timeout in milliseconds.\n\t *\n\t * - Defaults to `60_000` (60 s) when omitted.\n\t * - `0` or `Infinity` disables the internal timeout entirely (caller's\n\t * own `signal` still applies).\n\t * - The timeout is applied SEPARATELY to attempt #1 and to the post-401\n\t * refresh retry — each gets a fresh budget.\n\t * - Composed with `options.signal` via `AbortSignal.any([...])` so caller\n\t * cancellation still works and a tighter caller signal wins.\n\t * - Does NOT apply to the internal `/api/auth/refresh` sub-call.\n\t */\n\ttimeoutMs?: number;\n}\n\n/** Default per-attempt timeout when `timeoutMs` is not provided. */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\n/**\n * Build the effective per-attempt AbortSignal for a single fetch attempt.\n *\n * - When `timeoutMs` disables the timeout, we pass through the caller signal\n * (or `undefined`).\n * - When timeout is enabled and the caller supplied a signal, we compose them\n * with `AbortSignal.any` so whichever aborts first wins.\n *\n * Node engine is >=20 so `AbortSignal.any` / `AbortSignal.timeout` are\n * available; modern browsers (Chrome 116+, Firefox 124+, Safari 17.4+)\n * also ship them.\n */\nfunction buildAttemptSignal(\n\tcallerSignal: AbortSignal | null | undefined,\n\ttimeoutMs: number\n): AbortSignal | undefined {\n\tconst timeoutDisabled = timeoutMs === 0 || !Number.isFinite(timeoutMs);\n\tif (timeoutDisabled) {\n\t\treturn callerSignal ?? undefined;\n\t}\n\tconst timeoutSignal = AbortSignal.timeout(timeoutMs);\n\tif (!callerSignal) {\n\t\treturn timeoutSignal;\n\t}\n\treturn AbortSignal.any([callerSignal, timeoutSignal]);\n}\n\n/**\n * Authenticated fetch wrapper with automatic token refresh.\n *\n * Applies a per-attempt timeout (default 60 s) to attempt #1 and to the\n * post-401-refresh retry. Each attempt gets its OWN fresh timeout budget so\n * a slow first attempt cannot starve the retry. See {@link AuthFetchOptions}.\n */\nexport async function authFetch(\n\turl: string | URL,\n\toptions?: AuthFetchOptions\n): Promise<Response> {\n\tconst { timeoutMs, signal: callerSignal, ...restOptions } = options ?? {};\n\tconst effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;\n\n\t// Attempt #1 — fresh timeout budget composed with caller signal.\n\tlet response = await fetch(url, {\n\t\t...restOptions,\n\t\tcredentials: 'include',\n\t\tsignal: buildAttemptSignal(callerSignal, effectiveTimeout)\n\t});\n\n\tif (response.status === 401) {\n\t\tconst refreshed = await refreshAccessToken();\n\n\t\tif (refreshed) {\n\t\t\t// Attempt #2 — brand new timeout budget. Do NOT reuse attempt #1's\n\t\t\t// signal (that budget may already be nearly exhausted); build a\n\t\t\t// fresh one instead. The caller's signal is still honored via\n\t\t\t// AbortSignal.any so external cancellation continues to work.\n\t\t\tresponse = await fetch(url, {\n\t\t\t\t...restOptions,\n\t\t\t\tcredentials: 'include',\n\t\t\t\tsignal: buildAttemptSignal(callerSignal, effectiveTimeout)\n\t\t\t});\n\t\t}\n\t\t// If refresh failed, do NOT hard-redirect to /auth.\n\t\t// With Sentinel's rotating refresh tokens, concurrent requests can\n\t\t// race: one wins the refresh (R1→R2) while the others fail because\n\t\t// R1 is already invalidated. A hard redirect would log the user out\n\t\t// even though the browser already has fresh cookies from the winning\n\t\t// response. Instead, return the original 401 response and let the\n\t\t// caller handle it — polling callers will silently retry on the next\n\t\t// interval with the updated cookies, and SvelteKit's protected-route\n\t\t// redirect handles truly expired sessions.\n\t}\n\n\treturn response;\n}\n\n/**\n * Authenticated fetch wrapper with JSON response parsing\n */\nexport async function authFetchJson<T = any>(\n\turl: string | URL,\n\toptions?: AuthFetchOptions\n): Promise<T> {\n\tconst response = await authFetch(url, options);\n\n\tif (!response.ok) {\n\t\tthrow new Error(`HTTP error! status: ${response.status}`);\n\t}\n\n\treturn response.json();\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Cookies } from '@sveltejs/kit';
|
|
2
|
+
export declare const COOKIE_NAMES: {
|
|
3
|
+
readonly ACCESS_TOKEN: "access_token";
|
|
4
|
+
readonly REFRESH_TOKEN: "refresh_token";
|
|
5
|
+
readonly VERIFY_ACCESS_TOKEN: "verify_access_token";
|
|
6
|
+
readonly VERIFY_REFRESH_TOKEN: "verify_refresh_token";
|
|
7
|
+
};
|
|
8
|
+
export declare const COOKIE_OPTIONS: {
|
|
9
|
+
readonly ACCESS_TOKEN: {
|
|
10
|
+
readonly path: "/";
|
|
11
|
+
readonly httpOnly: true;
|
|
12
|
+
readonly secure: true;
|
|
13
|
+
readonly sameSite: "lax";
|
|
14
|
+
readonly maxAge: number;
|
|
15
|
+
};
|
|
16
|
+
readonly REFRESH_TOKEN_LONG: {
|
|
17
|
+
readonly path: "/";
|
|
18
|
+
readonly httpOnly: true;
|
|
19
|
+
readonly secure: true;
|
|
20
|
+
readonly sameSite: "lax";
|
|
21
|
+
readonly maxAge: number;
|
|
22
|
+
};
|
|
23
|
+
readonly REFRESH_TOKEN_SHORT: {
|
|
24
|
+
readonly path: "/";
|
|
25
|
+
readonly httpOnly: true;
|
|
26
|
+
readonly secure: true;
|
|
27
|
+
readonly sameSite: "lax";
|
|
28
|
+
readonly maxAge: number;
|
|
29
|
+
};
|
|
30
|
+
readonly VERIFY_TOKEN: {
|
|
31
|
+
readonly path: "/auth";
|
|
32
|
+
readonly httpOnly: true;
|
|
33
|
+
readonly secure: true;
|
|
34
|
+
readonly sameSite: "lax";
|
|
35
|
+
readonly maxAge: 60;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Set session cookies (access + refresh tokens)
|
|
40
|
+
* @param rememberMe - If true, refresh token lasts 7 days. If false, 2 hours.
|
|
41
|
+
*/
|
|
42
|
+
export declare function setSessionCookies(cookies: Cookies, accessToken: string, refreshToken: string, rememberMe?: boolean): void;
|
|
43
|
+
/**
|
|
44
|
+
* Get session cookies (access + refresh tokens)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getSessionCookies(cookies: Cookies): {
|
|
47
|
+
accessToken: string | undefined;
|
|
48
|
+
refreshToken: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Clear all authentication cookies
|
|
52
|
+
*/
|
|
53
|
+
export declare function clearAuthCookies(cookies: Cookies): void;
|
|
54
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/sveltekit/cookies.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAO,MAAM,YAAY;;;;;CAKf,CAAC;AAEX,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BjB,CAAC;AAEX;;;GAGG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,OAAe,GACzB,IAAI,CAQN;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG;IACpD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;CACjC,CAKA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAGvD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const COOKIE_NAMES = {
|
|
2
|
+
ACCESS_TOKEN: 'access_token',
|
|
3
|
+
REFRESH_TOKEN: 'refresh_token',
|
|
4
|
+
VERIFY_ACCESS_TOKEN: 'verify_access_token',
|
|
5
|
+
VERIFY_REFRESH_TOKEN: 'verify_refresh_token'
|
|
6
|
+
};
|
|
7
|
+
export const COOKIE_OPTIONS = {
|
|
8
|
+
ACCESS_TOKEN: {
|
|
9
|
+
path: '/',
|
|
10
|
+
httpOnly: true,
|
|
11
|
+
secure: true,
|
|
12
|
+
sameSite: 'lax',
|
|
13
|
+
maxAge: 60 * 15 // 15 minutes
|
|
14
|
+
},
|
|
15
|
+
REFRESH_TOKEN_LONG: {
|
|
16
|
+
path: '/',
|
|
17
|
+
httpOnly: true,
|
|
18
|
+
secure: true,
|
|
19
|
+
sameSite: 'lax',
|
|
20
|
+
maxAge: 60 * 60 * 24 * 7 // 7 days (when "Remember Me" is checked)
|
|
21
|
+
},
|
|
22
|
+
REFRESH_TOKEN_SHORT: {
|
|
23
|
+
path: '/',
|
|
24
|
+
httpOnly: true,
|
|
25
|
+
secure: true,
|
|
26
|
+
sameSite: 'lax',
|
|
27
|
+
maxAge: 60 * 60 * 2 // 2 hours (when "Remember Me" is NOT checked)
|
|
28
|
+
},
|
|
29
|
+
VERIFY_TOKEN: {
|
|
30
|
+
path: '/auth',
|
|
31
|
+
httpOnly: true,
|
|
32
|
+
secure: true,
|
|
33
|
+
sameSite: 'lax',
|
|
34
|
+
maxAge: 60 // 1 minute — single-use, consumed immediately by /auth/verified
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Set session cookies (access + refresh tokens)
|
|
39
|
+
* @param rememberMe - If true, refresh token lasts 7 days. If false, 2 hours.
|
|
40
|
+
*/
|
|
41
|
+
export function setSessionCookies(cookies, accessToken, refreshToken, rememberMe = false) {
|
|
42
|
+
cookies.set(COOKIE_NAMES.ACCESS_TOKEN, accessToken, COOKIE_OPTIONS.ACCESS_TOKEN);
|
|
43
|
+
const refreshTokenOptions = rememberMe
|
|
44
|
+
? COOKIE_OPTIONS.REFRESH_TOKEN_LONG
|
|
45
|
+
: COOKIE_OPTIONS.REFRESH_TOKEN_SHORT;
|
|
46
|
+
cookies.set(COOKIE_NAMES.REFRESH_TOKEN, refreshToken, refreshTokenOptions);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get session cookies (access + refresh tokens)
|
|
50
|
+
*/
|
|
51
|
+
export function getSessionCookies(cookies) {
|
|
52
|
+
return {
|
|
53
|
+
accessToken: cookies.get(COOKIE_NAMES.ACCESS_TOKEN),
|
|
54
|
+
refreshToken: cookies.get(COOKIE_NAMES.REFRESH_TOKEN)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Clear all authentication cookies
|
|
59
|
+
*/
|
|
60
|
+
export function clearAuthCookies(cookies) {
|
|
61
|
+
cookies.delete(COOKIE_NAMES.ACCESS_TOKEN, { path: '/' });
|
|
62
|
+
cookies.delete(COOKIE_NAMES.REFRESH_TOKEN, { path: '/' });
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/sveltekit/cookies.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,YAAY,GAAG;IAC3B,YAAY,EAAE,cAAc;IAC5B,aAAa,EAAE,eAAe;IAC9B,mBAAmB,EAAE,qBAAqB;IAC1C,oBAAoB,EAAE,sBAAsB;CACnC,CAAC;AAEX,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,YAAY,EAAE;QACb,IAAI,EAAE,GAAG;QACT,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,KAAc;QACxB,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,aAAa;KAC7B;IACD,kBAAkB,EAAE;QACnB,IAAI,EAAE,GAAG;QACT,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,KAAc;QACxB,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,yCAAyC;KAClE;IACD,mBAAmB,EAAE;QACpB,IAAI,EAAE,GAAG;QACT,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,KAAc;QACxB,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,8CAA8C;KAClE;IACD,YAAY,EAAE;QACb,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE,IAAI;QACd,MAAM,EAAE,IAAI;QACZ,QAAQ,EAAE,KAAc;QACxB,MAAM,EAAE,EAAE,CAAC,gEAAgE;KAC3E;CACQ,CAAC;AAEX;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAChC,OAAgB,EAChB,WAAmB,EACnB,YAAoB,EACpB,aAAsB,KAAK;IAE3B,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,YAAY,EAAE,WAAW,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;IAEjF,MAAM,mBAAmB,GAAG,UAAU;QACrC,CAAC,CAAC,cAAc,CAAC,kBAAkB;QACnC,CAAC,CAAC,cAAc,CAAC,mBAAmB,CAAC;IAEtC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,aAAa,EAAE,YAAY,EAAE,mBAAmB,CAAC,CAAC;AAC5E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAIjD,OAAO;QACN,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,YAAY,CAAC;QACnD,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,aAAa,CAAC;KACrD,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAChD,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IACzD,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;AAC3D,CAAC","sourcesContent":["// src/sveltekit/cookies.ts\nimport type { Cookies } from '@sveltejs/kit';\n\nexport const COOKIE_NAMES = {\n\tACCESS_TOKEN: 'access_token',\n\tREFRESH_TOKEN: 'refresh_token',\n\tVERIFY_ACCESS_TOKEN: 'verify_access_token',\n\tVERIFY_REFRESH_TOKEN: 'verify_refresh_token'\n} as const;\n\nexport const COOKIE_OPTIONS = {\n\tACCESS_TOKEN: {\n\t\tpath: '/',\n\t\thttpOnly: true,\n\t\tsecure: true,\n\t\tsameSite: 'lax' as const,\n\t\tmaxAge: 60 * 15 // 15 minutes\n\t},\n\tREFRESH_TOKEN_LONG: {\n\t\tpath: '/',\n\t\thttpOnly: true,\n\t\tsecure: true,\n\t\tsameSite: 'lax' as const,\n\t\tmaxAge: 60 * 60 * 24 * 7 // 7 days (when \"Remember Me\" is checked)\n\t},\n\tREFRESH_TOKEN_SHORT: {\n\t\tpath: '/',\n\t\thttpOnly: true,\n\t\tsecure: true,\n\t\tsameSite: 'lax' as const,\n\t\tmaxAge: 60 * 60 * 2 // 2 hours (when \"Remember Me\" is NOT checked)\n\t},\n\tVERIFY_TOKEN: {\n\t\tpath: '/auth',\n\t\thttpOnly: true,\n\t\tsecure: true,\n\t\tsameSite: 'lax' as const,\n\t\tmaxAge: 60 // 1 minute — single-use, consumed immediately by /auth/verified\n\t}\n} as const;\n\n/**\n * Set session cookies (access + refresh tokens)\n * @param rememberMe - If true, refresh token lasts 7 days. If false, 2 hours.\n */\nexport function setSessionCookies(\n\tcookies: Cookies,\n\taccessToken: string,\n\trefreshToken: string,\n\trememberMe: boolean = false\n): void {\n\tcookies.set(COOKIE_NAMES.ACCESS_TOKEN, accessToken, COOKIE_OPTIONS.ACCESS_TOKEN);\n\n\tconst refreshTokenOptions = rememberMe\n\t\t? COOKIE_OPTIONS.REFRESH_TOKEN_LONG\n\t\t: COOKIE_OPTIONS.REFRESH_TOKEN_SHORT;\n\n\tcookies.set(COOKIE_NAMES.REFRESH_TOKEN, refreshToken, refreshTokenOptions);\n}\n\n/**\n * Get session cookies (access + refresh tokens)\n */\nexport function getSessionCookies(cookies: Cookies): {\n\taccessToken: string | undefined;\n\trefreshToken: string | undefined;\n} {\n\treturn {\n\t\taccessToken: cookies.get(COOKIE_NAMES.ACCESS_TOKEN),\n\t\trefreshToken: cookies.get(COOKIE_NAMES.REFRESH_TOKEN)\n\t};\n}\n\n/**\n * Clear all authentication cookies\n */\nexport function clearAuthCookies(cookies: Cookies): void {\n\tcookies.delete(COOKIE_NAMES.ACCESS_TOKEN, { path: '/' });\n\tcookies.delete(COOKIE_NAMES.REFRESH_TOKEN, { path: '/' });\n}\n\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Handle } from '@sveltejs/kit';
|
|
2
|
+
import type { SentinelHandleOptions, UserLocals } from '../core/types.js';
|
|
3
|
+
/** Locals shape set by the Sentinel auth middleware. */
|
|
4
|
+
export interface SentinelLocals {
|
|
5
|
+
user: UserLocals | null;
|
|
6
|
+
accessToken: string | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates a SvelteKit handle function with Sentinel authentication built-in.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Local JWT decode on every request (no per-request API call)
|
|
13
|
+
* - API-validated token refresh when expired or structurally invalid
|
|
14
|
+
* - Local expiration check (saves API calls)
|
|
15
|
+
* - Protected route redirection
|
|
16
|
+
* - Sets event.locals.user for all routes
|
|
17
|
+
* - Never clears cookies (only explicit logout does)
|
|
18
|
+
*/
|
|
19
|
+
export declare function createSentinelHandle(options?: SentinelHandleOptions): Handle;
|
|
20
|
+
//# sourceMappingURL=handle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../../src/sveltekit/handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,UAAU,EAAgC,MAAM,kBAAkB,CAAC;AAMxG,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CA0DhF"}
|