@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,146 @@
|
|
|
1
|
+
import { SentinelClient } from '../core/client.js';
|
|
2
|
+
import { isTokenExpired, decodeTokenLocally } from '../core/auth-utils.js';
|
|
3
|
+
import { createLogger } from '../core/logger.js';
|
|
4
|
+
import { COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';
|
|
5
|
+
/**
|
|
6
|
+
* Creates a SvelteKit handle function with Sentinel authentication built-in.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Local JWT decode on every request (no per-request API call)
|
|
10
|
+
* - API-validated token refresh when expired or structurally invalid
|
|
11
|
+
* - Local expiration check (saves API calls)
|
|
12
|
+
* - Protected route redirection
|
|
13
|
+
* - Sets event.locals.user for all routes
|
|
14
|
+
* - Never clears cookies (only explicit logout does)
|
|
15
|
+
*/
|
|
16
|
+
export function createSentinelHandle(options = {}) {
|
|
17
|
+
const { protectedRoutes = [], loginRedirect = '/auth' } = options;
|
|
18
|
+
const log = options.logger ?? createLogger('sentinel-handle');
|
|
19
|
+
return async ({ event, resolve }) => {
|
|
20
|
+
// Initialize locals with typed shape
|
|
21
|
+
const locals = event.locals;
|
|
22
|
+
locals.user = null;
|
|
23
|
+
locals.accessToken = null;
|
|
24
|
+
// Get config from environment or options (with request URL fallback)
|
|
25
|
+
const config = getConfig(options, event.platform, event.request.url);
|
|
26
|
+
const accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);
|
|
27
|
+
const refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);
|
|
28
|
+
if (accessToken) {
|
|
29
|
+
if (!isTokenExpired(accessToken)) {
|
|
30
|
+
const decoded = decodeTokenLocally(accessToken);
|
|
31
|
+
if (decoded) {
|
|
32
|
+
locals.user = createUserLocals(decoded);
|
|
33
|
+
locals.accessToken = accessToken;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
await tryRefreshToken(event, config, refreshToken, log);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (refreshToken) {
|
|
40
|
+
await tryRefreshToken(event, config, refreshToken, log);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (refreshToken) {
|
|
44
|
+
await tryRefreshToken(event, config, refreshToken, log);
|
|
45
|
+
}
|
|
46
|
+
// Protected routes check — redirect to login with return URL
|
|
47
|
+
const isProtectedRoute = protectedRoutes.some(route => event.url.pathname.startsWith(route));
|
|
48
|
+
if (isProtectedRoute && !locals.user) {
|
|
49
|
+
const returnPath = event.url.pathname + event.url.search;
|
|
50
|
+
const redirectUrl = `${loginRedirect}?redirect=${encodeURIComponent(returnPath)}`;
|
|
51
|
+
return new Response('Redirect', {
|
|
52
|
+
status: 302,
|
|
53
|
+
headers: { Location: redirectUrl }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const response = await resolve(event);
|
|
57
|
+
// Add security headers
|
|
58
|
+
response.headers.set('X-Frame-Options', 'DENY');
|
|
59
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
60
|
+
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
61
|
+
return response;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function getConfig(options, platform, requestUrl) {
|
|
65
|
+
let apiUrl = options.apiUrl || platform?.env?.SENTINEL_API_URL || '';
|
|
66
|
+
let baseUrl = options.baseUrl || platform?.env?.VITE_BASE_URL || '';
|
|
67
|
+
if (!apiUrl) {
|
|
68
|
+
throw new Error('SENTINEL_API_URL must be configured');
|
|
69
|
+
}
|
|
70
|
+
// Normalize: strip trailing slashes, then append /api if not already present
|
|
71
|
+
while (apiUrl.endsWith('/'))
|
|
72
|
+
apiUrl = apiUrl.slice(0, -1);
|
|
73
|
+
if (!apiUrl.endsWith('/api')) {
|
|
74
|
+
apiUrl = apiUrl + '/api';
|
|
75
|
+
}
|
|
76
|
+
// ARCH-003 fix: Derive baseUrl from request URL when not configured.
|
|
77
|
+
// Workers for Platforms deployments don't know their URL at build time.
|
|
78
|
+
if (!baseUrl && requestUrl) {
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(requestUrl);
|
|
81
|
+
const isLoopback = url.hostname === 'localhost'
|
|
82
|
+
|| url.hostname === '127.0.0.1'
|
|
83
|
+
|| url.hostname === '0.0.0.0'
|
|
84
|
+
|| url.hostname === '::1';
|
|
85
|
+
let protocol = url.protocol;
|
|
86
|
+
let forcedHttps = false;
|
|
87
|
+
if (!isLoopback && protocol !== 'https:') {
|
|
88
|
+
protocol = 'https:';
|
|
89
|
+
forcedHttps = true;
|
|
90
|
+
}
|
|
91
|
+
// Build URL with hostname (not host) to avoid carrying
|
|
92
|
+
// default-for-original-scheme ports across a protocol switch.
|
|
93
|
+
// Drop port entirely when forcing HTTPS (Workers serve on standard ports).
|
|
94
|
+
const host = url.hostname.includes(':') ? `[${url.hostname}]` : url.hostname;
|
|
95
|
+
const portSuffix = (!forcedHttps && url.port) ? `:${url.port}` : '';
|
|
96
|
+
baseUrl = `${protocol}//${host}${portSuffix}`;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// If URL parsing fails, baseUrl stays empty
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!baseUrl) {
|
|
103
|
+
throw new Error('VITE_BASE_URL must be configured (or provide a valid request URL for runtime detection)');
|
|
104
|
+
}
|
|
105
|
+
return { apiUrl, baseUrl };
|
|
106
|
+
}
|
|
107
|
+
function createUserLocals(decoded) {
|
|
108
|
+
return {
|
|
109
|
+
isAuthenticated: true,
|
|
110
|
+
id: decoded.sub,
|
|
111
|
+
email: decoded.email,
|
|
112
|
+
roles: decoded.roles || []
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function tryRefreshToken(event, config, refreshToken, log) {
|
|
116
|
+
if (!refreshToken)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
const sentinel = new SentinelClient(config, event.fetch);
|
|
120
|
+
const result = await sentinel.refreshToken(refreshToken);
|
|
121
|
+
if (result.code === 0 || result.success) {
|
|
122
|
+
if (result.access_token) {
|
|
123
|
+
event.cookies.set(COOKIE_NAMES.ACCESS_TOKEN, result.access_token, COOKIE_OPTIONS.ACCESS_TOKEN);
|
|
124
|
+
// Handle refresh token rotation
|
|
125
|
+
if (result.refresh_token) {
|
|
126
|
+
event.cookies.set(COOKIE_NAMES.REFRESH_TOKEN, result.refresh_token, COOKIE_OPTIONS.REFRESH_TOKEN_LONG);
|
|
127
|
+
}
|
|
128
|
+
// Local decode — no second API call. The token was just issued by
|
|
129
|
+
// the Sentinel backend during refresh, so it's already valid.
|
|
130
|
+
const decoded = decodeTokenLocally(result.access_token);
|
|
131
|
+
if (decoded) {
|
|
132
|
+
const locals = event.locals;
|
|
133
|
+
locals.user = createUserLocals(decoded);
|
|
134
|
+
locals.accessToken = result.access_token;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
log.warn('Token refresh failed - continuing without user');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
log.error('Token refresh error', { error: String(error) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=handle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handle.js","sourceRoot":"","sources":["../../src/sveltekit/handle.ts"],"names":[],"mappings":"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","sourcesContent":["// src/sveltekit/handle.ts\nimport type { Handle, RequestEvent } from '@sveltejs/kit';\nimport type { SentinelHandleOptions, UserLocals, SentinelConfig, DecodedToken } from '../core/types.js';\nimport { SentinelClient } from '../core/client.js';\nimport { isTokenExpired, decodeTokenLocally } from '../core/auth-utils.js';\nimport { createLogger, type SentinelLogger } from '../core/logger.js';\nimport { COOKIE_NAMES, COOKIE_OPTIONS } from './cookies.js';\n\n/** Locals shape set by the Sentinel auth middleware. */\nexport interface SentinelLocals {\n\tuser: UserLocals | null;\n\taccessToken: string | null;\n}\n\n/**\n * Creates a SvelteKit handle function with Sentinel authentication built-in.\n *\n * Features:\n * - Local JWT decode on every request (no per-request API call)\n * - API-validated token refresh when expired or structurally invalid\n * - Local expiration check (saves API calls)\n * - Protected route redirection\n * - Sets event.locals.user for all routes\n * - Never clears cookies (only explicit logout does)\n */\nexport function createSentinelHandle(options: SentinelHandleOptions = {}): Handle {\n\tconst {\n\t\tprotectedRoutes = [],\n\t\tloginRedirect = '/auth'\n\t} = options;\n\tconst log = options.logger ?? createLogger('sentinel-handle');\n\n\treturn async ({ event, resolve }) => {\n\t\t// Initialize locals with typed shape\n\t\tconst locals = event.locals as SentinelLocals;\n\t\tlocals.user = null;\n\t\tlocals.accessToken = null;\n\n\t\t// Get config from environment or options (with request URL fallback)\n\t\tconst config = getConfig(options, event.platform, event.request.url);\n\n\t\tconst accessToken = event.cookies.get(COOKIE_NAMES.ACCESS_TOKEN);\n\t\tconst refreshToken = event.cookies.get(COOKIE_NAMES.REFRESH_TOKEN);\n\n\t\tif (accessToken) {\n\t\t\tif (!isTokenExpired(accessToken)) {\n\t\t\t\tconst decoded = decodeTokenLocally(accessToken);\n\t\t\t\tif (decoded) {\n\t\t\t\t\tlocals.user = createUserLocals(decoded);\n\t\t\t\t\tlocals.accessToken = accessToken;\n\t\t\t\t} else {\n\t\t\t\t\tawait tryRefreshToken(event, config, refreshToken, log);\n\t\t\t\t}\n\t\t\t} else if (refreshToken) {\n\t\t\t\tawait tryRefreshToken(event, config, refreshToken, log);\n\t\t\t}\n\t\t} else if (refreshToken) {\n\t\t\tawait tryRefreshToken(event, config, refreshToken, log);\n\t\t}\n\n\t\t// Protected routes check — redirect to login with return URL\n\t\tconst isProtectedRoute = protectedRoutes.some(route =>\n\t\t\tevent.url.pathname.startsWith(route)\n\t\t);\n\n\t\tif (isProtectedRoute && !locals.user) {\n\t\t\tconst returnPath = event.url.pathname + event.url.search;\n\t\t\tconst redirectUrl = `${loginRedirect}?redirect=${encodeURIComponent(returnPath)}`;\n\t\t\treturn new Response('Redirect', {\n\t\t\t\tstatus: 302,\n\t\t\t\theaders: { Location: redirectUrl }\n\t\t\t});\n\t\t}\n\n\t\tconst response = await resolve(event);\n\n\t\t// Add security headers\n\t\tresponse.headers.set('X-Frame-Options', 'DENY');\n\t\tresponse.headers.set('X-Content-Type-Options', 'nosniff');\n\t\tresponse.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');\n\n\t\treturn response;\n\t};\n}\n\nfunction getConfig(options: SentinelHandleOptions, platform: Readonly<App.Platform> | undefined, requestUrl?: string): SentinelConfig {\n\tlet apiUrl = options.apiUrl || (platform as Record<string, any>)?.env?.SENTINEL_API_URL || '';\n\tlet baseUrl = options.baseUrl || (platform as Record<string, any>)?.env?.VITE_BASE_URL || '';\n\n\tif (!apiUrl) {\n\t\tthrow new Error('SENTINEL_API_URL must be configured');\n\t}\n\n\t// Normalize: strip trailing slashes, then append /api if not already present\n\twhile (apiUrl.endsWith('/')) apiUrl = apiUrl.slice(0, -1);\n\tif (!apiUrl.endsWith('/api')) {\n\t\tapiUrl = apiUrl + '/api';\n\t}\n\n\t// ARCH-003 fix: Derive baseUrl from request URL when not configured.\n\t// Workers for Platforms deployments don't know their URL at build time.\n\tif (!baseUrl && requestUrl) {\n\t\ttry {\n\t\t\tconst url = new URL(requestUrl);\n\t\t\tconst isLoopback = url.hostname === 'localhost'\n\t\t\t\t|| url.hostname === '127.0.0.1'\n\t\t\t\t|| url.hostname === '0.0.0.0'\n\t\t\t\t|| url.hostname === '::1';\n\n\t\t\tlet protocol = url.protocol;\n\t\t\tlet forcedHttps = false;\n\t\t\tif (!isLoopback && protocol !== 'https:') {\n\t\t\t\tprotocol = 'https:';\n\t\t\t\tforcedHttps = true;\n\t\t\t}\n\n\t\t\t// Build URL with hostname (not host) to avoid carrying\n\t\t\t// default-for-original-scheme ports across a protocol switch.\n\t\t\t// Drop port entirely when forcing HTTPS (Workers serve on standard ports).\n\t\t\tconst host = url.hostname.includes(':') ? `[${url.hostname}]` : url.hostname;\n\t\t\tconst portSuffix = (!forcedHttps && url.port) ? `:${url.port}` : '';\n\t\t\tbaseUrl = `${protocol}//${host}${portSuffix}`;\n\t\t} catch {\n\t\t\t// If URL parsing fails, baseUrl stays empty\n\t\t}\n\t}\n\n\tif (!baseUrl) {\n\t\tthrow new Error('VITE_BASE_URL must be configured (or provide a valid request URL for runtime detection)');\n\t}\n\n\treturn { apiUrl, baseUrl };\n}\n\nfunction createUserLocals(decoded: DecodedToken): UserLocals {\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\nasync function tryRefreshToken(event: RequestEvent, config: SentinelConfig, refreshToken: string | undefined, log: SentinelLogger): Promise<void> {\n\tif (!refreshToken) return;\n\n\ttry {\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\t// Handle refresh token rotation\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\t// Local decode — no second API call. The token was just issued by\n\t\t\t\t// the Sentinel backend during refresh, so it's already valid.\n\t\t\t\tconst decoded = decodeTokenLocally(result.access_token);\n\t\t\t\tif (decoded) {\n\t\t\t\t\tconst locals = event.locals as SentinelLocals;\n\t\t\t\t\tlocals.user = createUserLocals(decoded);\n\t\t\t\t\tlocals.accessToken = result.access_token;\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tlog.warn('Token refresh failed - continuing without user');\n\t\t}\n\t} catch (error) {\n\t\tlog.error('Token refresh error', { error: String(error) });\n\t}\n}\n\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RequestHandler } from '@sveltejs/kit';
|
|
2
|
+
import type { AuthHandlerOptions } from '../../core/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates all auth API route handlers for catch-all route pattern.
|
|
5
|
+
*
|
|
6
|
+
* @param options - Optional configuration for auth handlers.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // src/routes/api/auth/[...path]/+server.ts
|
|
11
|
+
* import { createAuthHandlers } from '@nexartis/sentinel-sdk/sveltekit/handlers';
|
|
12
|
+
* export const { GET, POST } = createAuthHandlers({
|
|
13
|
+
* loginCookieTtlSeconds: 1800,
|
|
14
|
+
* loginRedirect: '/auth',
|
|
15
|
+
* redirectCookie: true,
|
|
16
|
+
* onBeforeLogin: async (event, ctx) => {
|
|
17
|
+
* // custom rate limiting, validation, etc.
|
|
18
|
+
* },
|
|
19
|
+
* onAfterClaim: async (event, ctx) => {
|
|
20
|
+
* // create user record, sync profile, etc.
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createAuthHandlers(options?: AuthHandlerOptions): {
|
|
26
|
+
GET: RequestHandler;
|
|
27
|
+
POST: RequestHandler;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sveltekit/handlers/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,eAAe,CAAC;AAElE,OAAO,KAAK,EACX,kBAAkB,EAGlB,MAAM,qBAAqB,CAAC;AA+gB7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,GAAG;IAAE,GAAG,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,CA8ClH"}
|