@harperfast/oauth 1.2.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 (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +219 -0
  3. package/assets/test.html +321 -0
  4. package/config.yaml +23 -0
  5. package/dist/index.d.ts +43 -0
  6. package/dist/index.js +241 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/lib/CSRFTokenManager.d.ts +32 -0
  9. package/dist/lib/CSRFTokenManager.js +90 -0
  10. package/dist/lib/CSRFTokenManager.js.map +1 -0
  11. package/dist/lib/OAuthProvider.d.ts +59 -0
  12. package/dist/lib/OAuthProvider.js +370 -0
  13. package/dist/lib/OAuthProvider.js.map +1 -0
  14. package/dist/lib/config.d.ts +31 -0
  15. package/dist/lib/config.js +138 -0
  16. package/dist/lib/config.js.map +1 -0
  17. package/dist/lib/handlers.d.ts +56 -0
  18. package/dist/lib/handlers.js +386 -0
  19. package/dist/lib/handlers.js.map +1 -0
  20. package/dist/lib/hookManager.d.ts +52 -0
  21. package/dist/lib/hookManager.js +114 -0
  22. package/dist/lib/hookManager.js.map +1 -0
  23. package/dist/lib/providers/auth0.d.ts +8 -0
  24. package/dist/lib/providers/auth0.js +34 -0
  25. package/dist/lib/providers/auth0.js.map +1 -0
  26. package/dist/lib/providers/azure.d.ts +7 -0
  27. package/dist/lib/providers/azure.js +33 -0
  28. package/dist/lib/providers/azure.js.map +1 -0
  29. package/dist/lib/providers/generic.d.ts +7 -0
  30. package/dist/lib/providers/generic.js +20 -0
  31. package/dist/lib/providers/generic.js.map +1 -0
  32. package/dist/lib/providers/github.d.ts +7 -0
  33. package/dist/lib/providers/github.js +73 -0
  34. package/dist/lib/providers/github.js.map +1 -0
  35. package/dist/lib/providers/google.d.ts +7 -0
  36. package/dist/lib/providers/google.js +27 -0
  37. package/dist/lib/providers/google.js.map +1 -0
  38. package/dist/lib/providers/index.d.ts +17 -0
  39. package/dist/lib/providers/index.js +49 -0
  40. package/dist/lib/providers/index.js.map +1 -0
  41. package/dist/lib/providers/okta.d.ts +8 -0
  42. package/dist/lib/providers/okta.js +45 -0
  43. package/dist/lib/providers/okta.js.map +1 -0
  44. package/dist/lib/providers/validation.d.ts +67 -0
  45. package/dist/lib/providers/validation.js +156 -0
  46. package/dist/lib/providers/validation.js.map +1 -0
  47. package/dist/lib/resource.d.ts +102 -0
  48. package/dist/lib/resource.js +368 -0
  49. package/dist/lib/resource.js.map +1 -0
  50. package/dist/lib/sessionValidator.d.ts +38 -0
  51. package/dist/lib/sessionValidator.js +162 -0
  52. package/dist/lib/sessionValidator.js.map +1 -0
  53. package/dist/lib/tenantManager.d.ts +102 -0
  54. package/dist/lib/tenantManager.js +177 -0
  55. package/dist/lib/tenantManager.js.map +1 -0
  56. package/dist/lib/withOAuthValidation.d.ts +64 -0
  57. package/dist/lib/withOAuthValidation.js +188 -0
  58. package/dist/lib/withOAuthValidation.js.map +1 -0
  59. package/dist/types.d.ts +326 -0
  60. package/dist/types.js +5 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +89 -0
  63. package/schema/oauth.graphql +21 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * OAuth Session Validation and Token Refresh
3
+ *
4
+ * Handles automatic validation and refresh of OAuth tokens in sessions
5
+ */
6
+ import type { Request, IOAuthProvider, Logger } from '../types.ts';
7
+ import type { HookManager } from './hookManager.ts';
8
+ export interface SessionValidationResult {
9
+ /** Whether the session has valid OAuth data */
10
+ valid: boolean;
11
+ /** Whether tokens were refreshed during validation */
12
+ refreshed?: boolean;
13
+ /** Error message if validation failed */
14
+ error?: string;
15
+ }
16
+ /**
17
+ * Validate OAuth session and refresh tokens if needed
18
+ *
19
+ * This function checks if OAuth tokens in the session are expired or approaching
20
+ * expiration, and automatically refreshes them if possible.
21
+ *
22
+ * Token refresh strategy:
23
+ * - If token is expired (past expiresAt), refresh immediately
24
+ * - If token is approaching expiration (past refreshThreshold = 80% of lifetime), refresh proactively
25
+ * - If no refresh token available and token is expired, clear OAuth session data
26
+ *
27
+ * @param request - Harper request object with session
28
+ * @param provider - OAuth provider instance for token refresh
29
+ * @param logger - Optional logger for debugging
30
+ * @param hookManager - Optional hook manager for calling onTokenRefresh hook
31
+ * @returns Validation result indicating if session is valid and if refresh occurred
32
+ */
33
+ export declare function validateAndRefreshSession(request: Request, provider: IOAuthProvider, logger?: Logger, hookManager?: HookManager): Promise<SessionValidationResult>;
34
+ /**
35
+ * Check if a session has valid OAuth authentication
36
+ * Does not refresh tokens, only checks validity
37
+ */
38
+ export declare function hasValidOAuthSession(request: Request): boolean;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * OAuth Session Validation and Token Refresh
3
+ *
4
+ * Handles automatic validation and refresh of OAuth tokens in sessions
5
+ */
6
+ import { clearOAuthSession } from "./handlers.js";
7
+ /**
8
+ * Validate OAuth session and refresh tokens if needed
9
+ *
10
+ * This function checks if OAuth tokens in the session are expired or approaching
11
+ * expiration, and automatically refreshes them if possible.
12
+ *
13
+ * Token refresh strategy:
14
+ * - If token is expired (past expiresAt), refresh immediately
15
+ * - If token is approaching expiration (past refreshThreshold = 80% of lifetime), refresh proactively
16
+ * - If no refresh token available and token is expired, clear OAuth session data
17
+ *
18
+ * @param request - Harper request object with session
19
+ * @param provider - OAuth provider instance for token refresh
20
+ * @param logger - Optional logger for debugging
21
+ * @param hookManager - Optional hook manager for calling onTokenRefresh hook
22
+ * @returns Validation result indicating if session is valid and if refresh occurred
23
+ */
24
+ export async function validateAndRefreshSession(request, provider, logger, hookManager) {
25
+ const session = request.session;
26
+ // No session available
27
+ if (!session) {
28
+ return { valid: false, error: 'No session available' };
29
+ }
30
+ // Check for OAuth metadata in session
31
+ const oauthMetadata = session.oauth;
32
+ if (!oauthMetadata) {
33
+ // No OAuth data in session - not an OAuth session
34
+ return { valid: false, error: 'No OAuth data in session' };
35
+ }
36
+ // Validate required fields
37
+ if (!oauthMetadata.accessToken) {
38
+ logger?.warn?.('OAuth session missing access token, logging out');
39
+ await clearOAuthSession(session, logger);
40
+ return { valid: false, error: 'OAuth session missing access token' };
41
+ }
42
+ const now = Date.now();
43
+ const isExpired = oauthMetadata.expiresAt ? now >= oauthMetadata.expiresAt : false;
44
+ const needsRefresh = oauthMetadata.refreshThreshold ? now >= oauthMetadata.refreshThreshold : false;
45
+ // For tokens without expiration (like GitHub), perform periodic validation
46
+ if (!oauthMetadata.expiresAt && !oauthMetadata.refreshThreshold && provider.config.validateToken) {
47
+ const validationInterval = provider.config.tokenValidationInterval || 15 * 60 * 1000; // Default 15 minutes
48
+ const lastCheck = oauthMetadata.lastValidated || oauthMetadata.lastRefreshed || 0;
49
+ const timeSinceLastCheck = now - lastCheck;
50
+ if (timeSinceLastCheck > validationInterval) {
51
+ logger?.debug?.('Performing periodic token validation for non-expiring token');
52
+ try {
53
+ const isValid = await provider.config.validateToken(oauthMetadata.accessToken, logger);
54
+ if (!isValid) {
55
+ logger?.debug?.('OAuth token validation failed (token revoked or invalid), logging out');
56
+ await clearOAuthSession(session, logger);
57
+ return { valid: false, error: 'Token validation failed - token may have been revoked' };
58
+ }
59
+ // Update last validated timestamp in session
60
+ oauthMetadata.lastValidated = now;
61
+ session.oauth = oauthMetadata;
62
+ if (typeof session.update === 'function') {
63
+ await session.update(session);
64
+ }
65
+ logger?.debug?.('Token validation successful');
66
+ }
67
+ catch (error) {
68
+ logger?.error?.('Token validation error:', error.message);
69
+ // Don't logout on validation errors - could be network issue
70
+ // Token will be validated again on next request
71
+ }
72
+ }
73
+ return { valid: true, refreshed: false };
74
+ }
75
+ // Token is still valid and doesn't need refresh
76
+ if (!isExpired && !needsRefresh) {
77
+ return { valid: true, refreshed: false };
78
+ }
79
+ // Token needs refresh - check if refresh token is available
80
+ if (!oauthMetadata.refreshToken) {
81
+ if (isExpired) {
82
+ logger?.warn?.('OAuth token expired and no refresh token available, logging out');
83
+ await clearOAuthSession(session, logger);
84
+ return { valid: false, error: 'Token expired and no refresh token available' };
85
+ }
86
+ // Token approaching expiration but no refresh token - still valid for now
87
+ return { valid: true, refreshed: false };
88
+ }
89
+ // Attempt to refresh the token
90
+ logger?.debug?.(isExpired
91
+ ? 'OAuth token expired, attempting refresh...'
92
+ : 'OAuth token approaching expiration (80% lifetime), refreshing proactively...');
93
+ try {
94
+ // Check if provider supports token refresh
95
+ if (!provider.refreshAccessToken) {
96
+ logger?.warn?.('OAuth provider does not support token refresh');
97
+ if (isExpired) {
98
+ await clearOAuthSession(session, logger);
99
+ return { valid: false, error: 'Token expired and provider does not support refresh' };
100
+ }
101
+ return { valid: true, refreshed: false };
102
+ }
103
+ // Attempt to refresh the token
104
+ logger?.debug?.('Attempting token refresh with refresh token');
105
+ // Perform token refresh
106
+ const tokenResponse = await provider.refreshAccessToken(oauthMetadata.refreshToken);
107
+ // Calculate new expiration times
108
+ const expiresIn = tokenResponse.expires_in || 3600; // Default 1 hour
109
+ const newExpiresAt = now + expiresIn * 1000;
110
+ const newRefreshThreshold = now + expiresIn * 800; // Refresh at 80% of lifetime
111
+ // Update session with new tokens and metadata
112
+ const updatedMetadata = {
113
+ provider: oauthMetadata.provider,
114
+ providerConfigId: oauthMetadata.providerConfigId,
115
+ providerType: oauthMetadata.providerType,
116
+ accessToken: tokenResponse.access_token,
117
+ refreshToken: tokenResponse.refresh_token || oauthMetadata.refreshToken, // Keep existing if not provided
118
+ expiresAt: newExpiresAt,
119
+ refreshThreshold: newRefreshThreshold,
120
+ scope: tokenResponse.scope || oauthMetadata.scope,
121
+ tokenType: tokenResponse.token_type || oauthMetadata.tokenType,
122
+ lastRefreshed: now,
123
+ lastValidated: oauthMetadata.lastValidated, // Preserve if exists
124
+ };
125
+ // Update session with refreshed token
126
+ session.oauth = updatedMetadata;
127
+ if (typeof session.update === 'function') {
128
+ await session.update(session);
129
+ }
130
+ logger?.info?.('OAuth token refreshed successfully');
131
+ // Call onTokenRefresh hook
132
+ if (hookManager) {
133
+ await hookManager.callOnTokenRefresh(session, true, request);
134
+ }
135
+ return { valid: true, refreshed: true };
136
+ }
137
+ catch (error) {
138
+ logger?.error?.('OAuth token refresh failed:', error.message);
139
+ // If token was expired and refresh failed, log out
140
+ if (isExpired) {
141
+ await clearOAuthSession(session, logger);
142
+ return { valid: false, error: `Token refresh failed: ${error.message}` };
143
+ }
144
+ // Token not yet expired, allow continued use
145
+ return { valid: true, refreshed: false };
146
+ }
147
+ }
148
+ /**
149
+ * Check if a session has valid OAuth authentication
150
+ * Does not refresh tokens, only checks validity
151
+ */
152
+ export function hasValidOAuthSession(request) {
153
+ const session = request.session;
154
+ if (!session)
155
+ return false;
156
+ const oauthMetadata = session.oauth;
157
+ if (!oauthMetadata || !oauthMetadata.accessToken)
158
+ return false;
159
+ // Check if token is expired - return true if valid, false if expired
160
+ return !(oauthMetadata.expiresAt && Date.now() >= oauthMetadata.expiresAt);
161
+ }
162
+ //# sourceMappingURL=sessionValidator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionValidator.js","sourceRoot":"","sources":["../../src/lib/sessionValidator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Multi-Tenant SSO Manager
3
+ *
4
+ * Enables dynamic tenant routing for enterprise SSO
5
+ * Supports Okta, Azure AD, Auth0, and any domain-based OAuth provider
6
+ */
7
+ import type { OAuthProviderConfig, Logger } from '../types.ts';
8
+ export interface TenantConfig {
9
+ /** Unique tenant identifier (e.g., 'acme-corp', 'globex') */
10
+ tenantId: string;
11
+ /** Tenant display name */
12
+ name: string;
13
+ /** OAuth provider type (okta, azure, auth0, etc.) */
14
+ provider: 'okta' | 'azure' | 'auth0' | string;
15
+ /** Provider-specific domain (for Okta/Auth0) or tenant ID (for Azure) */
16
+ domain?: string;
17
+ /** Azure AD specific tenant ID (alternative to domain) */
18
+ azureTenantId?: string;
19
+ /** OAuth client ID for this tenant */
20
+ clientId: string;
21
+ /** OAuth client secret for this tenant */
22
+ clientSecret: string;
23
+ /** Optional: Email domains that belong to this tenant */
24
+ emailDomains?: string[];
25
+ /** Optional: Custom scopes */
26
+ scope?: string;
27
+ /** Optional: Custom post-login redirect */
28
+ postLoginRedirect?: string;
29
+ /** Optional: Additional provider-specific config */
30
+ additionalConfig?: Record<string, any>;
31
+ }
32
+ export interface TenantRegistryEntry {
33
+ config: TenantConfig;
34
+ providerConfig: OAuthProviderConfig;
35
+ }
36
+ export declare class TenantManager {
37
+ private tenants;
38
+ private domainToTenant;
39
+ private logger?;
40
+ constructor(logger?: Logger);
41
+ /**
42
+ * Register a tenant with their OAuth provider configuration
43
+ * Supports Okta, Azure AD, Auth0, and custom providers
44
+ */
45
+ registerTenant(tenant: TenantConfig): void;
46
+ /**
47
+ * Register multiple tenants at once
48
+ */
49
+ registerTenants(tenants: TenantConfig[]): void;
50
+ /**
51
+ * Get tenant by ID
52
+ *
53
+ * ⚠️ **Security Warning**: Returns full tenant configuration including clientSecret.
54
+ * This method is intended for internal use (hooks, OAuth flows) only.
55
+ * Never expose the returned TenantRegistryEntry directly in HTTP responses.
56
+ *
57
+ * @example
58
+ * // ✅ Safe: Use in hooks/internal logic
59
+ * const tenant = tenantManager.getTenant(provider);
60
+ * return { tenantName: tenant?.config.name };
61
+ *
62
+ * // ❌ Unsafe: Direct HTTP exposure
63
+ * return { tenant: tenantManager.getTenant(id) }; // Leaks clientSecret!
64
+ */
65
+ getTenant(tenantId: string): TenantRegistryEntry | undefined;
66
+ /**
67
+ * Get tenant by email domain
68
+ *
69
+ * ⚠️ **Security Warning**: Returns full tenant configuration including clientSecret.
70
+ * This method is intended for internal use (hooks, OAuth flows) only.
71
+ * Never expose the returned TenantRegistryEntry directly in HTTP responses.
72
+ */
73
+ getTenantByEmail(email: string): TenantRegistryEntry | undefined;
74
+ /**
75
+ * Get all registered tenants
76
+ *
77
+ * Note: clientSecret is automatically redacted for security.
78
+ * Secrets are only needed internally for OAuth flows.
79
+ */
80
+ getAllTenants(): TenantConfig[];
81
+ /**
82
+ * Convert tenant configurations to provider registry format
83
+ * This can be merged with the standard provider registry
84
+ *
85
+ * ⚠️ **Security Warning**: Returns provider configurations including clientSecret.
86
+ * This method is intended for OAuth plugin initialization only.
87
+ * The returned configs are needed for OAuth token exchange flows.
88
+ * Never expose these configurations in HTTP responses.
89
+ */
90
+ toProviderRegistry(): Record<string, {
91
+ provider: any;
92
+ config: OAuthProviderConfig;
93
+ }>;
94
+ /**
95
+ * Load tenants from configuration
96
+ * Supports both static config and dynamic loading from database
97
+ */
98
+ static fromConfig(config: {
99
+ tenants?: TenantConfig[];
100
+ logger?: Logger;
101
+ }): TenantManager;
102
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Multi-Tenant SSO Manager
3
+ *
4
+ * Enables dynamic tenant routing for enterprise SSO
5
+ * Supports Okta, Azure AD, Auth0, and any domain-based OAuth provider
6
+ */
7
+ import { getProvider } from "./providers/index.js";
8
+ import { validateTenantId, validateEmailDomain } from "./providers/validation.js";
9
+ export class TenantManager {
10
+ tenants = new Map();
11
+ domainToTenant = new Map();
12
+ logger;
13
+ constructor(logger) {
14
+ this.logger = logger;
15
+ }
16
+ /**
17
+ * Register a tenant with their OAuth provider configuration
18
+ * Supports Okta, Azure AD, Auth0, and custom providers
19
+ */
20
+ registerTenant(tenant) {
21
+ // Validate tenant ID format
22
+ validateTenantId(tenant.tenantId);
23
+ // Validate email domains
24
+ if (tenant.emailDomains) {
25
+ for (const domain of tenant.emailDomains) {
26
+ validateEmailDomain(domain);
27
+ }
28
+ }
29
+ // Get the base provider configuration
30
+ const baseProvider = getProvider(tenant.provider);
31
+ if (!baseProvider) {
32
+ throw new Error(`Unknown provider type: ${tenant.provider}`);
33
+ }
34
+ // Apply provider-specific configuration
35
+ // Note: Provider configure() methods now include security validation
36
+ let providerSpecificConfig = {};
37
+ if (baseProvider.configure) {
38
+ switch (tenant.provider) {
39
+ case 'okta':
40
+ case 'auth0':
41
+ if (!tenant.domain) {
42
+ throw new Error(`${tenant.provider} provider requires domain configuration`);
43
+ }
44
+ providerSpecificConfig = baseProvider.configure(tenant.domain);
45
+ break;
46
+ case 'azure':
47
+ case 'microsoft':
48
+ if (!tenant.azureTenantId) {
49
+ throw new Error('Azure AD provider requires azureTenantId configuration');
50
+ }
51
+ providerSpecificConfig = baseProvider.configure(tenant.azureTenantId);
52
+ break;
53
+ default:
54
+ // For custom providers, pass domain or azureTenantId if available
55
+ const configParam = tenant.domain || tenant.azureTenantId;
56
+ if (configParam) {
57
+ providerSpecificConfig = baseProvider.configure(configParam);
58
+ }
59
+ }
60
+ }
61
+ // Build the complete provider configuration
62
+ const providerConfig = {
63
+ ...baseProvider,
64
+ ...providerSpecificConfig,
65
+ provider: tenant.provider,
66
+ clientId: tenant.clientId,
67
+ clientSecret: tenant.clientSecret,
68
+ scope: tenant.scope || baseProvider.scope,
69
+ postLoginRedirect: tenant.postLoginRedirect,
70
+ ...tenant.additionalConfig,
71
+ };
72
+ this.tenants.set(tenant.tenantId, {
73
+ config: tenant,
74
+ providerConfig,
75
+ });
76
+ // Map email domains to tenant (already validated above)
77
+ if (tenant.emailDomains) {
78
+ for (const domain of tenant.emailDomains) {
79
+ const normalizedDomain = domain.toLowerCase();
80
+ const existingTenantId = this.domainToTenant.get(normalizedDomain);
81
+ if (existingTenantId && existingTenantId !== tenant.tenantId) {
82
+ this.logger?.warn?.(`Email domain "${normalizedDomain}" is already mapped to tenant "${existingTenantId}". ` +
83
+ `Overwriting with tenant "${tenant.tenantId}".`);
84
+ }
85
+ this.domainToTenant.set(normalizedDomain, tenant.tenantId);
86
+ }
87
+ }
88
+ this.logger?.info?.(`Registered tenant: ${tenant.name} (${tenant.tenantId}) using ${tenant.provider} provider`);
89
+ }
90
+ /**
91
+ * Register multiple tenants at once
92
+ */
93
+ registerTenants(tenants) {
94
+ for (const tenant of tenants) {
95
+ this.registerTenant(tenant);
96
+ }
97
+ }
98
+ /**
99
+ * Get tenant by ID
100
+ *
101
+ * ⚠️ **Security Warning**: Returns full tenant configuration including clientSecret.
102
+ * This method is intended for internal use (hooks, OAuth flows) only.
103
+ * Never expose the returned TenantRegistryEntry directly in HTTP responses.
104
+ *
105
+ * @example
106
+ * // ✅ Safe: Use in hooks/internal logic
107
+ * const tenant = tenantManager.getTenant(provider);
108
+ * return { tenantName: tenant?.config.name };
109
+ *
110
+ * // ❌ Unsafe: Direct HTTP exposure
111
+ * return { tenant: tenantManager.getTenant(id) }; // Leaks clientSecret!
112
+ */
113
+ getTenant(tenantId) {
114
+ return this.tenants.get(tenantId);
115
+ }
116
+ /**
117
+ * Get tenant by email domain
118
+ *
119
+ * ⚠️ **Security Warning**: Returns full tenant configuration including clientSecret.
120
+ * This method is intended for internal use (hooks, OAuth flows) only.
121
+ * Never expose the returned TenantRegistryEntry directly in HTTP responses.
122
+ */
123
+ getTenantByEmail(email) {
124
+ const domain = email.split('@')[1]?.toLowerCase();
125
+ if (!domain)
126
+ return undefined;
127
+ const tenantId = this.domainToTenant.get(domain);
128
+ if (!tenantId)
129
+ return undefined;
130
+ return this.tenants.get(tenantId);
131
+ }
132
+ /**
133
+ * Get all registered tenants
134
+ *
135
+ * Note: clientSecret is automatically redacted for security.
136
+ * Secrets are only needed internally for OAuth flows.
137
+ */
138
+ getAllTenants() {
139
+ return Array.from(this.tenants.values()).map((entry) => ({
140
+ ...entry.config,
141
+ clientSecret: undefined, // Redact secret for security
142
+ }));
143
+ }
144
+ /**
145
+ * Convert tenant configurations to provider registry format
146
+ * This can be merged with the standard provider registry
147
+ *
148
+ * ⚠️ **Security Warning**: Returns provider configurations including clientSecret.
149
+ * This method is intended for OAuth plugin initialization only.
150
+ * The returned configs are needed for OAuth token exchange flows.
151
+ * Never expose these configurations in HTTP responses.
152
+ */
153
+ toProviderRegistry() {
154
+ const registry = {};
155
+ for (const [tenantId, entry] of this.tenants) {
156
+ // We'll need to create a provider instance here
157
+ // For now, return the config - the caller will need to instantiate OAuthProvider
158
+ registry[tenantId] = {
159
+ provider: null, // Will be instantiated by caller
160
+ config: entry.providerConfig,
161
+ };
162
+ }
163
+ return registry;
164
+ }
165
+ /**
166
+ * Load tenants from configuration
167
+ * Supports both static config and dynamic loading from database
168
+ */
169
+ static fromConfig(config) {
170
+ const manager = new TenantManager(config.logger);
171
+ if (config.tenants) {
172
+ manager.registerTenants(config.tenants);
173
+ }
174
+ return manager;
175
+ }
176
+ }
177
+ //# sourceMappingURL=tenantManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenantManager.js","sourceRoot":"","sources":["../../src/lib/tenantManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * OAuth Session Validation Wrapper
3
+ *
4
+ * Wraps Harper resources to add automatic OAuth session validation and token refresh
5
+ * before handling any request. This enables transparent token management for protected endpoints.
6
+ */
7
+ import type { Request, Logger, ProviderRegistry } from '../types.ts';
8
+ export interface OAuthValidationOptions {
9
+ /** OAuth provider registry from plugin initialization */
10
+ providers: ProviderRegistry;
11
+ /** Logger instance for debugging */
12
+ logger?: Logger;
13
+ /** Whether to require OAuth authentication (401 if not present) */
14
+ requireAuth?: boolean;
15
+ /** Custom error handler for validation failures */
16
+ onValidationError?: (request: Request, error: string) => any;
17
+ }
18
+ /**
19
+ * Wraps a Harper resource to add automatic OAuth session validation
20
+ *
21
+ * This wrapper intercepts all resource method calls (get, post, put, patch, delete)
22
+ * and validates/refreshes OAuth tokens before passing the request to the original resource.
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // In your application component:
27
+ * import { withOAuthValidation } from '@harperfast/oauth';
28
+ *
29
+ * export function handleApplication(scope) {
30
+ * // Get OAuth providers from the OAuth plugin
31
+ * const oauthPlugin = scope.parent.resources.get('oauth');
32
+ *
33
+ * // Wrap your protected resource
34
+ * const myResource = {
35
+ * async get(target, request) {
36
+ * // This code only runs if OAuth session is valid
37
+ * return { user: request.session.oauthUser };
38
+ * }
39
+ * };
40
+ *
41
+ * scope.resources.set('protected', withOAuthValidation(myResource, {
42
+ * providers: oauthPlugin.providers,
43
+ * requireAuth: true,
44
+ * logger: scope.logger
45
+ * }));
46
+ * }
47
+ * ```
48
+ */
49
+ export declare function withOAuthValidation(resource: any, options: OAuthValidationOptions): any;
50
+ /**
51
+ * Helper to get OAuth providers from the OAuth plugin
52
+ * Call this from your application to access the provider registry
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * import { getOAuthProviders } from '@harperfast/oauth';
57
+ *
58
+ * export function handleApplication(scope) {
59
+ * const providers = getOAuthProviders(scope);
60
+ * // Use providers with withOAuthValidation
61
+ * }
62
+ * ```
63
+ */
64
+ export declare function getOAuthProviders(scope: any): ProviderRegistry | null;