@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.
- 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 +2880 -1667
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +810 -169
- package/dist/server.d.ts +810 -169
- package/dist/server.js +2848 -1646
- 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 +365 -0
- package/src/provider/built-in/quickstart-server.ts +760 -0
- package/src/provider/built-in/sqlite-user-store.ts +335 -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 +285 -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 +338 -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,401 @@
|
|
|
1
|
+
import { KoraError } from '@korajs/core'
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Session Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A user session with metadata.
|
|
9
|
+
*/
|
|
10
|
+
export interface Session {
|
|
11
|
+
/** Unique session ID */
|
|
12
|
+
id: string
|
|
13
|
+
/** User ID */
|
|
14
|
+
userId: string
|
|
15
|
+
/** Device ID (if available) */
|
|
16
|
+
deviceId: string | null
|
|
17
|
+
/** IP address of the client (for display, not for security decisions) */
|
|
18
|
+
ipAddress: string | null
|
|
19
|
+
/** User agent string */
|
|
20
|
+
userAgent: string | null
|
|
21
|
+
/** When the session was created */
|
|
22
|
+
createdAt: number
|
|
23
|
+
/** When the session was last active */
|
|
24
|
+
lastActiveAt: number
|
|
25
|
+
/** When the session expires (absolute expiry) */
|
|
26
|
+
expiresAt: number
|
|
27
|
+
/** Whether MFA has been completed for this session */
|
|
28
|
+
mfaVerified: boolean
|
|
29
|
+
/** Custom metadata */
|
|
30
|
+
metadata?: Record<string, unknown>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Configuration for the session manager.
|
|
35
|
+
*/
|
|
36
|
+
export interface SessionManagerConfig {
|
|
37
|
+
/** Session store implementation */
|
|
38
|
+
store: SessionStore
|
|
39
|
+
/** Session TTL in milliseconds. Default: 7 days */
|
|
40
|
+
sessionTtlMs?: number
|
|
41
|
+
/** Idle timeout in milliseconds. Default: 30 minutes */
|
|
42
|
+
idleTimeoutMs?: number
|
|
43
|
+
/** Maximum concurrent sessions per user. Default: 10 */
|
|
44
|
+
maxSessionsPerUser?: number
|
|
45
|
+
/** Whether to extend session on activity. Default: true */
|
|
46
|
+
slidingWindow?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parameters for creating a new session.
|
|
51
|
+
*/
|
|
52
|
+
export interface CreateSessionParams {
|
|
53
|
+
userId: string
|
|
54
|
+
deviceId?: string
|
|
55
|
+
ipAddress?: string
|
|
56
|
+
userAgent?: string
|
|
57
|
+
mfaVerified?: boolean
|
|
58
|
+
metadata?: Record<string, unknown>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Store for session data.
|
|
63
|
+
*/
|
|
64
|
+
export interface SessionStore {
|
|
65
|
+
/** Create a new session */
|
|
66
|
+
create(session: Session): Promise<void>
|
|
67
|
+
/** Get a session by ID */
|
|
68
|
+
getById(sessionId: string): Promise<Session | null>
|
|
69
|
+
/** Update a session */
|
|
70
|
+
update(session: Session): Promise<void>
|
|
71
|
+
/** Delete a session */
|
|
72
|
+
delete(sessionId: string): Promise<void>
|
|
73
|
+
/** Get all sessions for a user */
|
|
74
|
+
listByUserId(userId: string): Promise<Session[]>
|
|
75
|
+
/** Delete all sessions for a user */
|
|
76
|
+
deleteAllForUser(userId: string): Promise<number>
|
|
77
|
+
/** Delete all sessions for a user except one */
|
|
78
|
+
deleteAllExcept(userId: string, keepSessionId: string): Promise<number>
|
|
79
|
+
/** Clean up expired sessions */
|
|
80
|
+
cleanExpired(): Promise<number>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Errors
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
export class SessionError extends KoraError {
|
|
88
|
+
constructor(message: string, code: string, context?: Record<string, unknown>) {
|
|
89
|
+
super(message, code, context)
|
|
90
|
+
this.name = 'SessionError'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class SessionNotFoundError extends SessionError {
|
|
95
|
+
constructor(sessionId: string) {
|
|
96
|
+
super('Session not found or expired.', 'SESSION_NOT_FOUND', { sessionId })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class SessionExpiredError extends SessionError {
|
|
101
|
+
constructor(sessionId: string) {
|
|
102
|
+
super('Session has expired.', 'SESSION_EXPIRED', { sessionId })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class SessionLimitExceededError extends SessionError {
|
|
107
|
+
constructor(userId: string, limit: number) {
|
|
108
|
+
super(
|
|
109
|
+
`Maximum concurrent sessions (${limit}) reached. Revoke an existing session first.`,
|
|
110
|
+
'SESSION_LIMIT_EXCEEDED',
|
|
111
|
+
{ userId, limit },
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class SessionMfaRequiredError extends SessionError {
|
|
117
|
+
constructor(sessionId: string) {
|
|
118
|
+
super('MFA verification is required for this session.', 'SESSION_MFA_REQUIRED', { sessionId })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// InMemorySessionStore
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* In-memory session store for development and testing.
|
|
128
|
+
*/
|
|
129
|
+
export class InMemorySessionStore implements SessionStore {
|
|
130
|
+
private readonly sessions = new Map<string, Session>()
|
|
131
|
+
|
|
132
|
+
async create(session: Session): Promise<void> {
|
|
133
|
+
this.sessions.set(session.id, { ...session })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getById(sessionId: string): Promise<Session | null> {
|
|
137
|
+
const session = this.sessions.get(sessionId)
|
|
138
|
+
return session ? { ...session } : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async update(session: Session): Promise<void> {
|
|
142
|
+
this.sessions.set(session.id, { ...session })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async delete(sessionId: string): Promise<void> {
|
|
146
|
+
this.sessions.delete(sessionId)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async listByUserId(userId: string): Promise<Session[]> {
|
|
150
|
+
const results: Session[] = []
|
|
151
|
+
for (const session of this.sessions.values()) {
|
|
152
|
+
if (session.userId === userId) {
|
|
153
|
+
results.push({ ...session })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return results.sort((a, b) => b.lastActiveAt - a.lastActiveAt)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async deleteAllForUser(userId: string): Promise<number> {
|
|
160
|
+
let count = 0
|
|
161
|
+
for (const [id, session] of this.sessions) {
|
|
162
|
+
if (session.userId === userId) {
|
|
163
|
+
this.sessions.delete(id)
|
|
164
|
+
count++
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return count
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async deleteAllExcept(userId: string, keepSessionId: string): Promise<number> {
|
|
171
|
+
let count = 0
|
|
172
|
+
for (const [id, session] of this.sessions) {
|
|
173
|
+
if (session.userId === userId && id !== keepSessionId) {
|
|
174
|
+
this.sessions.delete(id)
|
|
175
|
+
count++
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return count
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async cleanExpired(): Promise<number> {
|
|
182
|
+
const now = Date.now()
|
|
183
|
+
let count = 0
|
|
184
|
+
for (const [id, session] of this.sessions) {
|
|
185
|
+
if (now > session.expiresAt) {
|
|
186
|
+
this.sessions.delete(id)
|
|
187
|
+
count++
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return count
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// SessionManager
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
const DEFAULT_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
|
|
199
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
|
|
200
|
+
const DEFAULT_MAX_SESSIONS = 10
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Manages user sessions with support for:
|
|
204
|
+
* - Session creation, validation, and revocation
|
|
205
|
+
* - Sliding window expiry (extends on activity)
|
|
206
|
+
* - Idle timeout detection
|
|
207
|
+
* - Max concurrent sessions per user
|
|
208
|
+
* - MFA verification tracking
|
|
209
|
+
* - "Sign out everywhere" support
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```typescript
|
|
213
|
+
* const sessions = new SessionManager({
|
|
214
|
+
* store: new InMemorySessionStore(),
|
|
215
|
+
* sessionTtlMs: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
216
|
+
* idleTimeoutMs: 30 * 60 * 1000, // 30 minutes
|
|
217
|
+
* maxSessionsPerUser: 5,
|
|
218
|
+
* })
|
|
219
|
+
*
|
|
220
|
+
* // Create session on login
|
|
221
|
+
* const session = await sessions.create({
|
|
222
|
+
* userId: 'user-123',
|
|
223
|
+
* ipAddress: '192.168.1.1',
|
|
224
|
+
* userAgent: 'Mozilla/5.0...',
|
|
225
|
+
* })
|
|
226
|
+
*
|
|
227
|
+
* // Validate session on each request
|
|
228
|
+
* const valid = await sessions.validate(session.id)
|
|
229
|
+
*
|
|
230
|
+
* // Touch session to extend idle timeout
|
|
231
|
+
* await sessions.touch(session.id)
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export class SessionManager {
|
|
235
|
+
private readonly store: SessionStore
|
|
236
|
+
private readonly sessionTtlMs: number
|
|
237
|
+
private readonly idleTimeoutMs: number
|
|
238
|
+
private readonly maxSessionsPerUser: number
|
|
239
|
+
private readonly slidingWindow: boolean
|
|
240
|
+
|
|
241
|
+
constructor(config: SessionManagerConfig) {
|
|
242
|
+
this.store = config.store
|
|
243
|
+
this.sessionTtlMs = config.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS
|
|
244
|
+
this.idleTimeoutMs = config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS
|
|
245
|
+
this.maxSessionsPerUser = config.maxSessionsPerUser ?? DEFAULT_MAX_SESSIONS
|
|
246
|
+
this.slidingWindow = config.slidingWindow ?? true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Create a new session for a user.
|
|
251
|
+
* Enforces the maximum concurrent sessions limit.
|
|
252
|
+
*/
|
|
253
|
+
async create(params: CreateSessionParams): Promise<Session> {
|
|
254
|
+
// Enforce max sessions
|
|
255
|
+
const existing = await this.store.listByUserId(params.userId)
|
|
256
|
+
const activeSessions = existing.filter((s) => Date.now() <= s.expiresAt)
|
|
257
|
+
|
|
258
|
+
if (activeSessions.length >= this.maxSessionsPerUser) {
|
|
259
|
+
throw new SessionLimitExceededError(params.userId, this.maxSessionsPerUser)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const now = Date.now()
|
|
263
|
+
const session: Session = {
|
|
264
|
+
id: generateSessionId(),
|
|
265
|
+
userId: params.userId,
|
|
266
|
+
deviceId: params.deviceId ?? null,
|
|
267
|
+
ipAddress: params.ipAddress ?? null,
|
|
268
|
+
userAgent: params.userAgent ?? null,
|
|
269
|
+
createdAt: now,
|
|
270
|
+
lastActiveAt: now,
|
|
271
|
+
expiresAt: now + this.sessionTtlMs,
|
|
272
|
+
mfaVerified: params.mfaVerified ?? false,
|
|
273
|
+
metadata: params.metadata,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await this.store.create(session)
|
|
277
|
+
return session
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate a session.
|
|
282
|
+
* Returns the session if valid, throws if not found or expired.
|
|
283
|
+
*/
|
|
284
|
+
async validate(sessionId: string): Promise<Session> {
|
|
285
|
+
const session = await this.store.getById(sessionId)
|
|
286
|
+
if (!session) {
|
|
287
|
+
throw new SessionNotFoundError(sessionId)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const now = Date.now()
|
|
291
|
+
|
|
292
|
+
// Check absolute expiry
|
|
293
|
+
if (now > session.expiresAt) {
|
|
294
|
+
await this.store.delete(sessionId)
|
|
295
|
+
throw new SessionExpiredError(sessionId)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Check idle timeout
|
|
299
|
+
if (now - session.lastActiveAt > this.idleTimeoutMs) {
|
|
300
|
+
await this.store.delete(sessionId)
|
|
301
|
+
throw new SessionExpiredError(sessionId)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return session
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Touch a session to update its last activity time.
|
|
309
|
+
* If sliding window is enabled, also extends the absolute expiry.
|
|
310
|
+
*/
|
|
311
|
+
async touch(sessionId: string): Promise<Session> {
|
|
312
|
+
const session = await this.validate(sessionId)
|
|
313
|
+
const now = Date.now()
|
|
314
|
+
|
|
315
|
+
session.lastActiveAt = now
|
|
316
|
+
|
|
317
|
+
if (this.slidingWindow) {
|
|
318
|
+
session.expiresAt = now + this.sessionTtlMs
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await this.store.update(session)
|
|
322
|
+
return session
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Mark a session as MFA-verified.
|
|
327
|
+
*/
|
|
328
|
+
async markMfaVerified(sessionId: string): Promise<Session> {
|
|
329
|
+
const session = await this.validate(sessionId)
|
|
330
|
+
session.mfaVerified = true
|
|
331
|
+
await this.store.update(session)
|
|
332
|
+
return session
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Require MFA verification on a session.
|
|
337
|
+
* Throws SessionMfaRequiredError if MFA is not verified.
|
|
338
|
+
*/
|
|
339
|
+
async requireMfa(sessionId: string): Promise<Session> {
|
|
340
|
+
const session = await this.validate(sessionId)
|
|
341
|
+
if (!session.mfaVerified) {
|
|
342
|
+
throw new SessionMfaRequiredError(sessionId)
|
|
343
|
+
}
|
|
344
|
+
return session
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Revoke (delete) a session.
|
|
349
|
+
*/
|
|
350
|
+
async revoke(sessionId: string): Promise<void> {
|
|
351
|
+
await this.store.delete(sessionId)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Revoke all sessions for a user (sign out everywhere).
|
|
356
|
+
* Returns the number of sessions revoked.
|
|
357
|
+
*/
|
|
358
|
+
async revokeAll(userId: string): Promise<number> {
|
|
359
|
+
return this.store.deleteAllForUser(userId)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Revoke all sessions for a user except the current one.
|
|
364
|
+
* Returns the number of sessions revoked.
|
|
365
|
+
*/
|
|
366
|
+
async revokeOthers(userId: string, currentSessionId: string): Promise<number> {
|
|
367
|
+
return this.store.deleteAllExcept(userId, currentSessionId)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* List all active sessions for a user.
|
|
372
|
+
*/
|
|
373
|
+
async listSessions(userId: string): Promise<Session[]> {
|
|
374
|
+
const sessions = await this.store.listByUserId(userId)
|
|
375
|
+
const now = Date.now()
|
|
376
|
+
// Filter out expired sessions
|
|
377
|
+
return sessions.filter((s) => now <= s.expiresAt && now - s.lastActiveAt <= this.idleTimeoutMs)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Clean up expired sessions.
|
|
382
|
+
* Returns the number of sessions cleaned.
|
|
383
|
+
*/
|
|
384
|
+
async cleanExpired(): Promise<number> {
|
|
385
|
+
return this.store.cleanExpired()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Helpers
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
function generateSessionId(): string {
|
|
394
|
+
const bytes = new Uint8Array(32)
|
|
395
|
+
globalThis.crypto.getRandomValues(bytes)
|
|
396
|
+
let hex = ''
|
|
397
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
398
|
+
hex += bytes[i]?.toString(16).padStart(2, '0')
|
|
399
|
+
}
|
|
400
|
+
return hex
|
|
401
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte'
|
|
2
|
+
import { type AuthSession, createAuthSession } from '../bindings/create-auth-session'
|
|
3
|
+
import type { AuthClient, AuthState } from '../client/auth-client'
|
|
4
|
+
|
|
5
|
+
// Registered key: AuthProvider.svelte is compiled from src/ while useAuth comes from dist/,
|
|
6
|
+
// so two copies of this module exist in an app and must share the key.
|
|
7
|
+
const authContextKey = Symbol.for('@korajs/auth:svelte-auth-context')
|
|
8
|
+
|
|
9
|
+
export interface AuthContextValue {
|
|
10
|
+
client: AuthClient
|
|
11
|
+
session: AuthSession
|
|
12
|
+
get state(): AuthState
|
|
13
|
+
get isLoading(): boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize auth context inside a Svelte component (root layout).
|
|
18
|
+
* Must be called during component initialization before children render.
|
|
19
|
+
*/
|
|
20
|
+
export function initAuthProvider(client: AuthClient): AuthContextValue {
|
|
21
|
+
const session = createAuthSession(client)
|
|
22
|
+
|
|
23
|
+
const value: AuthContextValue = {
|
|
24
|
+
client,
|
|
25
|
+
session,
|
|
26
|
+
get state() {
|
|
27
|
+
return session.getSnapshot().state
|
|
28
|
+
},
|
|
29
|
+
get isLoading() {
|
|
30
|
+
return session.getSnapshot().isLoading
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setContext(authContextKey, value)
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getAuthContext(): AuthContextValue {
|
|
39
|
+
const context = getContext<AuthContextValue | undefined>(authContextKey)
|
|
40
|
+
if (!context) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'Auth context missing. Call initAuthProvider(client) in your root layout component.',
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return context
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function destroyAuthProvider(context: AuthContextValue): void {
|
|
49
|
+
context.session.destroy()
|
|
50
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte'
|
|
2
|
+
import { type OrgSession, createOrgSession } from '../bindings/create-org-session'
|
|
3
|
+
import type { OrgClient } from '../client/org-client'
|
|
4
|
+
|
|
5
|
+
// Registered key: OrgProvider.svelte (src/) and the org hooks (dist/) must share it.
|
|
6
|
+
const orgContextKey = Symbol.for('@korajs/auth:svelte-org-context')
|
|
7
|
+
|
|
8
|
+
export interface OrgContextValue {
|
|
9
|
+
client: OrgClient
|
|
10
|
+
session: OrgSession
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function initOrgProvider(client: OrgClient): OrgContextValue {
|
|
14
|
+
const session = createOrgSession(client)
|
|
15
|
+
const value: OrgContextValue = { client, session }
|
|
16
|
+
setContext(orgContextKey, value)
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getOrgContext(): OrgContextValue {
|
|
21
|
+
const context = getContext<OrgContextValue | undefined>(orgContextKey)
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'Org context missing. Wrap your app with <OrgProvider client={orgClient}> from @korajs/auth/svelte.',
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return context
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function destroyOrgProvider(context: OrgContextValue): void {
|
|
31
|
+
context.session.destroy()
|
|
32
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { type Readable, derived, get, readable, writable } from 'svelte/store'
|
|
2
|
+
import {
|
|
3
|
+
checkOrgPermission,
|
|
4
|
+
createOrgMembersActions,
|
|
5
|
+
loadOrgMembers,
|
|
6
|
+
} from '../bindings/create-org-session'
|
|
7
|
+
import type { ClientInvitation, ClientMembership, ClientOrganization } from '../client/org-client'
|
|
8
|
+
import { getOrgContext } from './org-context'
|
|
9
|
+
|
|
10
|
+
export interface UseOrgResult {
|
|
11
|
+
subscribe: Readable<{
|
|
12
|
+
org: ClientOrganization | null
|
|
13
|
+
role: string | null
|
|
14
|
+
orgId: string | null
|
|
15
|
+
error: string | null
|
|
16
|
+
}>['subscribe']
|
|
17
|
+
get org(): ClientOrganization | null
|
|
18
|
+
get role(): string | null
|
|
19
|
+
get orgId(): string | null
|
|
20
|
+
switchOrg: (orgId: string) => Promise<void>
|
|
21
|
+
createOrg: (params: { name: string; slug?: string }) => Promise<ClientOrganization>
|
|
22
|
+
leaveOrg: () => Promise<void>
|
|
23
|
+
clearOrg: () => void
|
|
24
|
+
listOrgs: () => Promise<ClientOrganization[]>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseOrgMembersResult {
|
|
28
|
+
subscribe: Readable<{
|
|
29
|
+
members: ClientMembership[]
|
|
30
|
+
isLoading: boolean
|
|
31
|
+
error: string | null
|
|
32
|
+
}>['subscribe']
|
|
33
|
+
refresh: () => Promise<void>
|
|
34
|
+
invite: (email: string, role: string) => Promise<ClientInvitation>
|
|
35
|
+
removeMember: (userId: string) => Promise<void>
|
|
36
|
+
updateRole: (userId: string, role: string) => Promise<void>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createOrgSnapshotStore(): Readable<{
|
|
40
|
+
org: ClientOrganization | null
|
|
41
|
+
role: string | null
|
|
42
|
+
orgId: string | null
|
|
43
|
+
}> {
|
|
44
|
+
const { session } = getOrgContext()
|
|
45
|
+
|
|
46
|
+
return readable(session.getSnapshot(), (set) => {
|
|
47
|
+
set(session.getSnapshot())
|
|
48
|
+
return session.subscribe(() => {
|
|
49
|
+
set(session.getSnapshot())
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function useOrg(): UseOrgResult {
|
|
55
|
+
const { client, session } = getOrgContext()
|
|
56
|
+
const snapshotStore = createOrgSnapshotStore()
|
|
57
|
+
const error = writable<string | null>(null)
|
|
58
|
+
|
|
59
|
+
const store = derived([snapshotStore, error], ([snapshot, errorValue]) => ({
|
|
60
|
+
...snapshot,
|
|
61
|
+
error: errorValue,
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
const switchOrg = async (orgId: string): Promise<void> => {
|
|
65
|
+
error.set(null)
|
|
66
|
+
try {
|
|
67
|
+
await client.switchOrg(orgId)
|
|
68
|
+
} catch (err) {
|
|
69
|
+
error.set(err instanceof Error ? err.message : String(err))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const createOrg = async (params: {
|
|
74
|
+
name: string
|
|
75
|
+
slug?: string
|
|
76
|
+
}): Promise<ClientOrganization> => {
|
|
77
|
+
error.set(null)
|
|
78
|
+
try {
|
|
79
|
+
return await client.createOrg(params)
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
82
|
+
error.set(message)
|
|
83
|
+
throw err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const leaveOrg = async (): Promise<void> => {
|
|
88
|
+
const orgId = get(snapshotStore).orgId
|
|
89
|
+
if (!orgId) return
|
|
90
|
+
error.set(null)
|
|
91
|
+
try {
|
|
92
|
+
await client.leaveOrg(orgId)
|
|
93
|
+
} catch (err) {
|
|
94
|
+
error.set(err instanceof Error ? err.message : String(err))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const clearOrg = (): void => {
|
|
99
|
+
client.clearActiveOrg()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const listOrgs = async (): Promise<ClientOrganization[]> => {
|
|
103
|
+
try {
|
|
104
|
+
return await client.listOrgs()
|
|
105
|
+
} catch (err) {
|
|
106
|
+
error.set(err instanceof Error ? err.message : String(err))
|
|
107
|
+
return []
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
subscribe: store.subscribe,
|
|
113
|
+
get org() {
|
|
114
|
+
return get(snapshotStore).org
|
|
115
|
+
},
|
|
116
|
+
get role() {
|
|
117
|
+
return get(snapshotStore).role
|
|
118
|
+
},
|
|
119
|
+
get orgId() {
|
|
120
|
+
return get(snapshotStore).orgId
|
|
121
|
+
},
|
|
122
|
+
switchOrg,
|
|
123
|
+
createOrg,
|
|
124
|
+
leaveOrg,
|
|
125
|
+
clearOrg,
|
|
126
|
+
listOrgs,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function useOrgMembers(orgId: string): UseOrgMembersResult {
|
|
131
|
+
const { client } = getOrgContext()
|
|
132
|
+
const members = writable<ClientMembership[]>([])
|
|
133
|
+
const isLoading = writable(true)
|
|
134
|
+
const error = writable<string | null>(null)
|
|
135
|
+
|
|
136
|
+
const refresh = async (targetOrgId: string): Promise<void> => {
|
|
137
|
+
isLoading.set(true)
|
|
138
|
+
error.set(null)
|
|
139
|
+
try {
|
|
140
|
+
members.set(await loadOrgMembers(client, targetOrgId))
|
|
141
|
+
} catch (err) {
|
|
142
|
+
error.set(err instanceof Error ? err.message : String(err))
|
|
143
|
+
} finally {
|
|
144
|
+
isLoading.set(false)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$effect(() => {
|
|
149
|
+
void refresh(orgId)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const actions = createOrgMembersActions(client, orgId, (message) => {
|
|
153
|
+
error.set(message)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const invite = async (email: string, role: string): Promise<ClientInvitation> => {
|
|
157
|
+
const result = await actions.invite(email, role)
|
|
158
|
+
await refresh(orgId)
|
|
159
|
+
return result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const removeMember = async (userId: string): Promise<void> => {
|
|
163
|
+
await actions.removeMember(userId)
|
|
164
|
+
await refresh(orgId)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const updateRole = async (userId: string, role: string): Promise<void> => {
|
|
168
|
+
await actions.updateRole(userId, role)
|
|
169
|
+
await refresh(orgId)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const store = derived([members, isLoading, error], ([memberList, loading, errorValue]) => ({
|
|
173
|
+
members: memberList,
|
|
174
|
+
isLoading: loading,
|
|
175
|
+
error: errorValue,
|
|
176
|
+
}))
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
subscribe: store.subscribe,
|
|
180
|
+
refresh: () => refresh(orgId),
|
|
181
|
+
invite,
|
|
182
|
+
removeMember,
|
|
183
|
+
updateRole,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function createPermissionStore(requiredRole: string): Readable<boolean> {
|
|
188
|
+
const { session } = getOrgContext()
|
|
189
|
+
|
|
190
|
+
return readable(session.checkPermission(requiredRole), (set) => {
|
|
191
|
+
set(session.checkPermission(requiredRole))
|
|
192
|
+
return session.subscribe(() => {
|
|
193
|
+
set(session.checkPermission(requiredRole))
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Alias for {@link createPermissionStore}. */
|
|
199
|
+
export const usePermission = createPermissionStore
|
|
200
|
+
|
|
201
|
+
export { checkOrgPermission }
|