@cloudauth/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/LICENSE +21 -0
  2. package/dist/adapter.d.ts +20 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/adapter.js +2 -0
  5. package/dist/adapter.js.map +1 -0
  6. package/dist/additional-fields.d.ts +31 -0
  7. package/dist/additional-fields.d.ts.map +1 -0
  8. package/dist/additional-fields.js +2 -0
  9. package/dist/additional-fields.js.map +1 -0
  10. package/dist/auth.d.ts +26 -0
  11. package/dist/auth.d.ts.map +1 -0
  12. package/dist/auth.js +280 -0
  13. package/dist/auth.js.map +1 -0
  14. package/dist/config.d.ts +55 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +2 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/cookies.d.ts +29 -0
  19. package/dist/cookies.d.ts.map +1 -0
  20. package/dist/cookies.js +98 -0
  21. package/dist/cookies.js.map +1 -0
  22. package/dist/crypto.d.ts +23 -0
  23. package/dist/crypto.d.ts.map +1 -0
  24. package/dist/crypto.js +57 -0
  25. package/dist/crypto.js.map +1 -0
  26. package/dist/csrf.d.ts +6 -0
  27. package/dist/csrf.d.ts.map +1 -0
  28. package/dist/csrf.js +24 -0
  29. package/dist/csrf.js.map +1 -0
  30. package/dist/encryption.d.ts +10 -0
  31. package/dist/encryption.d.ts.map +1 -0
  32. package/dist/encryption.js +61 -0
  33. package/dist/encryption.js.map +1 -0
  34. package/dist/errors.d.ts +29 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +57 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/hooks.d.ts +45 -0
  39. package/dist/hooks.d.ts.map +1 -0
  40. package/dist/hooks.js +2 -0
  41. package/dist/hooks.js.map +1 -0
  42. package/dist/http.d.ts +23 -0
  43. package/dist/http.d.ts.map +1 -0
  44. package/dist/http.js +126 -0
  45. package/dist/http.js.map +1 -0
  46. package/dist/index.d.ts +23 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +53 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/provider.d.ts +19 -0
  51. package/dist/provider.d.ts.map +1 -0
  52. package/dist/provider.js +2 -0
  53. package/dist/provider.js.map +1 -0
  54. package/dist/rate-limiter.d.ts +33 -0
  55. package/dist/rate-limiter.d.ts.map +1 -0
  56. package/dist/rate-limiter.js +45 -0
  57. package/dist/rate-limiter.js.map +1 -0
  58. package/dist/test/adapter-conformance.d.ts +13 -0
  59. package/dist/test/adapter-conformance.d.ts.map +1 -0
  60. package/dist/test/adapter-conformance.js +212 -0
  61. package/dist/test/adapter-conformance.js.map +1 -0
  62. package/dist/test/cookies.test.d.ts +2 -0
  63. package/dist/test/cookies.test.d.ts.map +1 -0
  64. package/dist/test/cookies.test.js +83 -0
  65. package/dist/test/cookies.test.js.map +1 -0
  66. package/dist/test/crypto.test.d.ts +2 -0
  67. package/dist/test/crypto.test.d.ts.map +1 -0
  68. package/dist/test/crypto.test.js +84 -0
  69. package/dist/test/crypto.test.js.map +1 -0
  70. package/dist/test/encryption.test.d.ts +2 -0
  71. package/dist/test/encryption.test.d.ts.map +1 -0
  72. package/dist/test/encryption.test.js +34 -0
  73. package/dist/test/encryption.test.js.map +1 -0
  74. package/dist/test/factories.d.ts +11 -0
  75. package/dist/test/factories.d.ts.map +1 -0
  76. package/dist/test/factories.js +110 -0
  77. package/dist/test/factories.js.map +1 -0
  78. package/dist/test/index.d.ts +6 -0
  79. package/dist/test/index.d.ts.map +1 -0
  80. package/dist/test/index.js +4 -0
  81. package/dist/test/index.js.map +1 -0
  82. package/dist/test/mock-provider.d.ts +34 -0
  83. package/dist/test/mock-provider.d.ts.map +1 -0
  84. package/dist/test/mock-provider.js +99 -0
  85. package/dist/test/mock-provider.js.map +1 -0
  86. package/dist/test/rate-limiter.test.d.ts +2 -0
  87. package/dist/test/rate-limiter.test.d.ts.map +1 -0
  88. package/dist/test/rate-limiter.test.js +38 -0
  89. package/dist/test/rate-limiter.test.js.map +1 -0
  90. package/dist/test/url.test.d 2.ts +2 -0
  91. package/dist/test/url.test.d.ts +2 -0
  92. package/dist/test/url.test.d.ts 2.map +1 -0
  93. package/dist/test/url.test.d.ts.map +1 -0
  94. package/dist/test/url.test.js +40 -0
  95. package/dist/test/url.test.js 2.map +1 -0
  96. package/dist/test/url.test.js.map +1 -0
  97. package/dist/types.d.ts +109 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +2 -0
  100. package/dist/types.js.map +1 -0
  101. package/dist/url.d.ts +11 -0
  102. package/dist/url.d.ts.map +1 -0
  103. package/dist/url.js +35 -0
  104. package/dist/url.js.map +1 -0
  105. package/package.json +27 -0
  106. package/src/adapter.ts +39 -0
  107. package/src/additional-fields.ts +53 -0
  108. package/src/auth.ts +355 -0
  109. package/src/config.ts +62 -0
  110. package/src/cookies.ts +123 -0
  111. package/src/crypto.ts +62 -0
  112. package/src/csrf.ts +27 -0
  113. package/src/encryption.ts +88 -0
  114. package/src/errors.ts +65 -0
  115. package/src/hooks.ts +54 -0
  116. package/src/http.ts +192 -0
  117. package/src/index.ts +161 -0
  118. package/src/provider.ts +21 -0
  119. package/src/rate-limiter.ts +64 -0
  120. package/src/test/adapter-conformance.ts +267 -0
  121. package/src/test/cookies.test.ts +99 -0
  122. package/src/test/crypto.test.ts +103 -0
  123. package/src/test/encryption.test.ts +40 -0
  124. package/src/test/factories.ts +139 -0
  125. package/src/test/index.ts +17 -0
  126. package/src/test/mock-provider.ts +130 -0
  127. package/src/test/rate-limiter.test.ts +49 -0
  128. package/src/test/url.test.ts +48 -0
  129. package/src/types.ts +122 -0
  130. package/src/url.ts +35 -0
  131. package/tsconfig 2.tsbuildinfo +1 -0
  132. package/tsconfig.json +11 -0
  133. package/tsconfig.tsbuildinfo +1 -0
  134. package/vitest.config.ts +14 -0
package/src/csrf.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { isTrustedOrigin } from './url.js'
2
+
3
+ /**
4
+ * Validate that a request's Origin or Referer header matches a trusted origin.
5
+ * This is a basic CSRF protection for POST endpoints (logout, account linking).
6
+ */
7
+ export function validateCsrf(request: Request, trustedOrigins: string[]): boolean {
8
+ // Check Origin header first (more reliable)
9
+ const origin = request.headers.get('origin')
10
+ if (origin && isTrustedOrigin(origin, trustedOrigins)) {
11
+ return true
12
+ }
13
+
14
+ // Fall back to Referer header
15
+ const referer = request.headers.get('referer')
16
+ if (referer && isTrustedOrigin(referer, trustedOrigins)) {
17
+ return true
18
+ }
19
+
20
+ // If no Origin or Referer header, allow through (e.g., server-to-server)
21
+ // In production, this should be handled by SameSite cookies
22
+ if (!origin && !referer) {
23
+ return true
24
+ }
25
+
26
+ return false
27
+ }
@@ -0,0 +1,88 @@
1
+ const ALGORITHM = 'AES-GCM'
2
+ const KEY_LENGTH = 256
3
+ const IV_LENGTH = 12
4
+
5
+ /**
6
+ * Derive an AES-256-GCM key from a secret using PBKDF2.
7
+ * This allows using any string as the encryption key.
8
+ */
9
+ async function deriveKey(secret: string, salt: Uint8Array): Promise<CryptoKey> {
10
+ const keyMaterial = await crypto.subtle.importKey(
11
+ 'raw',
12
+ new TextEncoder().encode(secret),
13
+ 'PBKDF2',
14
+ false,
15
+ ['deriveKey'],
16
+ )
17
+
18
+ return crypto.subtle.deriveKey(
19
+ {
20
+ name: 'PBKDF2',
21
+ salt,
22
+ iterations: 100_000,
23
+ hash: 'SHA-256',
24
+ },
25
+ keyMaterial,
26
+ { name: ALGORITHM, length: KEY_LENGTH },
27
+ false,
28
+ ['encrypt', 'decrypt'],
29
+ )
30
+ }
31
+
32
+ /**
33
+ * Encrypt a plaintext string using AES-256-GCM.
34
+ * Returns a base64url-encoded string containing salt + iv + ciphertext.
35
+ */
36
+ export async function encrypt(plaintext: string, secret: string): Promise<string> {
37
+ const salt = crypto.getRandomValues(new Uint8Array(16))
38
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
39
+ const key = await deriveKey(secret, salt)
40
+
41
+ const ciphertext = await crypto.subtle.encrypt(
42
+ { name: ALGORITHM, iv },
43
+ key,
44
+ new TextEncoder().encode(plaintext),
45
+ )
46
+
47
+ // Combine salt + iv + ciphertext into a single base64url string
48
+ const combined = new Uint8Array(salt.length + iv.length + ciphertext.byteLength)
49
+ combined.set(salt, 0)
50
+ combined.set(iv, salt.length)
51
+ combined.set(new Uint8Array(ciphertext), salt.length + iv.length)
52
+
53
+ return base64url(combined)
54
+ }
55
+
56
+ /**
57
+ * Decrypt a base64url-encoded string that was encrypted with encrypt().
58
+ */
59
+ export async function decrypt(encoded: string, secret: string): Promise<string> {
60
+ const combined = base64urlDecode(encoded)
61
+ const salt = combined.slice(0, 16)
62
+ const iv = combined.slice(16, 16 + IV_LENGTH)
63
+ const ciphertext = combined.slice(16 + IV_LENGTH)
64
+
65
+ const key = await deriveKey(secret, salt)
66
+
67
+ const plaintext = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext)
68
+
69
+ return new TextDecoder().decode(plaintext)
70
+ }
71
+
72
+ function base64url(bytes: Uint8Array): string {
73
+ return btoa(String.fromCharCode(...bytes))
74
+ .replace(/\+/g, '-')
75
+ .replace(/\//g, '_')
76
+ .replace(/=+$/, '')
77
+ }
78
+
79
+ function base64urlDecode(str: string): Uint8Array {
80
+ const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
81
+ const padding = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4))
82
+ const binary = atob(base64 + padding)
83
+ const bytes = new Uint8Array(binary.length)
84
+ for (let i = 0; i < binary.length; i++) {
85
+ bytes[i] = binary.charCodeAt(i)
86
+ }
87
+ return bytes
88
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,65 @@
1
+ export class AuthError extends Error {
2
+ public readonly code: string
3
+
4
+ constructor(code: string, message: string) {
5
+ super(message)
6
+ this.name = 'AuthError'
7
+ this.code = code
8
+ }
9
+ }
10
+
11
+ export class OAuthCallbackError extends AuthError {
12
+ constructor(message: string, code = 'OAUTH_CALLBACK_FAILED') {
13
+ super(code, message)
14
+ this.name = 'OAuthCallbackError'
15
+ }
16
+ }
17
+
18
+ export class InvalidStateError extends AuthError {
19
+ constructor(message = 'OAuth state is invalid, expired, or already used') {
20
+ super('INVALID_STATE', message)
21
+ this.name = 'InvalidStateError'
22
+ }
23
+ }
24
+
25
+ export class ProviderProfileError extends AuthError {
26
+ constructor(message = 'Failed to fetch provider profile') {
27
+ super('PROVIDER_PROFILE_FAILED', message)
28
+ this.name = 'ProviderProfileError'
29
+ }
30
+ }
31
+
32
+ export class ProviderUnreachableError extends AuthError {
33
+ constructor(message = 'Provider is unreachable or timed out') {
34
+ super('PROVIDER_UNREACHABLE', message)
35
+ this.name = 'ProviderUnreachableError'
36
+ }
37
+ }
38
+
39
+ export class AccountLinkingError extends AuthError {
40
+ constructor(message: string, code = 'ACCOUNT_LINKING_FAILED') {
41
+ super(code, message)
42
+ this.name = 'AccountLinkingError'
43
+ }
44
+ }
45
+
46
+ export class SessionExpiredError extends AuthError {
47
+ constructor(message = 'Session has expired') {
48
+ super('SESSION_EXPIRED', message)
49
+ this.name = 'SessionExpiredError'
50
+ }
51
+ }
52
+
53
+ export class UnauthorizedError extends AuthError {
54
+ constructor(message = 'Authentication required') {
55
+ super('UNAUTHORIZED', message)
56
+ this.name = 'UnauthorizedError'
57
+ }
58
+ }
59
+
60
+ export class ForbiddenCallbackURLError extends AuthError {
61
+ constructor(message = 'Callback URL is not in trusted origins') {
62
+ super('INVALID_CALLBACK_URL', message)
63
+ this.name = 'ForbiddenCallbackURLError'
64
+ }
65
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { User, Account, Session, ProviderProfile } from './types.js'
2
+ import type { AuthAdapter } from './adapter.js'
3
+
4
+ export interface HookContext {
5
+ adapter: AuthAdapter
6
+ request?: Request
7
+ }
8
+
9
+ export interface BeforeSignInCtx extends HookContext {
10
+ profile: ProviderProfile
11
+ }
12
+
13
+ export interface AfterSignInCtx extends HookContext {
14
+ session: Session
15
+ user: User
16
+ profile: ProviderProfile
17
+ }
18
+
19
+ export interface OnUserCreatedCtx extends HookContext {
20
+ user: User
21
+ }
22
+
23
+ export interface OnAccountLinkedCtx extends HookContext {
24
+ account: Account
25
+ user: User
26
+ }
27
+
28
+ export interface BeforeSessionCreatedCtx extends HookContext {
29
+ user: User
30
+ }
31
+
32
+ export interface AfterSessionCreatedCtx extends HookContext {
33
+ session: Session
34
+ user: User
35
+ }
36
+
37
+ export interface BeforeLogoutCtx extends HookContext {
38
+ session: Session
39
+ }
40
+
41
+ export interface AfterLogoutCtx extends HookContext {
42
+ session: Session
43
+ }
44
+
45
+ export interface HooksConfig {
46
+ beforeSignIn?: (ctx: BeforeSignInCtx) => Promise<void>
47
+ afterSignIn?: (ctx: AfterSignInCtx) => Promise<void>
48
+ onUserCreated?: (ctx: OnUserCreatedCtx) => Promise<void>
49
+ onAccountLinked?: (ctx: OnAccountLinkedCtx) => Promise<void>
50
+ beforeSessionCreated?: (ctx: BeforeSessionCreatedCtx) => Promise<void>
51
+ afterSessionCreated?: (ctx: AfterSessionCreatedCtx) => Promise<void>
52
+ beforeLogout?: (ctx: BeforeLogoutCtx) => Promise<void>
53
+ afterLogout?: (ctx: AfterLogoutCtx) => Promise<void>
54
+ }
package/src/http.ts ADDED
@@ -0,0 +1,192 @@
1
+ import type { TokenSet, ProviderProfile } from './types.js'
2
+ import { ProviderUnreachableError, ProviderProfileError } from './errors.js'
3
+
4
+ export interface ProviderHttpOptions {
5
+ timeout?: number
6
+ retries?: number
7
+ retryDelay?: number
8
+ }
9
+
10
+ const DEFAULT_TIMEOUT = 10_000 // 10 seconds
11
+ const DEFAULT_RETRIES = 0
12
+ const DEFAULT_RETRY_DELAY = 1_000 // 1 second
13
+
14
+ interface TokenEndpointResponse {
15
+ access_token?: string
16
+ refresh_token?: string
17
+ id_token?: string
18
+ scope?: string
19
+ token_type?: string
20
+ expires_in?: number
21
+ }
22
+
23
+ interface UserinfoResponse {
24
+ sub?: string
25
+ id?: string
26
+ email?: string
27
+ email_verified?: boolean
28
+ name?: string
29
+ picture?: string
30
+ provider?: string
31
+ [key: string]: unknown
32
+ }
33
+
34
+ async function fetchWithTimeout(
35
+ url: string,
36
+ options: RequestInit & { timeout?: number },
37
+ ): Promise<Response> {
38
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT
39
+ const controller = new AbortController()
40
+ const timeoutId = setTimeout(() => {
41
+ controller.abort()
42
+ }, timeout)
43
+
44
+ try {
45
+ const response = await fetch(url, {
46
+ ...options,
47
+ signal: controller.signal,
48
+ })
49
+ return response
50
+ } finally {
51
+ clearTimeout(timeoutId)
52
+ }
53
+ }
54
+
55
+ function isNetworkError(error: unknown): boolean {
56
+ return (
57
+ error instanceof TypeError || (error instanceof DOMException && error.name === 'AbortError')
58
+ )
59
+ }
60
+
61
+ function sleep(ms: number): Promise<void> {
62
+ return new Promise((resolve) => {
63
+ setTimeout(resolve, ms)
64
+ })
65
+ }
66
+
67
+ /**
68
+ * Exchange an authorization code for tokens at the provider's token endpoint.
69
+ * Uses POST with application/x-www-form-urlencoded body.
70
+ */
71
+ export async function exchangeCode(
72
+ tokenEndpoint: string,
73
+ params: {
74
+ clientId: string
75
+ clientSecret: string
76
+ code: string
77
+ codeVerifier: string
78
+ redirectUri: string
79
+ },
80
+ options?: ProviderHttpOptions,
81
+ ): Promise<TokenSet> {
82
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT
83
+ const retries = options?.retries ?? DEFAULT_RETRIES
84
+ const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY
85
+
86
+ const body = new URLSearchParams({
87
+ client_id: params.clientId,
88
+ client_secret: params.clientSecret,
89
+ code: params.code,
90
+ code_verifier: params.codeVerifier,
91
+ redirect_uri: params.redirectUri,
92
+ grant_type: 'authorization_code',
93
+ })
94
+
95
+ let lastError: unknown
96
+
97
+ for (let attempt = 0; attempt <= retries; attempt++) {
98
+ try {
99
+ const response = await fetchWithTimeout(tokenEndpoint, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
102
+ body,
103
+ timeout,
104
+ })
105
+
106
+ if (response.ok) {
107
+ const data = (await response.json()) as TokenEndpointResponse
108
+ return {
109
+ accessToken: data.access_token ?? '',
110
+ refreshToken: data.refresh_token,
111
+ idToken: data.id_token,
112
+ scope: data.scope,
113
+ tokenType: data.token_type,
114
+ expiresAt: data.expires_in,
115
+ }
116
+ }
117
+
118
+ // Non-retryable HTTP error
119
+ const errorBody = await response.text().catch(() => 'unknown error')
120
+ throw new ProviderUnreachableError(
121
+ `Token endpoint returned ${String(response.status)}: ${errorBody}`,
122
+ )
123
+ } catch (error) {
124
+ lastError = error
125
+
126
+ // Only retry on network errors, not 4xx/5xx responses
127
+ if (isNetworkError(error) && attempt < retries) {
128
+ await sleep(retryDelay)
129
+ continue
130
+ }
131
+
132
+ throw error
133
+ }
134
+ }
135
+
136
+ throw lastError
137
+ }
138
+
139
+ /**
140
+ * Fetch a provider's user profile from the userinfo endpoint.
141
+ * The access token is sent as a Bearer token in the Authorization header.
142
+ */
143
+ export async function fetchProfile(
144
+ userinfoEndpoint: string,
145
+ accessToken: string,
146
+ options?: ProviderHttpOptions,
147
+ ): Promise<ProviderProfile> {
148
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT
149
+ const retries = options?.retries ?? DEFAULT_RETRIES
150
+ const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY
151
+
152
+ let lastError: unknown
153
+
154
+ for (let attempt = 0; attempt <= retries; attempt++) {
155
+ try {
156
+ const response = await fetchWithTimeout(userinfoEndpoint, {
157
+ method: 'GET',
158
+ headers: {
159
+ Authorization: `Bearer ${accessToken}`,
160
+ Accept: 'application/json',
161
+ },
162
+ timeout,
163
+ })
164
+
165
+ if (response.ok) {
166
+ const data = (await response.json()) as UserinfoResponse
167
+ return {
168
+ provider: data.provider ?? '',
169
+ providerAccountId: data.sub ?? data.id ?? '',
170
+ email: data.email,
171
+ emailVerified: data.email_verified,
172
+ name: data.name,
173
+ image: data.picture,
174
+ raw: data,
175
+ }
176
+ }
177
+
178
+ throw new ProviderProfileError(`Userinfo endpoint returned ${String(response.status)}`)
179
+ } catch (error) {
180
+ lastError = error
181
+
182
+ if (isNetworkError(error) && attempt < retries) {
183
+ await sleep(retryDelay)
184
+ continue
185
+ }
186
+
187
+ throw error
188
+ }
189
+ }
190
+
191
+ throw lastError
192
+ }
package/src/index.ts ADDED
@@ -0,0 +1,161 @@
1
+ // Types
2
+ export type {
3
+ User,
4
+ Account,
5
+ Session,
6
+ SessionWithUser,
7
+ OAuthState,
8
+ ProviderProfile,
9
+ TokenSet,
10
+ CreateUserInput,
11
+ UpdateUserInput,
12
+ CreateAccountInput,
13
+ CreateSessionInput,
14
+ UpdateSessionInput,
15
+ CreateOAuthStateInput,
16
+ } from './types.js'
17
+
18
+ // Provider interface
19
+ export type { AuthProvider, AuthorizationParams, ExchangeCodeParams } from './provider.js'
20
+
21
+ // Adapter interface
22
+ export type { AuthAdapter } from './adapter.js'
23
+
24
+ // Errors
25
+ export {
26
+ AuthError,
27
+ OAuthCallbackError,
28
+ InvalidStateError,
29
+ ProviderProfileError,
30
+ ProviderUnreachableError,
31
+ AccountLinkingError,
32
+ SessionExpiredError,
33
+ UnauthorizedError,
34
+ ForbiddenCallbackURLError,
35
+ } from './errors.js'
36
+
37
+ // Additional fields
38
+ export type {
39
+ AdditionalField,
40
+ AdditionalFieldsConfig,
41
+ StringField,
42
+ BooleanField,
43
+ NumberField,
44
+ DateField,
45
+ InferFieldType,
46
+ InferAdditionalFields,
47
+ InferTypes,
48
+ } from './additional-fields.js'
49
+
50
+ // Hooks
51
+ export type {
52
+ HookContext,
53
+ BeforeSignInCtx,
54
+ AfterSignInCtx,
55
+ OnUserCreatedCtx,
56
+ OnAccountLinkedCtx,
57
+ BeforeSessionCreatedCtx,
58
+ AfterSessionCreatedCtx,
59
+ BeforeLogoutCtx,
60
+ AfterLogoutCtx,
61
+ HooksConfig,
62
+ } from './hooks.js'
63
+
64
+ // Config
65
+ export type {
66
+ SessionConfig,
67
+ UserConfig,
68
+ AccountLinkingConfig,
69
+ SecurityConfig,
70
+ AuthConfig,
71
+ CloudAuthInstance,
72
+ } from './config.js'
73
+
74
+ // Crypto utilities
75
+ export {
76
+ generateSessionToken,
77
+ hashToken,
78
+ generateCodeVerifier,
79
+ generateCodeChallenge,
80
+ constantTimeEqual,
81
+ } from './crypto.js'
82
+
83
+ // Cookie utilities
84
+ export { serializeCookie, parseCookie, signCookie, verifyCookie } from './cookies.js'
85
+ export type { CookieOptions } from './cookies.js'
86
+
87
+ // URL utilities
88
+ export { isTrustedOrigin, buildCallbackURL } from './url.js'
89
+
90
+ // Provider HTTP client
91
+ export { exchangeCode, fetchProfile } from './http.js'
92
+ export type { ProviderHttpOptions } from './http.js'
93
+
94
+ // Rate limiter
95
+ export { inMemoryRateLimiter, defaultRateLimits } from './rate-limiter.js'
96
+ export type { RateLimiter, RateLimitResult } from './rate-limiter.js'
97
+
98
+ // CSRF protection
99
+ export { validateCsrf } from './csrf.js'
100
+
101
+ // Token encryption
102
+ export { encrypt, decrypt } from './encryption.js'
103
+
104
+ // Auth engine
105
+ export {
106
+ createAuthUrl,
107
+ handleCallback,
108
+ getSession as getSessionFromAuth,
109
+ signOut as signOutFromAuth,
110
+ } from './auth.js'
111
+ export type { AuthResponse } from './auth.js'
112
+
113
+ // --- cloudAuth function ---
114
+
115
+ import type { AuthConfig, CloudAuthInstance } from './config.js'
116
+ import {
117
+ createAuthUrl,
118
+ handleCallback,
119
+ getSession as getSessionFromAuth,
120
+ signOut as signOutFromAuth,
121
+ } from './auth.js'
122
+
123
+ export function cloudAuth(config: AuthConfig): CloudAuthInstance {
124
+ // Validate required config
125
+ if (!config.appName) {
126
+ throw new Error('cloudAuth: appName is required')
127
+ }
128
+ if (!config.baseURL) {
129
+ throw new Error('cloudAuth: baseURL is required')
130
+ }
131
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition */
132
+ if (!config.providers || config.providers.length === 0) {
133
+ throw new Error('cloudAuth: at least one provider is required')
134
+ }
135
+ if (!config.database) {
136
+ throw new Error('cloudAuth: database adapter is required')
137
+ }
138
+ /* eslint-enable @typescript-eslint/no-unnecessary-condition */
139
+
140
+ return {
141
+ $Infer: {} as CloudAuthInstance['$Infer'],
142
+
143
+ createAuthUrl(providerId: string, request?: Request): Promise<URL> {
144
+ return createAuthUrl(config, providerId, request)
145
+ },
146
+
147
+ handleCallback(providerId: string, request: Request) {
148
+ return handleCallback(config, providerId, request, config.hooks)
149
+ },
150
+
151
+ getSession(request: Request) {
152
+ return getSessionFromAuth(config, request)
153
+ },
154
+
155
+ signOut(request: Request) {
156
+ return signOutFromAuth(config, request, config.hooks)
157
+ },
158
+ }
159
+ }
160
+
161
+ export const version = '0.0.1'
@@ -0,0 +1,21 @@
1
+ import type { ProviderProfile, TokenSet } from './types.js'
2
+
3
+ export interface AuthorizationParams {
4
+ state: string
5
+ codeVerifier: string
6
+ redirectUri: string
7
+ }
8
+
9
+ export interface ExchangeCodeParams {
10
+ code: string
11
+ codeVerifier: string
12
+ redirectUri: string
13
+ }
14
+
15
+ export interface AuthProvider {
16
+ id: string
17
+ type: 'oauth' | 'oidc'
18
+ authorizationUrl(params: AuthorizationParams): URL
19
+ exchangeCode(params: ExchangeCodeParams): Promise<TokenSet>
20
+ getProfile(tokens: TokenSet): Promise<ProviderProfile>
21
+ }
@@ -0,0 +1,64 @@
1
+ export interface RateLimitResult {
2
+ allowed: boolean
3
+ remaining: number
4
+ resetAt: Date
5
+ }
6
+
7
+ export interface RateLimiter {
8
+ check(key: string, limit: number, windowMs: number): Promise<RateLimitResult>
9
+ }
10
+
11
+ interface WindowEntry {
12
+ timestamps: number[]
13
+ }
14
+
15
+ /**
16
+ * In-memory sliding window rate limiter.
17
+ * Suitable for single-instance development, NOT for production multi-instance use.
18
+ * For production, provide a Redis/D1-based implementation.
19
+ */
20
+ export function inMemoryRateLimiter(): RateLimiter {
21
+ const windows = new Map<string, WindowEntry>()
22
+
23
+ return {
24
+ check(key: string, limit: number, windowMs: number): Promise<RateLimitResult> {
25
+ return new Promise((resolve) => {
26
+ const now = Date.now()
27
+ let entry = windows.get(key)
28
+
29
+ if (!entry) {
30
+ entry = { timestamps: [] }
31
+ windows.set(key, entry)
32
+ }
33
+
34
+ // Remove timestamps outside the current window
35
+ const active = entry.timestamps.filter((ts) => now - ts < windowMs)
36
+ entry.timestamps = active
37
+
38
+ if (active.length >= limit) {
39
+ const oldest = active[0] ?? now
40
+ resolve({
41
+ allowed: false,
42
+ remaining: 0,
43
+ resetAt: new Date(oldest + windowMs),
44
+ })
45
+ return
46
+ }
47
+
48
+ active.push(now)
49
+ resolve({
50
+ allowed: true,
51
+ remaining: limit - active.length,
52
+ resetAt: new Date(now + windowMs),
53
+ })
54
+ })
55
+ },
56
+ }
57
+ }
58
+
59
+ export const defaultRateLimits = {
60
+ signIn: { limit: 10, windowMs: 60_000 },
61
+ callback: { limit: 20, windowMs: 60_000 },
62
+ logout: { limit: 30, windowMs: 60_000 },
63
+ linkAccount: { limit: 5, windowMs: 60_000 },
64
+ } as const