@korajs/auth 1.0.0-beta.11 → 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.
- package/README.md +52 -47
- package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
- package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
- package/dist/index.cjs +645 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -9
- package/dist/index.d.ts +27 -9
- package/dist/index.js +644 -150
- package/dist/index.js.map +1 -1
- package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
- package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
- package/dist/react.d.cts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/server.cjs +2852 -1675
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +779 -168
- package/dist/server.d.ts +779 -168
- package/dist/server.js +2831 -1665
- package/dist/server.js.map +1 -1
- package/dist/svelte.cjs +2 -2
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.d.cts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +2 -2
- package/dist/svelte.js.map +1 -1
- package/dist/vue.d.cts +1 -1
- package/dist/vue.d.ts +1 -1
- package/package.json +7 -7
- package/src/admin/admin-api.ts +327 -0
- package/src/admin/audit-log.ts +324 -0
- package/src/admin/webhooks.ts +576 -0
- package/src/bindings/create-auth-session.ts +184 -0
- package/src/bindings/create-org-session.ts +130 -0
- package/src/client/auth-client.ts +1592 -0
- package/src/client/auth-sync.ts +213 -0
- package/src/client/device-session.ts +104 -0
- package/src/client/org-client.ts +399 -0
- package/src/client/quickstart.ts +108 -0
- package/src/client/storage.ts +94 -0
- package/src/device/device-identity.ts +330 -0
- package/src/device/device-store.ts +379 -0
- package/src/encryption/auto-lock.ts +170 -0
- package/src/encryption/database-encryption.ts +265 -0
- package/src/encryption/key-derivation.ts +149 -0
- package/src/encryption/operation-encryptor.ts +361 -0
- package/src/index.ts +132 -0
- package/src/mfa/totp.ts +826 -0
- package/src/org/org-routes.ts +758 -0
- package/src/org/org-store.ts +490 -0
- package/src/org/org-types.ts +230 -0
- package/src/passkey/passkey-client.ts +597 -0
- package/src/passkey/passkey-server.ts +779 -0
- package/src/postgres/ensure-schema.ts +65 -0
- package/src/provider/adapter.ts +246 -0
- package/src/provider/built-in/auth-routes.ts +1313 -0
- package/src/provider/built-in/email-verification.ts +303 -0
- package/src/provider/built-in/password-hash.ts +118 -0
- package/src/provider/built-in/password-reset.ts +416 -0
- package/src/provider/built-in/postgres-user-store.ts +328 -0
- package/src/provider/built-in/quickstart-server.ts +760 -0
- package/src/provider/built-in/sqlite-user-store.ts +322 -0
- package/src/provider/built-in/sync-scopes.ts +85 -0
- package/src/provider/built-in/user-store.ts +465 -0
- package/src/provider/external/clerk-adapter.ts +157 -0
- package/src/provider/external/external-jwt-provider.ts +491 -0
- package/src/provider/external/supabase-adapter.ts +163 -0
- package/src/provider/oauth/linked-identity-store.ts +108 -0
- package/src/provider/oauth/oauth-flow.ts +550 -0
- package/src/provider/oauth/oauth-types.ts +184 -0
- package/src/provider/oauth/postgres-oauth-store.ts +296 -0
- package/src/provider/oauth/sqlite-oauth-store.ts +272 -0
- package/src/rbac/rbac-engine.ts +323 -0
- package/src/rbac/rbac-types.ts +210 -0
- package/src/rbac/scope-resolver.ts +140 -0
- package/src/react/AuthProvider.tsx +97 -0
- package/src/react/OrgProvider.tsx +41 -0
- package/src/react/auth-context.ts +26 -0
- package/src/react/hooks.ts +110 -0
- package/src/react/org-hooks.ts +214 -0
- package/src/react.ts +26 -0
- package/src/server.ts +334 -0
- package/src/session/session.ts +401 -0
- package/src/svelte/auth-context.ts +50 -0
- package/src/svelte/org-context.ts +32 -0
- package/src/svelte/org-hooks.ts +201 -0
- package/src/svelte/use-auth.ts +115 -0
- package/src/svelte.ts +25 -0
- package/src/tokens/encrypted-token-store.ts +360 -0
- package/src/tokens/jwt.ts +236 -0
- package/src/tokens/postgres-token-revocation-store.ts +140 -0
- package/src/tokens/sqlite-token-revocation-store.ts +121 -0
- package/src/tokens/token-manager.ts +821 -0
- package/src/tokens/token-store.ts +192 -0
- package/src/types.ts +394 -0
- package/src/vue/auth-context.ts +10 -0
- package/src/vue/auth-provider-types.ts +5 -0
- package/src/vue/auth-provider.ts +76 -0
- package/src/vue/org-hooks.ts +193 -0
- package/src/vue/org-provider.ts +49 -0
- package/src/vue/use-auth.ts +139 -0
- 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
|
+
}
|