@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,2 @@
1
+ /** Derive a deterministic OAuth client secret from a shared seed and client ID. */
2
+ export declare function deriveClientSecret(seed: string, clientId: string): string;
@@ -0,0 +1,6 @@
1
+ import { createHmac } from 'node:crypto';
2
+ /** Derive a deterministic OAuth client secret from a shared seed and client ID. */
3
+ export function deriveClientSecret(seed, clientId) {
4
+ return createHmac('sha256', seed).update(clientId).digest('hex');
5
+ }
6
+ //# sourceMappingURL=derive-secret.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive-secret.js","sourceRoot":"","sources":["../src/derive-secret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,mFAAmF;AACnF,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,QAAgB;IAChE,OAAO,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { OAuthHandlerConfig, OAuthHandler } from './types.js';
2
+ export declare function createOAuthHandler(config: OAuthHandlerConfig): OAuthHandler;
@@ -0,0 +1,83 @@
1
+ import { deriveClientSecret } from './derive-secret.js';
2
+ import { handleLogin } from './routes/login.js';
3
+ import { handleCallback } from './routes/callback.js';
4
+ import { handleLogout } from './routes/logout.js';
5
+ import { handleRefresh } from './routes/refresh.js';
6
+ import { handleSetTeam } from './routes/set-team.js';
7
+ import { createSessionReader, forceRefreshSession } from './middleware.js';
8
+ import { registerWithIAM } from './init.js';
9
+ const DEFAULT_IAM_SESSION_COOKIES = [
10
+ 'better-auth.session_token',
11
+ '__Secure-better-auth.session_token'
12
+ ];
13
+ function validateEnv(env) {
14
+ const required = ['IAM_URL', 'SAASAK_APP_SEED', 'SESSION_SECRET', 'ORIGIN'];
15
+ for (const key of required) {
16
+ if (!env[key])
17
+ throw new Error(`[kit-oauth] Missing required env: ${key}`);
18
+ }
19
+ }
20
+ export function createOAuthHandler(config) {
21
+ let ctx = null;
22
+ function getContext() {
23
+ if (ctx)
24
+ return ctx;
25
+ const env = config.env();
26
+ validateEnv(env);
27
+ const clientSecret = deriveClientSecret(env.SAASAK_APP_SEED, config.clientId);
28
+ ctx = {
29
+ config,
30
+ env,
31
+ clientSecret,
32
+ secure: env.ORIGIN.startsWith('https://'),
33
+ cookieNames: {
34
+ session: `${config.cookiePrefix}_session`,
35
+ activeTeam: `${config.cookiePrefix}_active_team`,
36
+ oauthState: `${config.cookiePrefix}_oauth_state`
37
+ },
38
+ iamSessionCookies: config.iamSessionCookies ?? DEFAULT_IAM_SESSION_COOKIES,
39
+ jwksRef: { current: null }
40
+ };
41
+ return ctx;
42
+ }
43
+ const handle = async ({ event, resolve }) => {
44
+ const c = getContext();
45
+ const pathname = event.url.pathname;
46
+ const method = event.request.method;
47
+ // Route interception — handle /auth/* paths directly
48
+ if (pathname === '/auth/login' && method === 'GET') {
49
+ return handleLogin(event, c);
50
+ }
51
+ if (pathname === '/auth/callback' && method === 'GET') {
52
+ return handleCallback(event, c);
53
+ }
54
+ if (pathname === '/auth/logout' && method === 'POST') {
55
+ return handleLogout(event, c);
56
+ }
57
+ if (pathname === '/auth/refresh' && method === 'POST') {
58
+ return handleRefresh(event, c);
59
+ }
60
+ if (pathname === '/auth/set-team' && method === 'POST') {
61
+ return handleSetTeam(event, c);
62
+ }
63
+ // Not an auth route — pass through
64
+ return resolve(event);
65
+ };
66
+ const init = async () => {
67
+ const c = getContext();
68
+ return registerWithIAM(c);
69
+ };
70
+ // Lazy session reader — created once on first call
71
+ let sessionReader = null;
72
+ const getUser = async (event) => {
73
+ if (!sessionReader) {
74
+ sessionReader = createSessionReader(getContext());
75
+ }
76
+ return sessionReader(event);
77
+ };
78
+ const forceRefresh = async (event, currentUser) => {
79
+ return forceRefreshSession(event, getContext(), currentUser);
80
+ };
81
+ return { handle, init, getUser, forceRefresh };
82
+ }
83
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { OrgMembership } from './types.js';
2
+ /**
3
+ * Check whether a feature flag is enabled for a given org.
4
+ */
5
+ export declare function hasFeatureFlag(orgs: OrgMembership[] | undefined, orgId: string | null, flag: string): boolean;
6
+ /**
7
+ * Server-side enforcement: throw 403 if the feature flag is not enabled.
8
+ */
9
+ export declare function requireFeatureFlag(orgs: OrgMembership[] | undefined, orgId: string | null, flag: string): void;
@@ -0,0 +1,19 @@
1
+ import { error } from '@sveltejs/kit';
2
+ /**
3
+ * Check whether a feature flag is enabled for a given org.
4
+ */
5
+ export function hasFeatureFlag(orgs, orgId, flag) {
6
+ if (!orgs || !orgId)
7
+ return false;
8
+ const org = orgs.find((o) => o.id === orgId);
9
+ return Boolean(org?.featureFlags?.[flag]);
10
+ }
11
+ /**
12
+ * Server-side enforcement: throw 403 if the feature flag is not enabled.
13
+ */
14
+ export function requireFeatureFlag(orgs, orgId, flag) {
15
+ if (!hasFeatureFlag(orgs, orgId, flag)) {
16
+ error(403, `Feature "${flag}" is not enabled`);
17
+ }
18
+ }
19
+ //# sourceMappingURL=feature-flags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feature-flags.js","sourceRoot":"","sources":["../src/feature-flags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAGtC;;GAEG;AACH,MAAM,UAAU,cAAc,CAC7B,IAAiC,EACjC,KAAoB,EACpB,IAAY;IAEZ,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAClC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAC;IAC7C,OAAO,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CACjC,IAAiC,EACjC,KAAoB,EACpB,IAAY;IAEZ,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;QACxC,KAAK,CAAC,GAAG,EAAE,YAAY,IAAI,kBAAkB,CAAC,CAAC;IAChD,CAAC;AACF,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { createOAuthHandler } from './factory.js';
2
+ export { RefreshError } from './oauth.js';
3
+ export { SYNC_AGE, SESSION_MAX_AGE } from './session.js';
4
+ export type { SessionReaderResult } from './middleware.js';
5
+ export type { OAuthHandlerConfig, ResolvedEnv, OAuthHandler, OAuthLocals, SessionClaims, OrgMembership, TeamMembership, AppModel } from './types.js';
6
+ export { buildAbility, getTeamPermissions, requirePermission, type AppAbility } from './permissions.js';
7
+ export { hasFeatureFlag, requireFeatureFlag } from './feature-flags.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { createOAuthHandler } from './factory.js';
2
+ export { RefreshError } from './oauth.js';
3
+ export { SYNC_AGE, SESSION_MAX_AGE } from './session.js';
4
+ export { buildAbility, getTeamPermissions, requirePermission } from './permissions.js';
5
+ export { hasFeatureFlag, requireFeatureFlag } from './feature-flags.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAYzD,OAAO,EACN,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EAEjB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/init.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { OAuthContext } from './types.js';
2
+ export declare function registerWithIAM(ctx: OAuthContext): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,45 @@
1
+ export async function registerWithIAM(ctx) {
2
+ const { env, config, clientSecret } = ctx;
3
+ if (!env.SAASAK_APP_SEED || !env.IAM_URL)
4
+ return;
5
+ const redirectUri = `${env.ORIGIN}/auth/callback`;
6
+ const payload = JSON.stringify({
7
+ clientId: config.clientId,
8
+ redirectUris: [redirectUri],
9
+ name: config.displayName,
10
+ appModel: config.appModel,
11
+ defaultAllow: config.registration?.defaultAllow ?? true,
12
+ skipConsent: config.registration?.skipConsent ?? true,
13
+ permissions: config.registration?.permissions,
14
+ featureFlags: config.registration?.featureFlags
15
+ });
16
+ const maxAttempts = 5;
17
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
18
+ try {
19
+ const res = await fetch(`${env.IAM_URL}/api/register-app`, {
20
+ method: 'POST',
21
+ headers: {
22
+ 'Content-Type': 'application/json',
23
+ Authorization: `Bearer ${env.SAASAK_APP_SEED}`
24
+ },
25
+ body: payload
26
+ });
27
+ if (!res.ok) {
28
+ const body = await res.text().catch(() => 'unknown');
29
+ throw new Error(`HTTP ${res.status}: ${body}`);
30
+ }
31
+ console.log(`[init] Registered OAuth client with IAM: ${config.clientId}`);
32
+ return;
33
+ }
34
+ catch (err) {
35
+ if (attempt < maxAttempts) {
36
+ console.warn(`[init] IAM not ready (attempt ${attempt}/${maxAttempts}), retrying in 2s...`);
37
+ await new Promise((r) => setTimeout(r, 2000));
38
+ }
39
+ else {
40
+ throw new Error(`[init] Failed to register with IAM after ${maxAttempts} attempts: ${err}`);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAiB;IACtD,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC;IAE1C,IAAI,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,GAAG,CAAC,OAAO;QAAE,OAAO;IAEjD,MAAM,WAAW,GAAG,GAAG,GAAG,CAAC,MAAM,gBAAgB,CAAC;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QAC9B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,YAAY,EAAE,CAAC,WAAW,CAAC;QAC3B,IAAI,EAAE,MAAM,CAAC,WAAW;QACxB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY,IAAI,IAAI;QACvD,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,WAAW,IAAI,IAAI;QACrD,WAAW,EAAE,MAAM,CAAC,YAAY,EAAE,WAAW;QAC7C,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,YAAY;KAC/C,CAAC,CAAC;IAEH,MAAM,WAAW,GAAG,CAAC,CAAC;IACtB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;QACzD,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,GAAG,CAAC,OAAO,mBAAmB,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACR,cAAc,EAAE,kBAAkB;oBAClC,aAAa,EAAE,UAAU,GAAG,CAAC,eAAe,EAAE;iBAC9C;gBACD,IAAI,EAAE,OAAO;aACb,CAAC,CAAC;YAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACb,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBACrD,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;YAED,OAAO,CAAC,GAAG,CAAC,4CAA4C,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC3E,OAAO;QACR,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,OAAO,GAAG,WAAW,EAAE,CAAC;gBAC3B,OAAO,CAAC,IAAI,CACX,iCAAiC,OAAO,IAAI,WAAW,sBAAsB,CAC7E,CAAC;gBACF,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;YAC/C,CAAC;iBAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CACd,4CAA4C,WAAW,cAAc,GAAG,EAAE,CAC1E,CAAC;YACH,CAAC;QACF,CAAC;IACF,CAAC;AACF,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { OAuthContext, SessionClaims } from './types.js';
3
+ export interface SessionReaderResult {
4
+ user: SessionClaims | null;
5
+ refreshedToken?: string;
6
+ }
7
+ export declare function createSessionReader(ctx: OAuthContext): (event: RequestEvent) => Promise<SessionReaderResult>;
8
+ /**
9
+ * Force-refresh the session tokens. Useful when claims may be stale
10
+ * (e.g., org just created on IAM but not yet reflected in local JWT).
11
+ * Returns updated user or null on failure.
12
+ */
13
+ export declare function forceRefreshSession(event: RequestEvent, ctx: OAuthContext, currentUser: SessionClaims): Promise<SessionClaims | null>;
@@ -0,0 +1,176 @@
1
+ import * as jose from 'jose';
2
+ import { refreshTokens, verifyIdToken, RefreshError } from './oauth.js';
3
+ import { readSession, getSessionToken, createSession, setSessionCookie, clearSessionCookie, clearActiveTeamCookie, isBearerRequest, isApiToken, SYNC_AGE } from './session.js';
4
+ /**
5
+ * Reads the JWT session from cookie/Bearer header, checks cross-subdomain
6
+ * logout sync, and performs staleness refresh if needed.
7
+ *
8
+ * Does NOT handle public path checks, redirects, team validation, or locals
9
+ * assignment — those are the responsibility of kit-auth-check.
10
+ */
11
+ // In-memory cache for exchanged API token sessions
12
+ const API_TOKEN_CACHE_TTL = 300; // 5 minutes
13
+ const API_TOKEN_CACHE_MAX = 1000;
14
+ const apiTokenCache = new Map();
15
+ function evictExpiredTokens() {
16
+ const now = Date.now();
17
+ for (const [key, entry] of apiTokenCache) {
18
+ if (entry.expiresAt <= now)
19
+ apiTokenCache.delete(key);
20
+ }
21
+ }
22
+ export function createSessionReader(ctx) {
23
+ const secret = new TextEncoder().encode(ctx.env.SESSION_SECRET);
24
+ return async (event) => {
25
+ const isBearer = isBearerRequest(event.request);
26
+ const token = getSessionToken(event.request, event.cookies, ctx.cookieNames.session);
27
+ // Handle M2M API tokens (sk_live_ prefix)
28
+ if (token && isApiToken(token)) {
29
+ const cacheKey = token;
30
+ const cached = apiTokenCache.get(cacheKey);
31
+ if (cached && cached.expiresAt > Date.now()) {
32
+ return { user: cached.claims };
33
+ }
34
+ try {
35
+ const res = await fetch(`${ctx.env.IAM_URL}/api/auth/api-tokens/exchange`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ token, clientId: ctx.config.clientId })
39
+ });
40
+ if (!res.ok) {
41
+ return { user: null };
42
+ }
43
+ const data = (await res.json());
44
+ const claims = {
45
+ sub: data.sub,
46
+ email: data.email,
47
+ name: data.name,
48
+ emailVerified: data.emailVerified ?? false,
49
+ orgs: (data.orgs ?? []),
50
+ accessToken: '',
51
+ permissions: data.permissions,
52
+ impersonatedBy: data.impersonatedBy,
53
+ apiToken: true,
54
+ plan: data.plan,
55
+ entitlements: data.entitlements,
56
+ addons: data.addons
57
+ };
58
+ // Cache the claims
59
+ if (apiTokenCache.size >= API_TOKEN_CACHE_MAX)
60
+ evictExpiredTokens();
61
+ apiTokenCache.set(cacheKey, {
62
+ claims,
63
+ expiresAt: Date.now() + API_TOKEN_CACHE_TTL * 1000
64
+ });
65
+ return { user: claims };
66
+ }
67
+ catch {
68
+ return { user: null };
69
+ }
70
+ }
71
+ let user = token ? await readSession(secret, token) : null;
72
+ // If we have a local session but the cross-subdomain better-auth cookie is gone,
73
+ // IAM has logged the user out → clear local session immediately
74
+ if (user && !isBearer) {
75
+ let baSession;
76
+ for (const name of ctx.iamSessionCookies) {
77
+ baSession = event.cookies.get(name);
78
+ if (baSession)
79
+ break;
80
+ }
81
+ if (!baSession) {
82
+ clearSessionCookie(event.cookies, ctx.cookieNames.session);
83
+ clearActiveTeamCookie(event.cookies, ctx.cookieNames.activeTeam);
84
+ user = null;
85
+ }
86
+ }
87
+ let refreshedToken;
88
+ // Staleness check: if JWT was issued more than SYNC_AGE seconds ago, refresh tokens
89
+ if (user?.refreshToken && token) {
90
+ try {
91
+ const payload = jose.decodeJwt(token);
92
+ const iat = payload.iat ?? 0;
93
+ const age = Math.floor(Date.now() / 1000) - iat;
94
+ if (age > SYNC_AGE) {
95
+ const tokens = await refreshTokens(ctx, user.refreshToken);
96
+ let updatedClaims = {
97
+ ...user,
98
+ accessToken: tokens.access_token
99
+ };
100
+ if (tokens.refresh_token) {
101
+ updatedClaims.refreshToken = tokens.refresh_token;
102
+ }
103
+ if (tokens.id_token) {
104
+ const idClaims = await verifyIdToken(ctx, tokens.id_token);
105
+ updatedClaims = {
106
+ ...updatedClaims,
107
+ sub: idClaims.sub,
108
+ email: idClaims.email,
109
+ name: idClaims.name,
110
+ emailVerified: idClaims.email_verified,
111
+ orgs: idClaims.orgs,
112
+ permissions: idClaims.permissions,
113
+ impersonatedBy: idClaims.impersonatedBy
114
+ };
115
+ }
116
+ const newToken = await createSession(secret, updatedClaims);
117
+ setSessionCookie(event.cookies, ctx.cookieNames.session, newToken, ctx.secure);
118
+ user = updatedClaims;
119
+ if (isBearer) {
120
+ refreshedToken = newToken;
121
+ }
122
+ }
123
+ }
124
+ catch (err) {
125
+ if (err instanceof RefreshError && err.status >= 400 && err.status < 500) {
126
+ clearSessionCookie(event.cookies, ctx.cookieNames.session);
127
+ user = null;
128
+ }
129
+ else {
130
+ console.warn(`[${ctx.config.displayName}] Token refresh failed, continuing with stale session:`, err);
131
+ }
132
+ }
133
+ }
134
+ return { user, refreshedToken };
135
+ };
136
+ }
137
+ /**
138
+ * Force-refresh the session tokens. Useful when claims may be stale
139
+ * (e.g., org just created on IAM but not yet reflected in local JWT).
140
+ * Returns updated user or null on failure.
141
+ */
142
+ export async function forceRefreshSession(event, ctx, currentUser) {
143
+ if (!currentUser.refreshToken)
144
+ return null;
145
+ const secret = new TextEncoder().encode(ctx.env.SESSION_SECRET);
146
+ try {
147
+ const tokens = await refreshTokens(ctx, currentUser.refreshToken);
148
+ let updatedClaims = {
149
+ ...currentUser,
150
+ accessToken: tokens.access_token
151
+ };
152
+ if (tokens.refresh_token) {
153
+ updatedClaims.refreshToken = tokens.refresh_token;
154
+ }
155
+ if (tokens.id_token) {
156
+ const idClaims = await verifyIdToken(ctx, tokens.id_token);
157
+ updatedClaims = {
158
+ ...updatedClaims,
159
+ sub: idClaims.sub,
160
+ email: idClaims.email,
161
+ name: idClaims.name,
162
+ emailVerified: idClaims.email_verified,
163
+ orgs: idClaims.orgs,
164
+ permissions: idClaims.permissions,
165
+ impersonatedBy: idClaims.impersonatedBy
166
+ };
167
+ }
168
+ const newToken = await createSession(secret, updatedClaims);
169
+ setSessionCookie(event.cookies, ctx.cookieNames.session, newToken, ctx.secure);
170
+ return updatedClaims;
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ //# sourceMappingURL=middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { OAuthContext, OrgMembership } from './types.js';
2
+ export declare function generateState(): string;
3
+ export declare function generateCodeVerifier(): string;
4
+ export declare function computeS256Challenge(verifier: string): Promise<string>;
5
+ export declare function buildAuthorizeURL(ctx: OAuthContext, state: string, codeChallenge: string, orgHint?: string): string;
6
+ export declare function exchangeCode(ctx: OAuthContext, code: string, codeVerifier: string): Promise<{
7
+ access_token: string;
8
+ id_token?: string;
9
+ refresh_token?: string;
10
+ }>;
11
+ export declare class RefreshError extends Error {
12
+ status: number;
13
+ constructor(message: string, status: number);
14
+ }
15
+ export declare function refreshTokens(ctx: OAuthContext, refreshToken: string): Promise<{
16
+ access_token: string;
17
+ id_token?: string;
18
+ refresh_token?: string;
19
+ }>;
20
+ export declare function verifyIdToken(ctx: OAuthContext, idToken: string): Promise<{
21
+ sub: string;
22
+ email: string;
23
+ name: string;
24
+ email_verified: boolean;
25
+ orgs: OrgMembership[];
26
+ permissions?: Record<string, Record<string, string[]>>;
27
+ impersonatedBy?: string;
28
+ }>;
package/dist/oauth.js ADDED
@@ -0,0 +1,101 @@
1
+ import * as jose from 'jose';
2
+ // --- PKCE helpers (pure, no config needed) ---
3
+ function base64url(bytes) {
4
+ let binary = '';
5
+ for (const b of bytes)
6
+ binary += String.fromCharCode(b);
7
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
8
+ }
9
+ export function generateState() {
10
+ return base64url(crypto.getRandomValues(new Uint8Array(16)));
11
+ }
12
+ export function generateCodeVerifier() {
13
+ return base64url(crypto.getRandomValues(new Uint8Array(32)));
14
+ }
15
+ export async function computeS256Challenge(verifier) {
16
+ const encoded = new TextEncoder().encode(verifier);
17
+ const digest = await crypto.subtle.digest('SHA-256', encoded);
18
+ return base64url(new Uint8Array(digest));
19
+ }
20
+ // --- OAuth operations (parameterized via context) ---
21
+ export function buildAuthorizeURL(ctx, state, codeChallenge, orgHint) {
22
+ const redirectUri = `${ctx.env.ORIGIN}/auth/callback`;
23
+ const params = new URLSearchParams({
24
+ response_type: 'code',
25
+ client_id: ctx.config.clientId,
26
+ redirect_uri: redirectUri,
27
+ state,
28
+ code_challenge: codeChallenge,
29
+ code_challenge_method: 'S256',
30
+ scope: 'openid profile email'
31
+ });
32
+ if (orgHint) {
33
+ params.set('org_hint', orgHint);
34
+ }
35
+ return `${ctx.env.IAM_URL}/api/auth/oauth2/authorize?${params}`;
36
+ }
37
+ export async function exchangeCode(ctx, code, codeVerifier) {
38
+ const redirectUri = `${ctx.env.ORIGIN}/auth/callback`;
39
+ const res = await fetch(`${ctx.env.IAM_URL}/api/auth/oauth2/token`, {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/x-www-form-urlencoded',
43
+ Authorization: `Basic ${btoa(`${ctx.config.clientId}:${ctx.clientSecret}`)}`
44
+ },
45
+ body: new URLSearchParams({
46
+ grant_type: 'authorization_code',
47
+ code,
48
+ redirect_uri: redirectUri,
49
+ code_verifier: codeVerifier
50
+ })
51
+ });
52
+ if (!res.ok) {
53
+ const body = await res.text().catch(() => 'unknown error');
54
+ throw new Error(`Token exchange failed (${res.status}): ${body}`);
55
+ }
56
+ return res.json();
57
+ }
58
+ export class RefreshError extends Error {
59
+ constructor(message, status) {
60
+ super(message);
61
+ this.status = status;
62
+ this.name = 'RefreshError';
63
+ }
64
+ }
65
+ export async function refreshTokens(ctx, refreshToken) {
66
+ const res = await fetch(`${ctx.env.IAM_URL}/api/auth/oauth2/token`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/x-www-form-urlencoded',
70
+ Authorization: `Basic ${btoa(`${ctx.config.clientId}:${ctx.clientSecret}`)}`
71
+ },
72
+ body: new URLSearchParams({
73
+ grant_type: 'refresh_token',
74
+ refresh_token: refreshToken
75
+ })
76
+ });
77
+ if (!res.ok) {
78
+ const body = await res.text().catch(() => 'unknown error');
79
+ throw new RefreshError(`Token refresh failed (${res.status}): ${body}`, res.status);
80
+ }
81
+ return res.json();
82
+ }
83
+ export async function verifyIdToken(ctx, idToken) {
84
+ if (!ctx.jwksRef.current) {
85
+ ctx.jwksRef.current = jose.createRemoteJWKSet(new URL(`${ctx.env.IAM_URL}/api/auth/jwks`));
86
+ }
87
+ const { payload } = await jose.jwtVerify(idToken, ctx.jwksRef.current, {
88
+ issuer: `${ctx.env.IAM_URL}/api/auth`
89
+ });
90
+ const p = payload;
91
+ return {
92
+ sub: payload.sub,
93
+ email: p.email,
94
+ name: p.name,
95
+ email_verified: p.email_verified ?? false,
96
+ orgs: (Array.isArray(p.orgs) ? p.orgs : []),
97
+ permissions: p.permissions,
98
+ impersonatedBy: p.impersonatedBy
99
+ };
100
+ }
101
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { type MongoAbility } from '@casl/ability';
2
+ export type AppAbility = MongoAbility<[string, string]>;
3
+ /**
4
+ * Build a CASL ability from a permission map: { resource: action[] }
5
+ */
6
+ export declare function buildAbility(permissions: Record<string, string[]>): AppAbility;
7
+ /**
8
+ * Extract the active team's permission map from the full permissions object.
9
+ */
10
+ export declare function getTeamPermissions(allPermissions: Record<string, Record<string, string[]>> | undefined, teamId: string | null): Record<string, string[]>;
11
+ /**
12
+ * Server-side enforcement: throw 403 if the user lacks the given permission.
13
+ */
14
+ export declare function requirePermission(permissions: Record<string, Record<string, string[]>> | undefined, teamId: string | null, action: string, resource: string): void;
@@ -0,0 +1,33 @@
1
+ import { AbilityBuilder, createMongoAbility } from '@casl/ability';
2
+ import { error } from '@sveltejs/kit';
3
+ /**
4
+ * Build a CASL ability from a permission map: { resource: action[] }
5
+ */
6
+ export function buildAbility(permissions) {
7
+ const { can, build } = new AbilityBuilder(createMongoAbility);
8
+ for (const [resource, actions] of Object.entries(permissions)) {
9
+ for (const action of actions) {
10
+ can(action, resource);
11
+ }
12
+ }
13
+ return build();
14
+ }
15
+ /**
16
+ * Extract the active team's permission map from the full permissions object.
17
+ */
18
+ export function getTeamPermissions(allPermissions, teamId) {
19
+ if (!allPermissions || !teamId)
20
+ return {};
21
+ return allPermissions[teamId] ?? {};
22
+ }
23
+ /**
24
+ * Server-side enforcement: throw 403 if the user lacks the given permission.
25
+ */
26
+ export function requirePermission(permissions, teamId, action, resource) {
27
+ const teamPerms = getTeamPermissions(permissions, teamId);
28
+ const ability = buildAbility(teamPerms);
29
+ if (!ability.can(action, resource)) {
30
+ error(403, `Missing permission: ${action} on ${resource}`);
31
+ }
32
+ }
33
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAqB,MAAM,eAAe,CAAC;AACtF,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAItC;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,WAAqC;IACjE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,IAAI,cAAc,CAAa,kBAAkB,CAAC,CAAC;IAC1E,KAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/D,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC9B,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACvB,CAAC;IACF,CAAC;IACD,OAAO,KAAK,EAAE,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CACjC,cAAoE,EACpE,MAAqB;IAErB,IAAI,CAAC,cAAc,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAC1C,OAAO,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAChC,WAAiE,EACjE,MAAqB,EACrB,MAAc,EACd,QAAgB;IAEhB,MAAM,SAAS,GAAG,kBAAkB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;QACpC,KAAK,CAAC,GAAG,EAAE,uBAAuB,MAAM,OAAO,QAAQ,EAAE,CAAC,CAAC;IAC5D,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 handleCallback(event: RequestEvent, ctx: OAuthContext): Promise<Response>;