@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.13

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2852 -1675
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +779 -168
  17. package/dist/server.d.ts +779 -168
  18. package/dist/server.js +2831 -1665
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +328 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +322 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +272 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +334 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
@@ -0,0 +1,184 @@
1
+ import { KoraError } from '@korajs/core'
2
+
3
+ // ============================================================================
4
+ // OAuth Provider Configuration
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Configuration for an OAuth 2.0 provider.
9
+ */
10
+ export interface OAuthProviderConfig {
11
+ /** Provider identifier (e.g., 'google', 'github', 'microsoft') */
12
+ providerId: string
13
+ /** OAuth client ID */
14
+ clientId: string
15
+ /** OAuth client secret. Required for confidential server-side clients. */
16
+ clientSecret?: string
17
+ /** Authorization endpoint URL */
18
+ authorizationUrl: string
19
+ /** Token exchange endpoint URL */
20
+ tokenUrl: string
21
+ /** User info endpoint URL */
22
+ userInfoUrl: string
23
+ /** OAuth scopes to request */
24
+ scopes: string[]
25
+ /** Redirect URI for the callback */
26
+ redirectUri: string
27
+ /**
28
+ * Enable PKCE (Proof Key for Code Exchange).
29
+ *
30
+ * Required for public clients such as desktop and mobile apps, and recommended
31
+ * whenever the provider supports it.
32
+ */
33
+ pkce?: boolean
34
+ }
35
+
36
+ // ============================================================================
37
+ // OAuth Tokens
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Tokens returned by the OAuth provider after code exchange.
42
+ */
43
+ export interface OAuthTokens {
44
+ /** OAuth access token */
45
+ accessToken: string
46
+ /** Token type (usually 'Bearer') */
47
+ tokenType: string
48
+ /** Access token expiry in seconds (if provided) */
49
+ expiresIn?: number
50
+ /** Refresh token (if provided) */
51
+ refreshToken?: string
52
+ /** ID token (if provided, e.g., OpenID Connect) */
53
+ idToken?: string
54
+ /** Granted scopes (may differ from requested scopes) */
55
+ scope?: string
56
+ }
57
+
58
+ // ============================================================================
59
+ // OAuth User Info
60
+ // ============================================================================
61
+
62
+ /**
63
+ * User information from the OAuth provider.
64
+ */
65
+ export interface OAuthUserInfo {
66
+ /** Provider-specific user ID */
67
+ providerId: string
68
+ /** Provider name (e.g., 'google', 'github') */
69
+ provider: string
70
+ /** User's email address (may be null if not granted) */
71
+ email: string | null
72
+ /** Whether the email is verified by the provider */
73
+ emailVerified: boolean
74
+ /** User's display name */
75
+ name: string | null
76
+ /** URL to the user's avatar/profile picture */
77
+ avatarUrl: string | null
78
+ /** Raw profile data from the provider */
79
+ rawProfile: Record<string, unknown>
80
+ }
81
+
82
+ // ============================================================================
83
+ // OAuth State
84
+ // ============================================================================
85
+
86
+ /**
87
+ * State stored during the OAuth flow for CSRF protection.
88
+ */
89
+ export interface OAuthState {
90
+ /** Random state parameter for CSRF protection */
91
+ state: string
92
+ /** Provider ID */
93
+ provider: string
94
+ /** Redirect URI used for this flow */
95
+ redirectUri: string
96
+ /** When this state was created (ms since epoch) */
97
+ createdAt: number
98
+ /** When this state expires (ms since epoch) */
99
+ expiresAt: number
100
+ /** Optional: user-defined data to pass through the flow */
101
+ metadata?: Record<string, unknown>
102
+ /** PKCE code verifier, stored server-side until the callback is consumed. */
103
+ codeVerifier?: string
104
+ }
105
+
106
+ /**
107
+ * Store for OAuth state parameters.
108
+ */
109
+ export interface OAuthStateStore {
110
+ /** Store a state parameter for later validation. */
111
+ store(state: OAuthState): Promise<void>
112
+ /** Consume a state parameter (single-use). Returns null if not found or expired. */
113
+ consume(stateValue: string): Promise<OAuthState | null>
114
+ /** Clean up expired states. */
115
+ cleanExpired(): Promise<number>
116
+ }
117
+
118
+ // ============================================================================
119
+ // Linked Identity
120
+ // ============================================================================
121
+
122
+ /**
123
+ * A linked OAuth identity for a user.
124
+ * Users can have multiple linked identities (e.g., Google + GitHub).
125
+ */
126
+ export interface LinkedIdentity {
127
+ /** Unique ID of this link */
128
+ id: string
129
+ /** Kora user ID */
130
+ userId: string
131
+ /** OAuth provider name */
132
+ provider: string
133
+ /** Provider-specific user ID */
134
+ providerUserId: string
135
+ /** Provider email (at time of linking) */
136
+ email: string | null
137
+ /** When this identity was linked */
138
+ linkedAt: number
139
+ }
140
+
141
+ // ============================================================================
142
+ // Errors
143
+ // ============================================================================
144
+
145
+ export class OAuthError extends KoraError {
146
+ constructor(message: string, code: string, context?: Record<string, unknown>) {
147
+ super(message, code, context)
148
+ this.name = 'OAuthError'
149
+ }
150
+ }
151
+
152
+ export class OAuthStateMismatchError extends OAuthError {
153
+ constructor() {
154
+ super('OAuth state parameter does not match. Possible CSRF attack.', 'OAUTH_STATE_MISMATCH')
155
+ }
156
+ }
157
+
158
+ export class OAuthCodeExchangeError extends OAuthError {
159
+ constructor(details?: string) {
160
+ super(
161
+ `Failed to exchange authorization code for tokens.${details ? ` ${details}` : ''}`,
162
+ 'OAUTH_CODE_EXCHANGE_FAILED',
163
+ details ? { details } : undefined,
164
+ )
165
+ }
166
+ }
167
+
168
+ export class OAuthUserInfoError extends OAuthError {
169
+ constructor(details?: string) {
170
+ super(
171
+ `Failed to fetch user info from OAuth provider.${details ? ` ${details}` : ''}`,
172
+ 'OAUTH_USER_INFO_FAILED',
173
+ details ? { details } : undefined,
174
+ )
175
+ }
176
+ }
177
+
178
+ export class OAuthProviderNotFoundError extends OAuthError {
179
+ constructor(provider: string) {
180
+ super(`OAuth provider "${provider}" is not configured.`, 'OAUTH_PROVIDER_NOT_FOUND', {
181
+ provider,
182
+ })
183
+ }
184
+ }
@@ -0,0 +1,296 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { ensurePostgresSchema } from '../../postgres/ensure-schema'
3
+ import { DuplicateLinkedIdentityError, type LinkedIdentityStore } from './linked-identity-store'
4
+ import type { LinkedIdentity, OAuthState, OAuthStateStore } from './oauth-types'
5
+
6
+ interface PostgresClient {
7
+ begin<T>(fn: (sql: PostgresClient) => Promise<T>): Promise<T>
8
+ (template: TemplateStringsArray, ...args: unknown[]): Promise<Record<string, unknown>[]>
9
+ }
10
+
11
+ interface OAuthStateRow {
12
+ state: string
13
+ provider: string
14
+ redirect_uri: string
15
+ created_at: string | number
16
+ expires_at: string | number
17
+ metadata_json: string | null
18
+ code_verifier: string | null
19
+ }
20
+
21
+ interface LinkedIdentityRow {
22
+ id: string
23
+ user_id: string
24
+ provider: string
25
+ provider_user_id: string
26
+ email: string | null
27
+ linked_at: string | number
28
+ }
29
+
30
+ export class PostgresOAuthStateStore implements OAuthStateStore {
31
+ private readonly sql: PostgresClient
32
+ private readonly ready: Promise<void>
33
+
34
+ constructor(sql: PostgresClient) {
35
+ this.sql = sql
36
+ this.ready = this.ensureTables()
37
+ }
38
+
39
+ async store(state: OAuthState): Promise<void> {
40
+ await this.ready
41
+ await this.sql`
42
+ INSERT INTO auth_oauth_states
43
+ (state, provider, redirect_uri, created_at, expires_at, metadata_json, code_verifier)
44
+ VALUES (
45
+ ${state.state},
46
+ ${state.provider},
47
+ ${state.redirectUri},
48
+ ${state.createdAt},
49
+ ${state.expiresAt},
50
+ ${state.metadata ? JSON.stringify(state.metadata) : null},
51
+ ${state.codeVerifier ?? null}
52
+ )
53
+ ON CONFLICT (state) DO UPDATE SET
54
+ provider = EXCLUDED.provider,
55
+ redirect_uri = EXCLUDED.redirect_uri,
56
+ created_at = EXCLUDED.created_at,
57
+ expires_at = EXCLUDED.expires_at,
58
+ metadata_json = EXCLUDED.metadata_json,
59
+ code_verifier = EXCLUDED.code_verifier
60
+ `
61
+ }
62
+
63
+ async consume(stateValue: string): Promise<OAuthState | null> {
64
+ await this.ready
65
+ return this.sql.begin(async (tx) => {
66
+ const rows = (await tx`
67
+ DELETE FROM auth_oauth_states
68
+ WHERE state = ${stateValue}
69
+ RETURNING *
70
+ `) as unknown as OAuthStateRow[]
71
+
72
+ const row = rows[0]
73
+ if (!row || Date.now() > Number(row.expires_at)) {
74
+ return null
75
+ }
76
+ return rowToOAuthState(row)
77
+ })
78
+ }
79
+
80
+ async cleanExpired(): Promise<number> {
81
+ await this.ready
82
+ const rows = await this.sql`
83
+ DELETE FROM auth_oauth_states
84
+ WHERE expires_at < ${Date.now()}
85
+ RETURNING state
86
+ `
87
+ return rows.length
88
+ }
89
+
90
+ private async ensureTables(): Promise<void> {
91
+ // Concurrency-safe on an empty database shared by several instances.
92
+ await ensurePostgresSchema(this.sql, async (sql) => {
93
+ await sql`
94
+ CREATE TABLE IF NOT EXISTS auth_oauth_states (
95
+ state TEXT PRIMARY KEY,
96
+ provider TEXT NOT NULL,
97
+ redirect_uri TEXT NOT NULL,
98
+ created_at BIGINT NOT NULL,
99
+ expires_at BIGINT NOT NULL,
100
+ metadata_json TEXT,
101
+ code_verifier TEXT
102
+ )
103
+ `
104
+
105
+ await sql`
106
+ CREATE INDEX IF NOT EXISTS idx_auth_oauth_states_expires_at
107
+ ON auth_oauth_states(expires_at)
108
+ `
109
+ })
110
+ }
111
+ }
112
+
113
+ export class PostgresLinkedIdentityStore implements LinkedIdentityStore {
114
+ private readonly sql: PostgresClient
115
+ private readonly ready: Promise<void>
116
+
117
+ constructor(sql: PostgresClient) {
118
+ this.sql = sql
119
+ this.ready = this.ensureTables()
120
+ }
121
+
122
+ async findByProvider(provider: string, providerUserId: string): Promise<LinkedIdentity | null> {
123
+ await this.ready
124
+ const rows = (await this.sql`
125
+ SELECT * FROM auth_linked_identities
126
+ WHERE provider = ${provider} AND provider_user_id = ${providerUserId}
127
+ `) as unknown as LinkedIdentityRow[]
128
+
129
+ return rows[0] ? rowToLinkedIdentity(rows[0]) : null
130
+ }
131
+
132
+ async findByUser(userId: string): Promise<LinkedIdentity[]> {
133
+ await this.ready
134
+ const rows = (await this.sql`
135
+ SELECT * FROM auth_linked_identities
136
+ WHERE user_id = ${userId}
137
+ ORDER BY linked_at ASC
138
+ `) as unknown as LinkedIdentityRow[]
139
+
140
+ return rows.map(rowToLinkedIdentity)
141
+ }
142
+
143
+ async create(params: {
144
+ userId: string
145
+ provider: string
146
+ providerUserId: string
147
+ email: string | null
148
+ }): Promise<LinkedIdentity> {
149
+ await this.ready
150
+ const identity: LinkedIdentity = {
151
+ id: randomUUID(),
152
+ userId: params.userId,
153
+ provider: params.provider,
154
+ providerUserId: params.providerUserId,
155
+ email: params.email,
156
+ linkedAt: Date.now(),
157
+ }
158
+
159
+ try {
160
+ await this.sql`
161
+ INSERT INTO auth_linked_identities
162
+ (id, user_id, provider, provider_user_id, email, linked_at)
163
+ VALUES (
164
+ ${identity.id},
165
+ ${identity.userId},
166
+ ${identity.provider},
167
+ ${identity.providerUserId},
168
+ ${identity.email},
169
+ ${identity.linkedAt}
170
+ )
171
+ `
172
+ } catch (error) {
173
+ if (isUniqueViolation(error)) {
174
+ throw new DuplicateLinkedIdentityError(params.provider)
175
+ }
176
+ throw error
177
+ }
178
+
179
+ return identity
180
+ }
181
+
182
+ async delete(userId: string, provider: string): Promise<void> {
183
+ await this.ready
184
+ await this.sql`
185
+ DELETE FROM auth_linked_identities
186
+ WHERE user_id = ${userId} AND provider = ${provider}
187
+ `
188
+ }
189
+
190
+ private async ensureTables(): Promise<void> {
191
+ // Concurrency-safe on an empty database shared by several instances.
192
+ await ensurePostgresSchema(this.sql, async (sql) => {
193
+ await sql`
194
+ CREATE TABLE IF NOT EXISTS auth_linked_identities (
195
+ id TEXT PRIMARY KEY,
196
+ user_id TEXT NOT NULL,
197
+ provider TEXT NOT NULL,
198
+ provider_user_id TEXT NOT NULL,
199
+ email TEXT,
200
+ linked_at BIGINT NOT NULL,
201
+ UNIQUE(provider, provider_user_id),
202
+ UNIQUE(user_id, provider)
203
+ )
204
+ `
205
+
206
+ await sql`
207
+ CREATE INDEX IF NOT EXISTS idx_auth_linked_identities_user_id
208
+ ON auth_linked_identities(user_id)
209
+ `
210
+ })
211
+ }
212
+ }
213
+
214
+ export async function createPostgresOAuthStateStore(options: {
215
+ connectionString: string
216
+ }): Promise<PostgresOAuthStateStore> {
217
+ const postgresClient = await loadPostgresDeps()
218
+ const sql = postgresClient(options.connectionString) as unknown as PostgresClient
219
+ return new PostgresOAuthStateStore(sql)
220
+ }
221
+
222
+ export async function createPostgresLinkedIdentityStore(options: {
223
+ connectionString: string
224
+ }): Promise<PostgresLinkedIdentityStore> {
225
+ const postgresClient = await loadPostgresDeps()
226
+ const sql = postgresClient(options.connectionString) as unknown as PostgresClient
227
+ return new PostgresLinkedIdentityStore(sql)
228
+ }
229
+
230
+ export async function createPostgresOAuthStores(options: { connectionString: string }): Promise<{
231
+ stateStore: PostgresOAuthStateStore
232
+ linkedIdentityStore: PostgresLinkedIdentityStore
233
+ }> {
234
+ const postgresClient = await loadPostgresDeps()
235
+ const sql = postgresClient(options.connectionString) as unknown as PostgresClient
236
+ return {
237
+ stateStore: new PostgresOAuthStateStore(sql),
238
+ linkedIdentityStore: new PostgresLinkedIdentityStore(sql),
239
+ }
240
+ }
241
+
242
+ async function loadPostgresDeps(): Promise<(connectionString: string) => unknown> {
243
+ try {
244
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (
245
+ specifier: string,
246
+ ) => Promise<unknown>
247
+
248
+ const postgresMod = (await dynamicImport('postgres')) as { default: (cs: string) => unknown }
249
+ return postgresMod.default
250
+ } catch {
251
+ throw new Error(
252
+ 'PostgreSQL OAuth stores require the "postgres" package. Install it in your project dependencies.',
253
+ )
254
+ }
255
+ }
256
+
257
+ function rowToOAuthState(row: OAuthStateRow): OAuthState {
258
+ return {
259
+ state: row.state,
260
+ provider: row.provider,
261
+ redirectUri: row.redirect_uri,
262
+ createdAt: Number(row.created_at),
263
+ expiresAt: Number(row.expires_at),
264
+ metadata: parseMetadata(row.metadata_json),
265
+ codeVerifier: row.code_verifier ?? undefined,
266
+ }
267
+ }
268
+
269
+ function parseMetadata(value: string | null): Record<string, unknown> | undefined {
270
+ if (!value) return undefined
271
+ const parsed = JSON.parse(value) as unknown
272
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
273
+ ? (parsed as Record<string, unknown>)
274
+ : undefined
275
+ }
276
+
277
+ function rowToLinkedIdentity(row: LinkedIdentityRow): LinkedIdentity {
278
+ return {
279
+ id: row.id,
280
+ userId: row.user_id,
281
+ provider: row.provider,
282
+ providerUserId: row.provider_user_id,
283
+ email: row.email,
284
+ linkedAt: Number(row.linked_at),
285
+ }
286
+ }
287
+
288
+ function isUniqueViolation(error: unknown): boolean {
289
+ if (!(error instanceof Error)) return false
290
+ const code = (error as Error & { code?: string }).code
291
+ return (
292
+ code === '23505' ||
293
+ error.message.includes('unique constraint') ||
294
+ error.message.includes('duplicate key')
295
+ )
296
+ }