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

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 +2880 -1667
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +810 -169
  17. package/dist/server.d.ts +810 -169
  18. package/dist/server.js +2848 -1646
  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 +365 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +335 -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 +285 -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 +338 -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,65 @@
1
+ /**
2
+ * Minimal postgres-js tagged-template client used for schema setup. `begin` is
3
+ * optional so the narrowest store clients (a bare tagged template) still work.
4
+ */
5
+ export interface PostgresDdlClient {
6
+ (template: TemplateStringsArray, ...args: unknown[]): Promise<Record<string, unknown>[]>
7
+ begin?: <T>(fn: (sql: PostgresDdlClient) => Promise<T>) => Promise<T>
8
+ }
9
+
10
+ /**
11
+ * Advisory-lock key serializing every Kora auth schema setup on one database
12
+ * ("Kora" in ASCII).
13
+ */
14
+ export const AUTH_SCHEMA_LOCK_KEY = 0x4b6f7261
15
+
16
+ /**
17
+ * SQLSTATEs a concurrent `CREATE ... IF NOT EXISTS` can still raise when two
18
+ * sessions create the same object at once: the check and the catalog insert are not
19
+ * atomic, so the loser fails on the catalog's unique index ("type ... already
20
+ * exists", 23505) or as a duplicate table / object (42P07 / 42710).
21
+ */
22
+ const CONCURRENT_DDL_CODES = new Set(['23505', '42P07', '42710'])
23
+
24
+ const MAX_ATTEMPTS = 3
25
+
26
+ /**
27
+ * Create a store's tables safely when several server instances start against an
28
+ * empty database at the same time.
29
+ *
30
+ * With a transaction-capable client the DDL runs inside one transaction holding
31
+ * {@link AUTH_SCHEMA_LOCK_KEY} as a transaction-scoped advisory lock, so concurrent
32
+ * first starts are serialized and the later ones find every object already there.
33
+ * A client without `begin` (or a database that still reports a creation race) is
34
+ * retried: the statements are idempotent (`IF NOT EXISTS`), so a retry after a lost
35
+ * race succeeds.
36
+ *
37
+ * @param sql - The postgres-js client
38
+ * @param ddl - Idempotent DDL statements, run with the client (or transaction) given
39
+ */
40
+ export async function ensurePostgresSchema(
41
+ sql: PostgresDdlClient,
42
+ ddl: (client: PostgresDdlClient) => Promise<void>,
43
+ ): Promise<void> {
44
+ for (let attempt = 1; ; attempt++) {
45
+ try {
46
+ if (typeof sql.begin === 'function') {
47
+ await sql.begin(async (tx) => {
48
+ await tx`SELECT pg_advisory_xact_lock(${AUTH_SCHEMA_LOCK_KEY}::bigint)`
49
+ await ddl(tx)
50
+ })
51
+ } else {
52
+ await ddl(sql)
53
+ }
54
+ return
55
+ } catch (error) {
56
+ if (attempt >= MAX_ATTEMPTS || !isConcurrentDdlError(error)) throw error
57
+ }
58
+ }
59
+ }
60
+
61
+ function isConcurrentDdlError(error: unknown): boolean {
62
+ if (error === null || typeof error !== 'object') return false
63
+ const code = (error as { code?: unknown }).code
64
+ return typeof code === 'string' && CONCURRENT_DDL_CODES.has(code)
65
+ }
@@ -0,0 +1,246 @@
1
+ import type { TokenManager } from '../tokens/token-manager'
2
+ import type { AuthTokens } from '../types'
3
+ import { type AuthRoutesConfig, BuiltInAuthRoutes } from './built-in/auth-routes'
4
+ import type { AuthDevice, AuthUser, UserStore } from './built-in/user-store'
5
+
6
+ /**
7
+ * Parameters for signing up a new user.
8
+ */
9
+ export interface SignUpParams {
10
+ /** The user's email address */
11
+ email: string
12
+ /** The plaintext password */
13
+ password: string
14
+ /** Optional display name */
15
+ name?: string
16
+ /** Optional device ID to register with the account */
17
+ deviceId?: string
18
+ /** Optional device public key (base64url) */
19
+ devicePublicKey?: string
20
+ }
21
+
22
+ /**
23
+ * Parameters for signing in an existing user.
24
+ */
25
+ export interface SignInParams {
26
+ /** The user's email address */
27
+ email: string
28
+ /** The plaintext password */
29
+ password: string
30
+ /** Optional device ID to register or associate */
31
+ deviceId?: string
32
+ /** Optional device public key (base64url) */
33
+ devicePublicKey?: string
34
+ }
35
+
36
+ /**
37
+ * Abstraction for authentication providers.
38
+ *
39
+ * This interface allows swapping between the built-in email/password provider
40
+ * and external providers (OAuth, SAML, custom) without changing application code.
41
+ * Every provider must support the same core operations: sign up, sign in,
42
+ * token refresh, token validation, user lookup, and device management.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // Use the built-in provider
47
+ * const provider: AuthProviderAdapter = new BuiltInProvider({
48
+ * userStore: new InMemoryUserStore(),
49
+ * tokenManager: new TokenManager({ secret: 'my-secret' }),
50
+ * })
51
+ *
52
+ * const { user, tokens } = await provider.signUp({
53
+ * email: 'alice@example.com',
54
+ * password: 'secure-password-123',
55
+ * })
56
+ * ```
57
+ */
58
+ export interface AuthProviderAdapter {
59
+ /**
60
+ * Create a new user account and issue authentication tokens.
61
+ *
62
+ * @param params - Sign-up parameters including email, password, and optional device info
63
+ * @returns The created user and tokens
64
+ * @throws If the email is already registered or input validation fails
65
+ */
66
+ signUp(params: SignUpParams): Promise<{ user: AuthUser; tokens: AuthTokens }>
67
+
68
+ /**
69
+ * Authenticate an existing user and issue tokens.
70
+ *
71
+ * @param params - Sign-in parameters including email and password
72
+ * @returns The authenticated user and tokens
73
+ * @throws If the credentials are invalid
74
+ */
75
+ signIn(params: SignInParams): Promise<{ user: AuthUser; tokens: AuthTokens }>
76
+
77
+ /**
78
+ * Exchange a refresh token for new tokens (rotation).
79
+ *
80
+ * @param refreshToken - The current refresh token
81
+ * @returns A new token pair
82
+ * @throws If the refresh token is invalid or expired
83
+ */
84
+ refreshTokens(refreshToken: string): Promise<AuthTokens>
85
+
86
+ /**
87
+ * Validate an access token and extract identity claims.
88
+ *
89
+ * @param token - The JWT access token
90
+ * @returns User ID and device ID if valid, or null if invalid/expired
91
+ */
92
+ validateAccessToken(token: string): Promise<{ userId: string; deviceId: string } | null>
93
+
94
+ /**
95
+ * Look up a user by ID.
96
+ *
97
+ * @param userId - The user's ID
98
+ * @returns The user profile, or null if not found
99
+ */
100
+ getUser(userId: string): Promise<AuthUser | null>
101
+
102
+ /**
103
+ * Revoke a device. Requires a valid access token for authorization.
104
+ *
105
+ * @param accessToken - The caller's access token
106
+ * @param deviceId - The ID of the device to revoke
107
+ * @throws If the token is invalid or the device does not belong to the caller
108
+ */
109
+ revokeDevice(accessToken: string, deviceId: string): Promise<void>
110
+
111
+ /**
112
+ * List all devices for the authenticated user.
113
+ *
114
+ * @param accessToken - The caller's access token
115
+ * @returns Array of device records
116
+ * @throws If the token is invalid
117
+ */
118
+ listDevices(accessToken: string): Promise<AuthDevice[]>
119
+ }
120
+
121
+ /**
122
+ * Error thrown by provider adapter methods when an operation fails.
123
+ * Wraps the HTTP-style status and error message from route handlers
124
+ * into an exception for use in the adapter pattern.
125
+ */
126
+ export class AuthProviderError extends Error {
127
+ /** HTTP-style status code */
128
+ readonly status: number
129
+
130
+ constructor(message: string, status: number) {
131
+ super(message)
132
+ this.name = 'AuthProviderError'
133
+ this.status = status
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Built-in authentication provider implementing email/password authentication.
139
+ *
140
+ * Wraps {@link BuiltInAuthRoutes} in the {@link AuthProviderAdapter} interface,
141
+ * converting HTTP-style responses into direct return values and exceptions.
142
+ * This is the default provider shipped with Kora and is suitable for
143
+ * applications that want simple email/password auth without external services.
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * import { BuiltInProvider } from '@korajs/auth'
148
+ * import { InMemoryUserStore } from '@korajs/auth'
149
+ * import { TokenManager } from '@korajs/auth'
150
+ *
151
+ * const provider = new BuiltInProvider({
152
+ * userStore: new InMemoryUserStore(),
153
+ * tokenManager: new TokenManager({ secret: process.env.AUTH_SECRET }),
154
+ * })
155
+ *
156
+ * const { user, tokens } = await provider.signUp({
157
+ * email: 'alice@example.com',
158
+ * password: 'strong-password-123',
159
+ * name: 'Alice',
160
+ * })
161
+ * ```
162
+ */
163
+ export class BuiltInProvider implements AuthProviderAdapter {
164
+ private readonly routes: BuiltInAuthRoutes
165
+ private readonly tokenManager: TokenManager
166
+ private readonly userStore: UserStore
167
+
168
+ constructor(config: AuthRoutesConfig) {
169
+ this.routes = new BuiltInAuthRoutes(config)
170
+ this.tokenManager = config.tokenManager
171
+ this.userStore = config.userStore
172
+ }
173
+
174
+ /** @inheritdoc */
175
+ async signUp(params: SignUpParams): Promise<{ user: AuthUser; tokens: AuthTokens }> {
176
+ const result = await this.routes.handleSignUp(params)
177
+ if ('error' in result.body) {
178
+ throw new AuthProviderError(result.body.error, result.status)
179
+ }
180
+ return result.body.data
181
+ }
182
+
183
+ /** @inheritdoc */
184
+ async signIn(params: SignInParams): Promise<{ user: AuthUser; tokens: AuthTokens }> {
185
+ const result = await this.routes.handleSignIn(params)
186
+ if ('error' in result.body) {
187
+ throw new AuthProviderError(result.body.error, result.status)
188
+ }
189
+ if ('mfaRequired' in result.body.data) {
190
+ // This adapter has no second-factor step; MFA users must sign in through
191
+ // the HTTP routes (POST /auth/mfa/verify).
192
+ throw new AuthProviderError('A second factor is required for this account.', 401)
193
+ }
194
+ return result.body.data
195
+ }
196
+
197
+ /** @inheritdoc */
198
+ async refreshTokens(refreshToken: string): Promise<AuthTokens> {
199
+ const result = await this.routes.handleRefresh({ refreshToken })
200
+ if ('error' in result.body) {
201
+ throw new AuthProviderError(result.body.error, result.status)
202
+ }
203
+ return result.body.data
204
+ }
205
+
206
+ /** @inheritdoc */
207
+ async validateAccessToken(token: string): Promise<{ userId: string; deviceId: string } | null> {
208
+ const payload = this.tokenManager.validateToken(token)
209
+ if (payload === null || payload.type !== 'access') {
210
+ return null
211
+ }
212
+ return { userId: payload.sub, deviceId: payload.dev }
213
+ }
214
+
215
+ /** @inheritdoc */
216
+ async getUser(userId: string): Promise<AuthUser | null> {
217
+ const stored = await this.userStore.findById(userId)
218
+ if (stored === null) {
219
+ return null
220
+ }
221
+ return {
222
+ id: stored.id,
223
+ email: stored.email,
224
+ name: stored.name,
225
+ emailVerified: stored.emailVerified,
226
+ createdAt: stored.createdAt,
227
+ }
228
+ }
229
+
230
+ /** @inheritdoc */
231
+ async revokeDevice(accessToken: string, deviceId: string): Promise<void> {
232
+ const result = await this.routes.handleRevokeDevice(accessToken, deviceId)
233
+ if ('error' in result.body) {
234
+ throw new AuthProviderError(result.body.error, result.status)
235
+ }
236
+ }
237
+
238
+ /** @inheritdoc */
239
+ async listDevices(accessToken: string): Promise<AuthDevice[]> {
240
+ const result = await this.routes.handleListDevices(accessToken)
241
+ if ('error' in result.body) {
242
+ throw new AuthProviderError(result.body.error, result.status)
243
+ }
244
+ return result.body.data
245
+ }
246
+ }