@saasak/kit-oauth 0.0.1

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.
Files changed (46) hide show
  1. package/dist/derive-secret.d.ts +2 -0
  2. package/dist/derive-secret.js +6 -0
  3. package/dist/derive-secret.js.map +1 -0
  4. package/dist/factory.d.ts +2 -0
  5. package/dist/factory.js +83 -0
  6. package/dist/factory.js.map +1 -0
  7. package/dist/feature-flags.d.ts +9 -0
  8. package/dist/feature-flags.js +19 -0
  9. package/dist/feature-flags.js.map +1 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.js +6 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/init.d.ts +2 -0
  14. package/dist/init.js +45 -0
  15. package/dist/init.js.map +1 -0
  16. package/dist/middleware.d.ts +13 -0
  17. package/dist/middleware.js +176 -0
  18. package/dist/middleware.js.map +1 -0
  19. package/dist/oauth.d.ts +28 -0
  20. package/dist/oauth.js +101 -0
  21. package/dist/oauth.js.map +1 -0
  22. package/dist/permissions.d.ts +14 -0
  23. package/dist/permissions.js +33 -0
  24. package/dist/permissions.js.map +1 -0
  25. package/dist/routes/callback.d.ts +3 -0
  26. package/dist/routes/callback.js +80 -0
  27. package/dist/routes/callback.js.map +1 -0
  28. package/dist/routes/login.d.ts +3 -0
  29. package/dist/routes/login.js +26 -0
  30. package/dist/routes/login.js.map +1 -0
  31. package/dist/routes/logout.d.ts +3 -0
  32. package/dist/routes/logout.js +41 -0
  33. package/dist/routes/logout.js.map +1 -0
  34. package/dist/routes/refresh.d.ts +3 -0
  35. package/dist/routes/refresh.js +44 -0
  36. package/dist/routes/refresh.js.map +1 -0
  37. package/dist/routes/set-team.d.ts +3 -0
  38. package/dist/routes/set-team.js +62 -0
  39. package/dist/routes/set-team.js.map +1 -0
  40. package/dist/session.d.ts +15 -0
  41. package/dist/session.js +107 -0
  42. package/dist/session.js.map +1 -0
  43. package/dist/types.d.ts +141 -0
  44. package/dist/types.js +2 -0
  45. package/dist/types.js.map +1 -0
  46. package/package.json +41 -0
@@ -0,0 +1,80 @@
1
+ import { error, redirect } from '@sveltejs/kit';
2
+ import { exchangeCode, verifyIdToken } from '../oauth.js';
3
+ import { createSession, setSessionCookie, setActiveTeamCookie } from '../session.js';
4
+ export async function handleCallback(event, ctx) {
5
+ const code = event.url.searchParams.get('code');
6
+ const state = event.url.searchParams.get('state');
7
+ const errorParam = event.url.searchParams.get('error');
8
+ if (errorParam) {
9
+ error(400, `OAuth error: ${errorParam}`);
10
+ }
11
+ if (!code || !state) {
12
+ error(400, 'Missing code or state parameter');
13
+ }
14
+ const oauthStateCookie = event.cookies.get(ctx.cookieNames.oauthState);
15
+ if (!oauthStateCookie) {
16
+ error(400, 'Missing OAuth state cookie — session may have expired');
17
+ }
18
+ let oauthState;
19
+ try {
20
+ oauthState = JSON.parse(oauthStateCookie);
21
+ }
22
+ catch {
23
+ error(400, 'Invalid OAuth state cookie');
24
+ }
25
+ if (oauthState.state !== state) {
26
+ error(400, 'OAuth state mismatch — possible CSRF');
27
+ }
28
+ event.cookies.delete(ctx.cookieNames.oauthState, { path: '/' });
29
+ let tokens;
30
+ try {
31
+ tokens = await exchangeCode(ctx, code, oauthState.code_verifier);
32
+ }
33
+ catch (err) {
34
+ console.error(`[${ctx.config.displayName}] Token exchange failed:`, err);
35
+ error(502, 'Failed to exchange authorization code with IAM');
36
+ }
37
+ if (!tokens.id_token) {
38
+ error(502, 'IAM did not return an id_token');
39
+ }
40
+ let claims;
41
+ try {
42
+ claims = await verifyIdToken(ctx, tokens.id_token);
43
+ }
44
+ catch (err) {
45
+ console.error(`[${ctx.config.displayName}] ID token verification failed:`, err);
46
+ error(502, 'Failed to verify ID token from IAM');
47
+ }
48
+ const secret = new TextEncoder().encode(ctx.env.SESSION_SECRET);
49
+ let activeTeamId = null;
50
+ if (ctx.config.appModel === 'b2b' && claims.orgs.length > 0) {
51
+ const accessibleOrgs = claims.orgs.filter((o) => o.appAccess !== false);
52
+ const hinted = oauthState.org_hint
53
+ ? accessibleOrgs.find((o) => o.slug === oauthState.org_hint)
54
+ : undefined;
55
+ const targetOrg = hinted ?? accessibleOrgs[0];
56
+ activeTeamId = targetOrg?.teams?.find((t) => t.isDefault)?.id ?? null;
57
+ }
58
+ const sessionToken = await createSession(secret, {
59
+ sub: claims.sub,
60
+ email: claims.email,
61
+ name: claims.name,
62
+ emailVerified: claims.email_verified,
63
+ orgs: claims.orgs,
64
+ accessToken: tokens.access_token,
65
+ refreshToken: tokens.refresh_token,
66
+ permissions: claims.permissions,
67
+ impersonatedBy: claims.impersonatedBy
68
+ });
69
+ if (oauthState.mobile) {
70
+ const mobileScheme = ctx.config.mobileScheme ?? `com.saasak.${ctx.config.cookiePrefix}`;
71
+ const params = new URLSearchParams({ token: sessionToken });
72
+ if (activeTeamId)
73
+ params.set('activeTeamId', activeTeamId);
74
+ redirect(303, `${mobileScheme}://auth?${params}`);
75
+ }
76
+ setSessionCookie(event.cookies, ctx.cookieNames.session, sessionToken, ctx.secure);
77
+ setActiveTeamCookie(event.cookies, ctx.cookieNames.activeTeam, activeTeamId, ctx.secure);
78
+ redirect(303, oauthState.returnTo || '/');
79
+ }
80
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/routes/callback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { OAuthContext } from '../types.js';
3
+ export declare function handleLogin(event: RequestEvent, ctx: OAuthContext): Promise<Response>;
@@ -0,0 +1,26 @@
1
+ import { redirect } from '@sveltejs/kit';
2
+ import { generateState, generateCodeVerifier, computeS256Challenge, buildAuthorizeURL } from '../oauth.js';
3
+ export async function handleLogin(event, ctx) {
4
+ const returnTo = event.url.searchParams.get('returnTo') ?? '/';
5
+ const orgHint = event.url.searchParams.get('org_hint') ?? undefined;
6
+ const mobile = event.url.searchParams.get('mobile') === '1';
7
+ const state = generateState();
8
+ const codeVerifier = generateCodeVerifier();
9
+ const codeChallenge = await computeS256Challenge(codeVerifier);
10
+ event.cookies.set(ctx.cookieNames.oauthState, JSON.stringify({
11
+ state,
12
+ code_verifier: codeVerifier,
13
+ returnTo,
14
+ org_hint: orgHint,
15
+ mobile
16
+ }), {
17
+ path: '/',
18
+ httpOnly: true,
19
+ sameSite: 'lax',
20
+ secure: ctx.secure,
21
+ maxAge: 600 // 10 minutes
22
+ });
23
+ const authorizeUrl = buildAuthorizeURL(ctx, state, codeChallenge, orgHint);
24
+ redirect(303, authorizeUrl);
25
+ }
26
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/routes/login.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE3G,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAmB,EAAE,GAAiB;IACvE,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,GAAG,CAAC;IAC/D,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,SAAS,CAAC;IACpE,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC;IAE5D,MAAM,KAAK,GAAG,aAAa,EAAE,CAAC;IAC9B,MAAM,YAAY,GAAG,oBAAoB,EAAE,CAAC;IAC5C,MAAM,aAAa,GAAG,MAAM,oBAAoB,CAAC,YAAY,CAAC,CAAC;IAE/D,KAAK,CAAC,OAAO,CAAC,GAAG,CAChB,GAAG,CAAC,WAAW,CAAC,UAAU,EAC1B,IAAI,CAAC,SAAS,CAAC;QACd,KAAK;QACL,aAAa,EAAE,YAAY;QAC3B,QAAQ;QACR,QAAQ,EAAE,OAAO;QACjB,MAAM;KACN,CAAC,EACF;QACC,IAAI,EAAE,GAAG;QACT,QAAQ,EAAE,IAAI;QACd,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,GAAG,CAAC,aAAa;KACzB,CACD,CAAC;IAEF,MAAM,YAAY,GAAG,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;IAC3E,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { OAuthContext } from '../types.js';
3
+ export declare function handleLogout(event: RequestEvent, ctx: OAuthContext): Promise<Response>;
@@ -0,0 +1,41 @@
1
+ import { json, redirect } from '@sveltejs/kit';
2
+ import { isBearerRequest, clearSessionCookie, clearActiveTeamCookie } from '../session.js';
3
+ export async function handleLogout(event, ctx) {
4
+ const isBearer = isBearerRequest(event.request);
5
+ // Invalidate IAM session by forwarding the cross-subdomain cookie
6
+ let iamSessionCookie;
7
+ let iamCookieName;
8
+ for (const name of ctx.iamSessionCookies) {
9
+ const val = event.cookies.get(name);
10
+ if (val) {
11
+ iamSessionCookie = val;
12
+ iamCookieName = name;
13
+ break;
14
+ }
15
+ }
16
+ if (iamSessionCookie && iamCookieName) {
17
+ try {
18
+ await fetch(`${ctx.env.IAM_URL}/api/auth/sign-out`, {
19
+ method: 'POST',
20
+ headers: {
21
+ cookie: `${iamCookieName}=${iamSessionCookie}`
22
+ }
23
+ });
24
+ }
25
+ catch {
26
+ // IAM unreachable — continue with local cleanup
27
+ }
28
+ }
29
+ clearSessionCookie(event.cookies, ctx.cookieNames.session);
30
+ clearActiveTeamCookie(event.cookies, ctx.cookieNames.activeTeam);
31
+ // Clear cross-subdomain better-auth cookies
32
+ const cookieDomain = ctx.env.AUTH_COOKIE_DOMAIN ?? '.lvh.me';
33
+ for (const name of ctx.iamSessionCookies) {
34
+ event.cookies.delete(name, { path: '/', domain: cookieDomain });
35
+ }
36
+ if (isBearer) {
37
+ return json({ logged_out: true });
38
+ }
39
+ redirect(303, '/auth/login');
40
+ }
41
+ //# sourceMappingURL=logout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logout.js","sourceRoot":"","sources":["../../src/routes/logout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAG/C,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE3F,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAmB,EAAE,GAAiB;IACxE,MAAM,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEhD,kEAAkE;IAClE,IAAI,gBAAoC,CAAC;IACzC,IAAI,aAAiC,CAAC;IACtC,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,iBAAiB,EAAE,CAAC;QAC1C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,GAAG,EAAE,CAAC;YACT,gBAAgB,GAAG,GAAG,CAAC;YACvB,aAAa,GAAG,IAAI,CAAC;YACrB,MAAM;QACP,CAAC;IACF,CAAC;IAED,IAAI,gBAAgB,IAAI,aAAa,EAAE,CAAC;QACvC,IAAI,CAAC;YACJ,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,OAAO,oBAAoB,EAAE;gBACnD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACR,MAAM,EAAE,GAAG,aAAa,IAAI,gBAAgB,EAAE;iBAC9C;aACD,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,gDAAgD;QACjD,CAAC;IACF,CAAC;IAED,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAC3D,qBAAqB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;IAEjE,4CAA4C;IAC5C,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC,kBAAkB,IAAI,SAAS,CAAC;IAC7D,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,iBAAiB,EAAE,CAAC;QAC1C,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;IACjE,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACd,OAAO,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAED,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { OAuthContext } from '../types.js';
3
+ export declare function handleRefresh(event: RequestEvent, ctx: OAuthContext): Promise<Response>;
@@ -0,0 +1,44 @@
1
+ import { json, error } from '@sveltejs/kit';
2
+ import { refreshTokens, verifyIdToken, RefreshError } from '../oauth.js';
3
+ import { readSession, getSessionToken, createSession, setSessionCookie, clearSessionCookie } from '../session.js';
4
+ export async function handleRefresh(event, ctx) {
5
+ const secret = new TextEncoder().encode(ctx.env.SESSION_SECRET);
6
+ const token = getSessionToken(event.request, event.cookies, ctx.cookieNames.session);
7
+ const user = token ? await readSession(secret, token) : null;
8
+ if (!user) {
9
+ throw error(401, 'No session');
10
+ }
11
+ if (!user.refreshToken) {
12
+ throw error(400, 'No refresh token available');
13
+ }
14
+ try {
15
+ const tokens = await refreshTokens(ctx, user.refreshToken);
16
+ let updatedClaims = { ...user, accessToken: tokens.access_token };
17
+ if (tokens.refresh_token) {
18
+ updatedClaims.refreshToken = tokens.refresh_token;
19
+ }
20
+ if (tokens.id_token) {
21
+ const idClaims = await verifyIdToken(ctx, tokens.id_token);
22
+ updatedClaims = {
23
+ ...updatedClaims,
24
+ sub: idClaims.sub,
25
+ email: idClaims.email,
26
+ name: idClaims.name,
27
+ emailVerified: idClaims.email_verified,
28
+ orgs: idClaims.orgs,
29
+ permissions: idClaims.permissions
30
+ };
31
+ }
32
+ const newToken = await createSession(secret, updatedClaims);
33
+ setSessionCookie(event.cookies, ctx.cookieNames.session, newToken, ctx.secure);
34
+ return json({ refreshed: true, token: newToken });
35
+ }
36
+ catch (err) {
37
+ if (err instanceof RefreshError && err.status >= 400 && err.status < 500) {
38
+ clearSessionCookie(event.cookies, ctx.cookieNames.session);
39
+ throw error(401, 'Session revoked or expired');
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/routes/refresh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EACN,WAAW,EACX,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,MAAM,eAAe,CAAC;AAEvB,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAmB,EAAE,GAAiB;IACzE,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACrF,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE7D,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,KAAK,CAAC,GAAG,EAAE,4BAA4B,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAE3D,IAAI,aAAa,GAAG,EAAE,GAAG,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;QAClE,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;YAC1B,aAAa,CAAC,YAAY,GAAG,MAAM,CAAC,aAAa,CAAC;QACnD,CAAC;QAED,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACrB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3D,aAAa,GAAG;gBACf,GAAG,aAAa;gBAChB,GAAG,EAAE,QAAQ,CAAC,GAAG;gBACjB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,aAAa,EAAE,QAAQ,CAAC,cAAc;gBACtC,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,WAAW,EAAE,QAAQ,CAAC,WAAW;aACjC,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;QAC5D,gBAAgB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAE/E,OAAO,IAAI,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,GAAG,YAAY,YAAY,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAC1E,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;YAC3D,MAAM,KAAK,CAAC,GAAG,EAAE,4BAA4B,CAAC,CAAC;QAChD,CAAC;QACD,MAAM,GAAG,CAAC;IACX,CAAC;AACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { OAuthContext } from '../types.js';
3
+ export declare function handleSetTeam(event: RequestEvent, ctx: OAuthContext): Promise<Response>;
@@ -0,0 +1,62 @@
1
+ import { json, redirect } from '@sveltejs/kit';
2
+ import { readSession, getSessionToken, isBearerRequest, setActiveTeamCookie } from '../session.js';
3
+ export async function handleSetTeam(event, ctx) {
4
+ const isBearer = isBearerRequest(event.request);
5
+ const secret = new TextEncoder().encode(ctx.env.SESSION_SECRET);
6
+ let teamId;
7
+ if (isBearer) {
8
+ let body;
9
+ try {
10
+ body = await event.request.json();
11
+ }
12
+ catch {
13
+ return json({ error: 'invalid JSON body' }, { status: 400 });
14
+ }
15
+ teamId = body.teamId ?? '';
16
+ }
17
+ else {
18
+ const form = await event.request.formData();
19
+ teamId = form.get('teamId')?.toString() ?? '';
20
+ }
21
+ const token = getSessionToken(event.request, event.cookies, ctx.cookieNames.session);
22
+ if (!token) {
23
+ if (isBearer)
24
+ return json({ error: 'unauthorized' }, { status: 401 });
25
+ redirect(303, '/auth/login');
26
+ }
27
+ const session = await readSession(secret, token);
28
+ if (!session) {
29
+ if (isBearer)
30
+ return json({ error: 'unauthorized' }, { status: 401 });
31
+ redirect(303, '/auth/login');
32
+ }
33
+ const activeTeamId = teamId || null;
34
+ if (activeTeamId) {
35
+ const hasAccess = session.orgs.some((o) => o.appAccess !== false && o.teams?.some((t) => t.id === activeTeamId));
36
+ if (!hasAccess) {
37
+ if (isBearer)
38
+ return json({ error: 'not a member of this team' }, { status: 403 });
39
+ redirect(303, '/');
40
+ }
41
+ }
42
+ if (isBearer) {
43
+ return json({ ok: true, activeTeamId });
44
+ }
45
+ setActiveTeamCookie(event.cookies, ctx.cookieNames.activeTeam, activeTeamId, ctx.secure);
46
+ // Validate referer is same-origin to prevent open redirect
47
+ const referer = event.request.headers.get('referer');
48
+ let redirectTo = '/';
49
+ if (referer) {
50
+ try {
51
+ const refererUrl = new URL(referer);
52
+ if (refererUrl.origin === event.url.origin) {
53
+ redirectTo = refererUrl.pathname + refererUrl.search;
54
+ }
55
+ }
56
+ catch {
57
+ // Invalid referer URL — use default
58
+ }
59
+ }
60
+ redirect(303, redirectTo);
61
+ }
62
+ //# sourceMappingURL=set-team.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-team.js","sourceRoot":"","sources":["../../src/routes/set-team.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Cookies } from '@sveltejs/kit';
2
+ import type { SessionClaims } from './types.js';
3
+ export declare const SESSION_MAX_AGE: number;
4
+ export declare const SYNC_AGE: number;
5
+ export declare function createSession(secret: Uint8Array, claims: SessionClaims, maxAge?: number): Promise<string>;
6
+ export declare function isApiToken(token: string): boolean;
7
+ export declare function readSession(secret: Uint8Array, token: string): Promise<SessionClaims | null>;
8
+ export declare function setSessionCookie(cookies: Cookies, cookieName: string, token: string, secure: boolean): void;
9
+ export declare function clearSessionCookie(cookies: Cookies, cookieName: string): void;
10
+ export declare function getSessionCookie(cookies: Cookies, cookieName: string): string | undefined;
11
+ export declare function getSessionToken(request: Request, cookies: Cookies, cookieName: string): string | undefined;
12
+ export declare function isBearerRequest(request: Request): boolean;
13
+ export declare function setActiveTeamCookie(cookies: Cookies, cookieName: string, teamId: string | null, secure: boolean): void;
14
+ export declare function getActiveTeamId(request: Request, cookies: Cookies, cookieName: string): string | null;
15
+ export declare function clearActiveTeamCookie(cookies: Cookies, cookieName: string): void;
@@ -0,0 +1,107 @@
1
+ import * as jose from 'jose';
2
+ export const SESSION_MAX_AGE = 60 * 60 * 24 * 7; // 7 days
3
+ export const SYNC_AGE = 5 * 60; // 5 minutes — revalidate against IAM
4
+ export async function createSession(secret, claims, maxAge = SESSION_MAX_AGE) {
5
+ const payload = {
6
+ email: claims.email,
7
+ name: claims.name,
8
+ emailVerified: claims.emailVerified,
9
+ orgs: claims.orgs,
10
+ accessToken: claims.accessToken,
11
+ refreshToken: claims.refreshToken,
12
+ permissions: claims.permissions
13
+ };
14
+ if (claims.impersonatedBy) {
15
+ payload.impersonatedBy = claims.impersonatedBy;
16
+ }
17
+ if (claims.apiToken)
18
+ payload.apiToken = true;
19
+ if (claims.plan)
20
+ payload.plan = claims.plan;
21
+ if (claims.entitlements)
22
+ payload.entitlements = claims.entitlements;
23
+ if (claims.addons)
24
+ payload.addons = claims.addons;
25
+ return new jose.SignJWT(payload)
26
+ .setProtectedHeader({ alg: 'HS256' })
27
+ .setSubject(claims.sub)
28
+ .setIssuedAt()
29
+ .setExpirationTime(`${maxAge}s`)
30
+ .sign(secret);
31
+ }
32
+ export function isApiToken(token) {
33
+ return token.startsWith('sk_live_');
34
+ }
35
+ export async function readSession(secret, token) {
36
+ try {
37
+ const { payload } = await jose.jwtVerify(token, secret);
38
+ const p = payload;
39
+ return {
40
+ sub: payload.sub,
41
+ email: p.email,
42
+ name: p.name,
43
+ emailVerified: p.emailVerified ?? false,
44
+ orgs: (Array.isArray(p.orgs) ? p.orgs : []),
45
+ accessToken: p.accessToken,
46
+ refreshToken: p.refreshToken,
47
+ permissions: p.permissions,
48
+ impersonatedBy: p.impersonatedBy,
49
+ apiToken: p.apiToken || undefined,
50
+ plan: p.plan,
51
+ entitlements: p.entitlements,
52
+ addons: p.addons
53
+ };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ export function setSessionCookie(cookies, cookieName, token, secure) {
60
+ cookies.set(cookieName, token, {
61
+ path: '/',
62
+ httpOnly: true,
63
+ sameSite: 'lax',
64
+ secure,
65
+ maxAge: SESSION_MAX_AGE
66
+ });
67
+ }
68
+ export function clearSessionCookie(cookies, cookieName) {
69
+ cookies.delete(cookieName, { path: '/' });
70
+ }
71
+ export function getSessionCookie(cookies, cookieName) {
72
+ return cookies.get(cookieName);
73
+ }
74
+ export function getSessionToken(request, cookies, cookieName) {
75
+ const auth = request.headers.get('authorization');
76
+ if (auth?.startsWith('Bearer ')) {
77
+ return auth.slice(7);
78
+ }
79
+ return getSessionCookie(cookies, cookieName);
80
+ }
81
+ export function isBearerRequest(request) {
82
+ return request.headers.get('authorization')?.startsWith('Bearer ') ?? false;
83
+ }
84
+ export function setActiveTeamCookie(cookies, cookieName, teamId, secure) {
85
+ if (teamId) {
86
+ cookies.set(cookieName, teamId, {
87
+ path: '/',
88
+ httpOnly: false,
89
+ sameSite: 'lax',
90
+ secure,
91
+ maxAge: SESSION_MAX_AGE
92
+ });
93
+ }
94
+ else {
95
+ cookies.delete(cookieName, { path: '/' });
96
+ }
97
+ }
98
+ export function getActiveTeamId(request, cookies, cookieName) {
99
+ const headerVal = request.headers.get('x-active-team');
100
+ if (headerVal)
101
+ return headerVal;
102
+ return cookies.get(cookieName) ?? null;
103
+ }
104
+ export function clearActiveTeamCookie(cookies, cookieName) {
105
+ cookies.delete(cookieName, { path: '/' });
106
+ }
107
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ import type { Handle, ServerInit } from '@sveltejs/kit';
2
+ export interface TeamMembership {
3
+ id: string;
4
+ name: string;
5
+ isDefault: boolean;
6
+ }
7
+ export interface PlanInfo {
8
+ key: string;
9
+ status: string;
10
+ trial: {
11
+ endsAt: string;
12
+ } | null;
13
+ }
14
+ export interface B2BEntitlements {
15
+ ssoEnabled: boolean;
16
+ maxMembers: number;
17
+ maxTeams: number;
18
+ features: Record<string, boolean>;
19
+ }
20
+ export interface OrgMembership {
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ role: 'owner' | 'admin' | 'member';
25
+ appAccess?: boolean;
26
+ teams: TeamMembership[];
27
+ plan?: PlanInfo;
28
+ entitlements?: B2BEntitlements;
29
+ featureFlags?: Record<string, boolean>;
30
+ }
31
+ export interface SessionClaims {
32
+ sub: string;
33
+ email: string;
34
+ name: string;
35
+ emailVerified: boolean;
36
+ orgs: OrgMembership[];
37
+ accessToken: string;
38
+ refreshToken?: string;
39
+ /** Per-team permissions for the requesting app: { [teamId]: { [resource]: action[] } } */
40
+ permissions?: Record<string, Record<string, string[]>>;
41
+ /** Present when the session was created via admin impersonation. Value is the admin's user ID. */
42
+ impersonatedBy?: string;
43
+ /** B2C: user-level plan info */
44
+ plan?: PlanInfo;
45
+ /** B2C: user-level entitlements (feature flags) */
46
+ entitlements?: {
47
+ features: Record<string, boolean>;
48
+ };
49
+ /** B2C: active addon keys */
50
+ addons?: string[];
51
+ /** True when this session was created via an M2M API token exchange */
52
+ apiToken?: boolean;
53
+ }
54
+ /** Helper type for consuming apps to augment their App.Locals */
55
+ export interface OAuthLocals {
56
+ user: SessionClaims | null;
57
+ activeTeamId: string | null;
58
+ }
59
+ export type AppModel = 'b2b' | 'b2c';
60
+ export interface ResolvedEnv {
61
+ IAM_URL: string;
62
+ SAASAK_APP_SEED: string;
63
+ SESSION_SECRET: string;
64
+ ORIGIN: string;
65
+ AUTH_COOKIE_DOMAIN?: string;
66
+ }
67
+ export interface OAuthHandlerConfig {
68
+ /** Package name used as OAuth clientId, e.g. '@saasak/app1' */
69
+ clientId: string;
70
+ /** Human-readable display name for IAM registration and logging */
71
+ displayName: string;
72
+ /** App model: 'b2b' (team-scoped) or 'b2c' (team-less) */
73
+ appModel: AppModel;
74
+ /** Cookie prefix, e.g. 'app1' → 'app1_session', 'app1_active_team', 'app1_oauth_state' */
75
+ cookiePrefix: string;
76
+ /**
77
+ * Lazy resolver for environment variables.
78
+ * Called once on first request (not at module load time) to respect
79
+ * SvelteKit's prohibition on accessing $env at module scope.
80
+ */
81
+ env: () => ResolvedEnv;
82
+ /** Custom URL scheme for mobile OAuth callback, e.g. 'com.saasak.app1' */
83
+ mobileScheme?: string;
84
+ /** IAM registration options (used by init) */
85
+ registration?: {
86
+ defaultAllow?: boolean;
87
+ skipConsent?: boolean;
88
+ /** App-specific permission schema: { resource: action[] } */
89
+ permissions?: Record<string, string[]>;
90
+ /** Feature flag declarations: { system: {...}, org: {...} } */
91
+ featureFlags?: {
92
+ system?: Record<string, {
93
+ description: string;
94
+ default: boolean;
95
+ }>;
96
+ org?: Record<string, {
97
+ description: string;
98
+ default: boolean;
99
+ }>;
100
+ };
101
+ };
102
+ /** Extra path prefixes exempt from auth (besides /auth and /api) */
103
+ publicPaths?: string[];
104
+ /**
105
+ * IAM cross-subdomain session cookie names to check for logout sync.
106
+ * Default: ['better-auth.session_token', '__Secure-better-auth.session_token']
107
+ */
108
+ iamSessionCookies?: string[];
109
+ }
110
+ export interface OAuthContext {
111
+ config: OAuthHandlerConfig;
112
+ env: ResolvedEnv;
113
+ clientSecret: string;
114
+ secure: boolean;
115
+ cookieNames: {
116
+ session: string;
117
+ activeTeam: string;
118
+ oauthState: string;
119
+ };
120
+ iamSessionCookies: string[];
121
+ jwksRef: {
122
+ current: ReturnType<typeof import('jose').createRemoteJWKSet> | null;
123
+ };
124
+ }
125
+ export interface OAuthHandler {
126
+ handle: Handle;
127
+ init: ServerInit;
128
+ /**
129
+ * Session reader for use with kit-auth-check's getUser callback.
130
+ * Reads JWT, handles cross-subdomain sync, and staleness refresh.
131
+ */
132
+ getUser: (event: import('@sveltejs/kit').RequestEvent) => Promise<{
133
+ user: SessionClaims | null;
134
+ refreshedToken?: string;
135
+ }>;
136
+ /**
137
+ * Force-refresh the session tokens. Useful when claims may be stale
138
+ * (e.g., org just created on IAM but not yet in the local JWT).
139
+ */
140
+ forceRefresh: (event: import('@sveltejs/kit').RequestEvent, currentUser: SessionClaims) => Promise<SessionClaims | null>;
141
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@saasak/kit-oauth",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "license": "MIT",
6
+ "author": "saasak <dev@saasak.studio>",
7
+ "description": "OAuth2 PKCE client handler for SvelteKit apps with IAM integration",
8
+ "keywords": [
9
+ "sveltekit",
10
+ "oauth",
11
+ "pkce"
12
+ ],
13
+ "type": "module",
14
+ "main": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ },
21
+ "./permissions": {
22
+ "types": "./dist/permissions.d.ts",
23
+ "default": "./dist/permissions.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@casl/ability": "^6.8.0",
31
+ "jose": "^6.0.0"
32
+ },
33
+ "peerDependencies": {
34
+ "@sveltejs/kit": "^2.0.0"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "clean:build": "rimraf dist",
39
+ "clean:modules": "rimraf node_modules"
40
+ }
41
+ }