@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,188 @@
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 { validateAndRefreshSession } from "./sessionValidator.js";
8
+ /**
9
+ * Wraps a Harper resource to add automatic OAuth session validation
10
+ *
11
+ * This wrapper intercepts all resource method calls (get, post, put, patch, delete)
12
+ * and validates/refreshes OAuth tokens before passing the request to the original resource.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // In your application component:
17
+ * import { withOAuthValidation } from '@harperfast/oauth';
18
+ *
19
+ * export function handleApplication(scope) {
20
+ * // Get OAuth providers from the OAuth plugin
21
+ * const oauthPlugin = scope.parent.resources.get('oauth');
22
+ *
23
+ * // Wrap your protected resource
24
+ * const myResource = {
25
+ * async get(target, request) {
26
+ * // This code only runs if OAuth session is valid
27
+ * return { user: request.session.oauthUser };
28
+ * }
29
+ * };
30
+ *
31
+ * scope.resources.set('protected', withOAuthValidation(myResource, {
32
+ * providers: oauthPlugin.providers,
33
+ * requireAuth: true,
34
+ * logger: scope.logger
35
+ * }));
36
+ * }
37
+ * ```
38
+ */
39
+ export function withOAuthValidation(resource, options) {
40
+ const { providers, logger, requireAuth = false, onValidationError } = options;
41
+ // Create a proxy that wraps all resource methods
42
+ return new Proxy(resource, {
43
+ get(target, prop) {
44
+ const originalMethod = target[prop];
45
+ // Only wrap HTTP methods
46
+ if (!['get', 'post', 'put', 'patch', 'delete'].includes(prop)) {
47
+ return originalMethod;
48
+ }
49
+ // Return wrapped method with OAuth validation
50
+ return async function (...args) {
51
+ // Extract request from arguments (usually last or second argument)
52
+ const request = args.find((arg) => arg?.session !== undefined);
53
+ if (!request) {
54
+ // No request object found - just pass through
55
+ return originalMethod.apply(this, args);
56
+ }
57
+ // Check if session has OAuth data
58
+ const hasOAuth = request.session?.oauth !== undefined;
59
+ if (!hasOAuth) {
60
+ if (requireAuth) {
61
+ // OAuth authentication required but not present
62
+ const error = 'OAuth authentication required';
63
+ if (onValidationError) {
64
+ return onValidationError(request, error);
65
+ }
66
+ return {
67
+ status: 401,
68
+ body: {
69
+ error: 'Unauthorized',
70
+ message: error,
71
+ },
72
+ };
73
+ }
74
+ // OAuth not required, pass through
75
+ return originalMethod.apply(this, args);
76
+ }
77
+ // Get provider for this OAuth session
78
+ const providerName = request.session?.oauth?.provider;
79
+ if (!providerName) {
80
+ // No provider name in session - invalid OAuth data
81
+ if (request.session) {
82
+ delete request.session.oauth;
83
+ delete request.session.oauthUser;
84
+ }
85
+ if (requireAuth) {
86
+ const error = 'Invalid OAuth session data';
87
+ if (onValidationError) {
88
+ return onValidationError(request, error);
89
+ }
90
+ return {
91
+ status: 401,
92
+ body: {
93
+ error: 'Unauthorized',
94
+ message: error,
95
+ },
96
+ };
97
+ }
98
+ return originalMethod.apply(this, args);
99
+ }
100
+ const providerData = providers[providerName];
101
+ if (!providerData) {
102
+ logger?.warn?.(`OAuth provider '${providerName}' not found for session validation`);
103
+ // Provider not found - clear OAuth data and continue
104
+ if (request.session) {
105
+ delete request.session.oauth;
106
+ delete request.session.oauthUser;
107
+ }
108
+ if (requireAuth) {
109
+ const error = `OAuth provider '${providerName}' not configured`;
110
+ if (onValidationError) {
111
+ return onValidationError(request, error);
112
+ }
113
+ return {
114
+ status: 401,
115
+ body: {
116
+ error: 'Unauthorized',
117
+ message: error,
118
+ },
119
+ };
120
+ }
121
+ return originalMethod.apply(this, args);
122
+ }
123
+ // Validate and refresh session
124
+ const validation = await validateAndRefreshSession(request, providerData.provider, logger);
125
+ if (!validation.valid) {
126
+ // Session validation failed
127
+ logger?.info?.(`OAuth session validation failed: ${validation.error}`);
128
+ if (requireAuth) {
129
+ const error = validation.error || 'OAuth session expired';
130
+ if (onValidationError) {
131
+ return onValidationError(request, error);
132
+ }
133
+ return {
134
+ status: 401,
135
+ body: {
136
+ error: 'Unauthorized',
137
+ message: 'OAuth session expired. Please log in again.',
138
+ details: validation.error,
139
+ },
140
+ };
141
+ }
142
+ // Not requiring auth, but validation failed - continue without OAuth
143
+ return originalMethod.apply(this, args);
144
+ }
145
+ // Session is valid (and possibly refreshed)
146
+ if (validation.refreshed) {
147
+ logger?.debug?.(`OAuth token refreshed for ${providerName} session`);
148
+ }
149
+ // Call original method with validated/refreshed session
150
+ return originalMethod.apply(this, args);
151
+ };
152
+ },
153
+ });
154
+ }
155
+ /**
156
+ * Helper to get OAuth providers from the OAuth plugin
157
+ * Call this from your application to access the provider registry
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * import { getOAuthProviders } from '@harperfast/oauth';
162
+ *
163
+ * export function handleApplication(scope) {
164
+ * const providers = getOAuthProviders(scope);
165
+ * // Use providers with withOAuthValidation
166
+ * }
167
+ * ```
168
+ */
169
+ export function getOAuthProviders(scope) {
170
+ try {
171
+ // Try to get OAuth plugin from parent scope
172
+ const oauthResource = scope.parent?.resources?.get?.('oauth');
173
+ if (oauthResource?.providers) {
174
+ return oauthResource.providers;
175
+ }
176
+ // Try to get from same scope (if plugin is loaded at same level)
177
+ const localOAuth = scope.resources?.get?.('oauth');
178
+ if (localOAuth?.providers) {
179
+ return localOAuth.providers;
180
+ }
181
+ return null;
182
+ }
183
+ catch {
184
+ // OAuth module not loaded or accessible
185
+ return null;
186
+ }
187
+ }
188
+ //# sourceMappingURL=withOAuthValidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withOAuthValidation.js","sourceRoot":"","sources":["../../src/lib/withOAuthValidation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,326 @@
1
+ /**
2
+ * OAuth Plugin Type Definitions
3
+ */
4
+ import type { IncomingMessage } from 'node:http';
5
+ import type { Scope, User, RequestTarget } from 'harperdb';
6
+ /**
7
+ * OAuth Plugin Configuration
8
+ * Runtime configuration for the OAuth plugin
9
+ */
10
+ export interface OAuthPluginConfig {
11
+ /** Enable debug mode to expose additional endpoints and information (can be boolean or string from env var) */
12
+ debug?: boolean | string;
13
+ /** OAuth provider configurations */
14
+ providers?: Record<string, any>;
15
+ /** Default redirect URI for all providers */
16
+ redirectUri?: string;
17
+ /** Default post-login redirect path */
18
+ postLoginRedirect?: string;
19
+ /** Default OAuth scopes */
20
+ scope?: string;
21
+ /** Default username claim path */
22
+ usernameClaim?: string;
23
+ /** Default role assignment */
24
+ defaultRole?: string;
25
+ /** Lifecycle hooks */
26
+ hooks?: OAuthHooks;
27
+ }
28
+ /**
29
+ * OAuth Lifecycle Hooks
30
+ * Callbacks invoked at key points in the OAuth flow
31
+ */
32
+ export interface OAuthHooks {
33
+ /**
34
+ * Resolve OAuth provider configuration dynamically
35
+ *
36
+ * Called when a provider is not found in the static registry.
37
+ * Allows applications to implement multi-tenant OAuth by
38
+ * returning provider configurations based on naming conventions.
39
+ *
40
+ * Example: Provider name "okta-org_abc123" can be parsed to load
41
+ * organization-specific Okta configuration from database.
42
+ *
43
+ * @param providerName - Provider name from URL path (e.g., "okta-org_abc123")
44
+ * @param logger - Optional logger instance
45
+ * @returns Provider configuration or null if not found
46
+ * @throws Error if resolution fails (returns 500 to client)
47
+ *
48
+ * Security Requirements:
49
+ * - MUST validate tenant ID format before database lookup
50
+ * - MUST validate domain safety (SSRF protection)
51
+ * - MUST validate provider-specific configuration
52
+ * - MUST NOT return configurations for disabled/inactive tenants
53
+ * - SHOULD log all resolution attempts for audit trail
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * onResolveProvider: async (providerName, logger) => {
58
+ * // Parse provider name: "okta-org_abc123" → ["okta", "org_abc123"]
59
+ * const match = providerName.match(/^(okta|azure|auth0)-(.+)$/);
60
+ * if (!match) return null;
61
+ *
62
+ * const [, provider, tenantId] = match;
63
+ *
64
+ * // Validate tenant ID format
65
+ * validateTenantId(tenantId);
66
+ *
67
+ * // Query database for tenant config
68
+ * const org = await Organization.get(tenantId, context);
69
+ * if (!org?.oauthConfig?.enabled) return null;
70
+ *
71
+ * // Build and return provider config
72
+ * return buildProviderConfig(org.oauthConfig, provider);
73
+ * }
74
+ * ```
75
+ */
76
+ onResolveProvider?: (providerName: string, logger?: Logger) => Promise<OAuthProviderConfig | null>;
77
+ /**
78
+ * Called after successful OAuth login, before session is finalized
79
+ * Use this to provision users, assign roles, etc.
80
+ * @param oauthUser - The OAuth user information
81
+ * @param tokenResponse - The token response from the provider
82
+ * @param session - The current session object
83
+ * @param request - The HTTP request object
84
+ * @param provider - The provider name (e.g., 'github', 'google')
85
+ * @returns Optional data to merge into the session
86
+ */
87
+ onLogin?: (oauthUser: OAuthUser, tokenResponse: TokenResponse, session: any, request: any, provider: string) => Promise<Record<string, any> | void>;
88
+ /**
89
+ * Called before logout, before session is cleared
90
+ * Use this to clean up user-specific data
91
+ * @param session - The current session object
92
+ * @param request - The HTTP request object
93
+ */
94
+ onLogout?: (session: any, request: any) => Promise<void>;
95
+ /**
96
+ * Called after token refresh completes
97
+ * @param session - The updated session with new tokens
98
+ * @param refreshed - Whether tokens were actually refreshed
99
+ * @param request - The HTTP request object (may be undefined for background refresh)
100
+ */
101
+ onTokenRefresh?: (session: any, refreshed: boolean, request?: any) => Promise<void>;
102
+ }
103
+ /**
104
+ * OAuth Provider Configuration
105
+ * Configuration options for an OAuth 2.0/OIDC provider
106
+ */
107
+ export interface OAuthProviderConfig {
108
+ /** Provider identifier (e.g., 'github', 'google', 'azure') */
109
+ provider: string;
110
+ clientId: string;
111
+ clientSecret: string;
112
+ authorizationUrl: string;
113
+ tokenUrl: string;
114
+ userInfoUrl: string;
115
+ redirectUri?: string;
116
+ /** OAuth scopes to request (space-separated) */
117
+ scope?: string;
118
+ /** JWKS URI for ID token validation (OIDC only) */
119
+ jwksUri?: string | null;
120
+ /** Expected token issuer for validation (OIDC only) */
121
+ issuer?: string | null;
122
+ /** Claim to use as username (dot notation supported for nested) */
123
+ usernameClaim?: string;
124
+ /** Claim containing user's email address */
125
+ emailClaim?: string;
126
+ /** Claim containing user's display name */
127
+ nameClaim?: string;
128
+ /** Claim containing user's role/group membership */
129
+ roleClaim?: string;
130
+ /** Default role if not found in claims */
131
+ defaultRole?: string;
132
+ /** URL to redirect after successful login */
133
+ postLoginRedirect?: string;
134
+ /** Prefer ID token claims over userinfo endpoint (OIDC) */
135
+ preferIdToken?: boolean;
136
+ /** Whether to fetch email from provider's dedicated email endpoint */
137
+ fetchEmail?: boolean;
138
+ /** Additional query parameters for authorization URL */
139
+ additionalParams?: Record<string, string>;
140
+ /** Custom function to fetch/transform user info */
141
+ getUserInfo?: (accessToken: string, helpers: GetUserInfoHelpers) => Promise<any>;
142
+ /** Custom function to validate token (for non-expiring tokens like GitHub) */
143
+ validateToken?: (accessToken: string, logger?: Logger) => Promise<boolean>;
144
+ /** Interval for periodic token validation (ms) - only for tokens without expiration */
145
+ tokenValidationInterval?: number;
146
+ /** Provider-specific configuration function (e.g., for tenant/domain setup) */
147
+ configure?: (param: string) => Partial<OAuthProviderConfig>;
148
+ }
149
+ /**
150
+ * Helpers passed to custom getUserInfo function
151
+ */
152
+ export interface GetUserInfoHelpers {
153
+ /** Default getUserInfo implementation to call */
154
+ getUserInfo: (accessToken: string) => Promise<any>;
155
+ logger?: Logger;
156
+ }
157
+ /**
158
+ * OAuth User Object
159
+ * Represents a user authenticated via OAuth
160
+ */
161
+ export interface OAuthUser {
162
+ /** Username extracted from configured claim */
163
+ username: string;
164
+ role: string;
165
+ /** OAuth provider name */
166
+ provider: string;
167
+ /** User ID from the OAuth provider */
168
+ providerUserId?: string;
169
+ email?: string;
170
+ name?: string;
171
+ /** Additional provider-specific data */
172
+ metadata?: Record<string, any>;
173
+ }
174
+ /**
175
+ * OAuth Token Response
176
+ * Standard OAuth 2.0 token endpoint response
177
+ */
178
+ export interface TokenResponse {
179
+ access_token: string;
180
+ /** Usually 'Bearer' */
181
+ token_type?: string;
182
+ /** Token lifetime in seconds */
183
+ expires_in?: number;
184
+ refresh_token?: string;
185
+ /** ID token for OIDC providers */
186
+ id_token?: string;
187
+ /** Space-separated granted scopes */
188
+ scope?: string;
189
+ /** Error code if token request failed (some providers return 200 with error) */
190
+ error?: string;
191
+ /** Human-readable error description */
192
+ error_description?: string;
193
+ /** URL to documentation about the error */
194
+ error_uri?: string;
195
+ }
196
+ /**
197
+ * CSRF Token Data
198
+ * Metadata stored with CSRF tokens during OAuth flow
199
+ */
200
+ export interface CSRFTokenData {
201
+ /** Unix timestamp when token was created */
202
+ timestamp: number;
203
+ /** URL to redirect to after successful authentication */
204
+ originalUrl?: string;
205
+ /** Session ID to link OAuth flow with existing session */
206
+ sessionId?: string;
207
+ [key: string]: any;
208
+ }
209
+ /**
210
+ * OAuth Provider Interface
211
+ * Methods that OAuthProvider class implements
212
+ */
213
+ export interface IOAuthProvider {
214
+ config: OAuthProviderConfig;
215
+ logger?: Logger;
216
+ /** Generate CSRF token for OAuth state parameter */
217
+ generateCSRFToken(metadata: Record<string, any>): Promise<string>;
218
+ /** Verify and consume CSRF token (one-time use) */
219
+ verifyCSRFToken(token: string): Promise<CSRFTokenData | null>;
220
+ /** Build authorization URL with all required parameters */
221
+ getAuthorizationUrl(state: string, redirectUri: string): string;
222
+ /** Exchange authorization code for access/refresh tokens */
223
+ exchangeCodeForToken(code: string, redirectUri: string): Promise<TokenResponse>;
224
+ /** Fetch user information from provider */
225
+ getUserInfo(accessToken: string, idTokenClaims?: any): Promise<any>;
226
+ /** Map provider user info to Harper user format */
227
+ mapUserToHarper(userInfo: any): OAuthUser;
228
+ /** Verify and decode ID token (OIDC only) */
229
+ verifyIdToken?(idToken: string): Promise<any>;
230
+ /** Exchange refresh token for new access token */
231
+ refreshAccessToken?(refreshToken: string): Promise<TokenResponse>;
232
+ }
233
+ /**
234
+ * Provider Registry Entry
235
+ * Stores initialized provider instance with its config
236
+ */
237
+ export interface ProviderRegistryEntry {
238
+ /** Initialized OAuth provider instance */
239
+ provider: IOAuthProvider;
240
+ config: OAuthProviderConfig;
241
+ }
242
+ /**
243
+ * Provider Registry
244
+ * Collection of all configured OAuth providers keyed by name
245
+ */
246
+ export interface ProviderRegistry {
247
+ [providerName: string]: ProviderRegistryEntry;
248
+ }
249
+ /**
250
+ * Harper Table Instance
251
+ * Methods available on a Harper table
252
+ */
253
+ export interface Table {
254
+ get(id: string): Promise<any>;
255
+ put(record: any): Promise<any>;
256
+ delete(id: string): Promise<void>;
257
+ }
258
+ /**
259
+ * Logger Interface
260
+ * Harper's logger interface for component logging
261
+ */
262
+ export interface Logger {
263
+ info?: (message: string, ...args: any[]) => void;
264
+ error?: (message: string, ...args: any[]) => void;
265
+ warn?: (message: string, ...args: any[]) => void;
266
+ debug?: (message: string, ...args: any[]) => void;
267
+ }
268
+ /**
269
+ * Harper HTTP Request
270
+ * Extended Node.js IncomingMessage with Harper additions
271
+ */
272
+ export interface Request extends IncomingMessage {
273
+ /** Authenticated Harper user or username string */
274
+ user?: User | string;
275
+ session?: Session;
276
+ headers: IncomingMessage['headers'];
277
+ /** Client IP address */
278
+ ip?: string;
279
+ }
280
+ /**
281
+ * OAuth Session Metadata
282
+ * Token and expiration data stored in session for automatic refresh
283
+ */
284
+ export interface OAuthSessionMetadata {
285
+ /**
286
+ * Provider configuration ID/key from config (e.g., 'my-custom-github', 'production-okta').
287
+ * @deprecated Use `providerConfigId` instead. This field is maintained for backwards compatibility only.
288
+ */
289
+ provider: string;
290
+ /** Provider configuration ID/key from config (e.g., 'my-custom-github', 'production-okta'). */
291
+ providerConfigId: string;
292
+ /** OAuth provider type (e.g., 'github', 'google', 'okta') */
293
+ providerType: string;
294
+ /** Current access token */
295
+ accessToken: string;
296
+ /** Refresh token for obtaining new access tokens */
297
+ refreshToken?: string;
298
+ /** Unix timestamp (ms) when the access token expires */
299
+ expiresAt?: number;
300
+ /** Unix timestamp (ms) when to proactively refresh (80% of lifetime) */
301
+ refreshThreshold?: number;
302
+ /** Space-separated list of granted scopes */
303
+ scope?: string;
304
+ /** Token type (usually 'Bearer') */
305
+ tokenType?: string;
306
+ /** Unix timestamp (ms) of last successful token refresh */
307
+ lastRefreshed?: number;
308
+ /** Unix timestamp (ms) of last token validation (for non-expiring tokens) */
309
+ lastValidated?: number;
310
+ }
311
+ /**
312
+ * Harper Session
313
+ * Session data stored for authenticated users
314
+ */
315
+ export interface Session {
316
+ id?: string;
317
+ /** Harper username (string) */
318
+ user?: string;
319
+ /** Full OAuth user object */
320
+ oauthUser?: OAuthUser;
321
+ /** OAuth session metadata for automatic token refresh */
322
+ oauth?: OAuthSessionMetadata;
323
+ /** Async session update method (when available) */
324
+ update?: (data: Partial<Session>) => Promise<void>;
325
+ }
326
+ export type { Scope, User, RequestTarget };
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * OAuth Plugin Type Definitions
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@harperfast/oauth",
3
+ "version": "1.2.1",
4
+ "description": "OAuth 2.0 authentication plugin for Harper",
5
+ "license": "Apache-2.0",
6
+ "author": {
7
+ "name": "HarperDB, Inc.",
8
+ "email": "opensource@harperdb.io",
9
+ "url": "https://harper.fast/"
10
+ },
11
+ "contributors": [],
12
+ "homepage": "https://harper.fast/",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/HarperFast/oauth.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/HarperFast/oauth/issues",
19
+ "email": "opensource@harperdb.io"
20
+ },
21
+ "keywords": [
22
+ "harperdb",
23
+ "harper",
24
+ "plugin",
25
+ "oauth",
26
+ "oauth2",
27
+ "authentication",
28
+ "oidc",
29
+ "openid"
30
+ ],
31
+ "type": "module",
32
+ "main": "dist/index.js",
33
+ "exports": {
34
+ ".": "./dist/index.js",
35
+ "./config": "./config.yaml"
36
+ },
37
+ "files": [
38
+ "dist/",
39
+ "assets/",
40
+ "schema/",
41
+ "config.yaml",
42
+ "LICENSE",
43
+ "README.md"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsc || true",
47
+ "dev": "tsc --watch",
48
+ "test": "npm run build && node --test \"test/**/*.test.js\"",
49
+ "test:watch": "npm run build && node --test --watch \"test/**/*.test.js\"",
50
+ "test:coverage": "npm run build && node --enable-source-maps --test --experimental-test-coverage --test-coverage-exclude='test/**' --test-coverage-exclude='**/harperdb/**' \"test/**/*.test.js\"",
51
+ "test:node-twenty": "npm run build && node --test test/",
52
+ "lint": "eslint . --ignore-pattern 'dist/**'",
53
+ "format": "prettier .",
54
+ "format:check": "npm run format -- --check",
55
+ "format:write": "npm run format -- --write",
56
+ "prepublishOnly": "npm run build"
57
+ },
58
+ "dependencies": {
59
+ "jsonwebtoken": "^9.0.2",
60
+ "jwks-rsa": "^3.1.0"
61
+ },
62
+ "devDependencies": {
63
+ "@harperdb/code-guidelines": "^0.0.5",
64
+ "@types/jsonwebtoken": "^9.0.5",
65
+ "@types/node": "^20.11.0",
66
+ "eslint": "^9.35.0",
67
+ "harperdb": "^4.7.14",
68
+ "prettier": "^3.6.2",
69
+ "typescript": "^5.3.3"
70
+ },
71
+ "peerDependencies": {
72
+ "harperdb": ">=4.6.0"
73
+ },
74
+ "engines": {
75
+ "node": ">=20",
76
+ "bun": ">=1.0"
77
+ },
78
+ "devEngines": {
79
+ "runtime": {
80
+ "name": "node",
81
+ "version": ">=20",
82
+ "onFail": "error"
83
+ },
84
+ "packageManager": {
85
+ "name": "npm",
86
+ "onFail": "error"
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,21 @@
1
+ ## OAuth Plugin Schema
2
+ ## This defines the tables needed for OAuth functionality
3
+
4
+ ## CSRF Token storage table in oauth database
5
+ ## Stores temporary CSRF tokens for OAuth flows (10 minute expiration)
6
+ type csrf_tokens @table(database: "oauth", expiration: 600) {
7
+ token_id: ID @primaryKey
8
+ data: String # JSON stringified CSRFTokenData
9
+ created_at: Float
10
+ }
11
+
12
+ ## OAuth User Session table (optional, for future use)
13
+ ## Could store OAuth-specific user data
14
+ # type oauth_sessions @table(database: "oauth") {
15
+ # username: ID @primaryKey
16
+ # provider: String @indexed
17
+ # provider_id: String
18
+ # access_token: String
19
+ # refresh_token: String
20
+ # expires_at: Float
21
+ # }