@kuratchi/auth 0.0.1

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 ADDED
@@ -0,0 +1,44 @@
1
+ # @kuratchi/auth
2
+
3
+ Config-driven authentication for kuratchi apps.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @kuratchi/auth @kuratchi/orm
9
+ ```
10
+
11
+ ## Main APIs
12
+
13
+ ```ts
14
+ import {
15
+ signUp,
16
+ signIn,
17
+ signOut,
18
+ getCurrentUser,
19
+ getAuth,
20
+ logActivity,
21
+ defineRoles,
22
+ hasPermission,
23
+ configureOAuth,
24
+ startOAuth,
25
+ handleOAuthCallback,
26
+ } from '@kuratchi/auth';
27
+ ```
28
+
29
+ ## Feature Areas
30
+
31
+ - Credentials auth: `signUp`, `signIn`, `signOut`, `requestPasswordReset`, `resetPassword`
32
+ - Session/auth context: `getAuth`, `getCurrentUser`
33
+ - Activity logging: `logActivity`, `getActivity`, `defineActivities`
34
+ - Roles and permissions: `defineRoles`, `hasRole`, `hasPermission`, `assignRole`
35
+ - OAuth providers: `configureOAuth`, `startOAuth`, `handleOAuthCallback`
36
+ - Guards, rate limits, Turnstile: compiler-interceptor friendly config and checks
37
+
38
+ ## Notes
39
+
40
+ - Depends on `@kuratchi/orm`.
41
+ - Designed for Cloudflare Workers execution.
42
+
43
+
44
+
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@kuratchi/auth",
3
+ "version": "0.0.1",
4
+ "description": "Config-driven auth for KuratchiJS — credentials, OAuth, RBAC, rate limiting, Turnstile",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./adapter": "./src/adapter.ts"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "dependencies": {
15
+ "@kuratchi/orm": "^0.0.1"
16
+ }
17
+ }
18
+
19
+
package/src/adapter.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface kuratchiAuthAdapterConfig {
2
+ cookieName?: string;
3
+ secretEnvKey?: string;
4
+ sessionEnabled?: boolean;
5
+ credentials?: Record<string, any>;
6
+ activity?: Record<string, any>;
7
+ roles?: Record<string, string[]>;
8
+ oauth?: Record<string, any>;
9
+ guards?: Record<string, any>;
10
+ rateLimit?: Record<string, any>;
11
+ turnstile?: Record<string, any>;
12
+ organizations?: Record<string, any>;
13
+ }
14
+
15
+ export function kuratchiAuthConfig(config: kuratchiAuthAdapterConfig = {}): kuratchiAuthAdapterConfig {
16
+ return {
17
+ cookieName: 'kuratchi_session',
18
+ secretEnvKey: 'AUTH_SECRET',
19
+ sessionEnabled: true,
20
+ ...config,
21
+ };
22
+ }
23
+
24
+
@@ -0,0 +1,294 @@
1
+ /**
2
+ * @kuratchi/auth — Activity Tracking API
3
+ *
4
+ * Ready-to-use server functions for activity logging.
5
+ * Reads DB from framework globals — zero config needed.
6
+ * Define allowed activity types for type safety.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { defineActivities, logActivity, getActivity } from '@kuratchi/auth';
11
+ *
12
+ * // Define allowed actions (once, at module scope):
13
+ * const Activity = defineActivities({
14
+ * 'user.signup': { label: 'User Signed Up', severity: 'info' },
15
+ * 'user.login': { label: 'User Logged In', severity: 'info' },
16
+ * 'admin.access': { label: 'Admin Accessed', severity: 'warning' },
17
+ * });
18
+ *
19
+ * // Type-safe logging:
20
+ * await logActivity(Activity['user.login'], { detail: 'Login from Chrome' });
21
+ *
22
+ * // Query:
23
+ * const recent = await getActivity({ limit: 50 });
24
+ * ```
25
+ */
26
+
27
+ import { parseSessionCookie } from '../utils/crypto.js';
28
+ import { getOrgStubByName, isOrgAvailable } from './organization.js';
29
+
30
+ // ============================================================================
31
+ // Types
32
+ // ============================================================================
33
+
34
+ export interface ActivityTypeDefinition {
35
+ label: string;
36
+ category?: string;
37
+ severity?: 'info' | 'warning' | 'critical';
38
+ description?: string;
39
+ }
40
+
41
+ export interface LogActivityOptions {
42
+ /** Optional detail string or structured data */
43
+ detail?: string | Record<string, any>;
44
+ /** Override userId (defaults to current session user) */
45
+ userId?: number | string | null;
46
+ /** Override IP address (defaults to request header) */
47
+ ip?: string | null;
48
+ /** Override user agent (defaults to request header) */
49
+ userAgent?: string | null;
50
+ }
51
+
52
+ export interface GetActivityOptions {
53
+ /** Max number of records to return (default: 50) */
54
+ limit?: number;
55
+ /** Filter by userId */
56
+ userId?: number | string;
57
+ /** Filter by action */
58
+ action?: string;
59
+ }
60
+
61
+ export interface ActivityConfig {
62
+ /** Table name (default: 'activityLog') */
63
+ table?: string;
64
+ }
65
+
66
+ // ============================================================================
67
+ // Module state
68
+ // ============================================================================
69
+
70
+ let _table: string = 'activityLog';
71
+ let _definitions: Record<string, ActivityTypeDefinition> | null = null;
72
+
73
+ /**
74
+ * Define allowed activity types. Returns a typed constant object
75
+ * mapping action names to themselves (for type-safe logActivity calls).
76
+ *
77
+ * Also registers the definitions so getActivity() can enrich results with labels.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const Activity = defineActivities({
82
+ * 'user.login': { label: 'User Logged In', severity: 'info' },
83
+ * 'todo.create': { label: 'Todo Created', severity: 'info' },
84
+ * });
85
+ *
86
+ * await logActivity(Activity['user.login']); // type-safe
87
+ * await logActivity('unknown.action'); // still works, but no autocomplete
88
+ * ```
89
+ */
90
+ export function defineActivities<T extends Record<string, ActivityTypeDefinition>>(
91
+ definitions: T,
92
+ config?: ActivityConfig,
93
+ ): { [K in keyof T]: K } {
94
+ _definitions = definitions;
95
+ if (config?.table) _table = config.table;
96
+
97
+ // Build a constant object: { 'user.login': 'user.login', ... }
98
+ const actions = {} as { [K in keyof T]: K };
99
+ for (const key of Object.keys(definitions) as (keyof T & string)[]) {
100
+ (actions as any)[key] = key;
101
+ }
102
+ return actions;
103
+ }
104
+
105
+ /**
106
+ * Get the registered activity type definitions.
107
+ */
108
+ export function getActivityDefinitions(): Record<string, ActivityTypeDefinition> {
109
+ return _definitions ?? {};
110
+ }
111
+
112
+ // ============================================================================
113
+ // Framework context resolution (same pattern as getAuth / credentials)
114
+ // ============================================================================
115
+
116
+ function _getContext() {
117
+ const dezContext = (globalThis as any).__kuratchi_context__;
118
+ const env = (globalThis as any).__cloudflare_env__ ?? {};
119
+ const request: Request = dezContext?.request ?? new Request('http://localhost');
120
+ const locals: Record<string, any> = dezContext?.locals ?? {};
121
+ return { env, request, locals };
122
+ }
123
+
124
+ function _getDb(): any {
125
+ const { env } = _getContext();
126
+ const binding = env.DB;
127
+ if (!binding) return null;
128
+ return binding;
129
+ }
130
+
131
+ async function _getOrgStub(): Promise<any | null> {
132
+ if (!isOrgAvailable()) return null;
133
+ const { env, locals, request } = _getContext();
134
+ const cookieName = locals.auth?.cookieName || 'kuratchi_session';
135
+ const sessionCookie =
136
+ locals.auth?.sessionCookie
137
+ ?? request.headers?.get('cookie')?.split(';').map(s => s.trim()).find(s => s.startsWith(`${cookieName}=`))?.slice(cookieName.length + 1)
138
+ ?? null;
139
+ if (!sessionCookie) return null;
140
+ const secret = env.AUTH_SECRET;
141
+ if (!secret) return null;
142
+ const parsed = await parseSessionCookie(secret, sessionCookie);
143
+ if (parsed && parsed.orgId && parsed.orgId !== 'default') {
144
+ return getOrgStubByName(parsed.orgId);
145
+ }
146
+
147
+ // Fallback when cookie parsing is unavailable but user/session context exists.
148
+ const fallbackOrgId = locals.session?.orgId ?? locals.user?.orgId ?? null;
149
+ if (fallbackOrgId && fallbackOrgId !== 'default') {
150
+ return getOrgStubByName(String(fallbackOrgId));
151
+ }
152
+ return null;
153
+ }
154
+
155
+ // ============================================================================
156
+ // Public API
157
+ // ============================================================================
158
+
159
+ /**
160
+ * Log an activity event to the database.
161
+ *
162
+ * Reads the current request context (IP, user agent, session user)
163
+ * automatically from the framework globals.
164
+ *
165
+ * @param action - Activity action identifier (e.g., 'user.login')
166
+ * @param detailOrOptions - String detail, or LogActivityOptions
167
+ */
168
+ export async function logActivity(
169
+ action: string,
170
+ detailOrOptions?: string | LogActivityOptions,
171
+ ): Promise<{ success: boolean; error?: string }> {
172
+ try {
173
+ const { request } = _getContext();
174
+
175
+ // Resolve options
176
+ let opts: LogActivityOptions;
177
+ if (typeof detailOrOptions === 'string') {
178
+ opts = { detail: detailOrOptions };
179
+ } else {
180
+ opts = detailOrOptions || {};
181
+ }
182
+
183
+ // Auto-resolve from request context
184
+ const ip = opts.ip ?? request.headers?.get('cf-connecting-ip')
185
+ ?? request.headers?.get('x-forwarded-for')
186
+ ?? null;
187
+ const userAgent = opts.userAgent ?? request.headers?.get('user-agent') ?? null;
188
+
189
+ // Resolve userId from session if not provided
190
+ let userId = opts.userId;
191
+ if (userId === undefined) {
192
+ const { locals } = _getContext();
193
+ userId = locals.session?.userId ?? locals.user?.id ?? null;
194
+ }
195
+
196
+ // Serialize detail
197
+ const detail = typeof opts.detail === 'object'
198
+ ? JSON.stringify(opts.detail)
199
+ : (opts.detail ?? null);
200
+
201
+ const orgStub = await _getOrgStub();
202
+ if (orgStub?.__kuratchiLogActivity) {
203
+ await orgStub.__kuratchiLogActivity({
204
+ userId: userId ?? null,
205
+ action,
206
+ detail,
207
+ ip,
208
+ userAgent,
209
+ });
210
+ return { success: true };
211
+ }
212
+
213
+ const db = _getDb();
214
+ if (!db) {
215
+ console.warn('[kuratchi/auth activity] No org stub and no DB binding for logActivity', { action });
216
+ return { success: false, error: '[kuratchi/auth] No DB binding found. Ensure D1 is configured.' };
217
+ }
218
+ const now = new Date().toISOString();
219
+
220
+ await db.prepare(
221
+ `INSERT INTO ${_table} (userId, action, detail, ip, userAgent, createdAt) VALUES (?, ?, ?, ?, ?, ?)`
222
+ ).bind(userId ?? null, action, detail, ip, userAgent, now).run();
223
+
224
+ return { success: true };
225
+ } catch (err: any) {
226
+ console.warn('[kuratchi/auth activity] logActivity failed:', err);
227
+ return { success: false, error: err.message };
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Query activity log entries from the database.
233
+ * Enriches results with labels/severity from defineActivities() if set.
234
+ *
235
+ * @param options - Filter and pagination options
236
+ */
237
+ export async function getActivity(
238
+ options?: GetActivityOptions,
239
+ ): Promise<{ success: boolean; data: any[]; error?: string }> {
240
+ try {
241
+ let rows: any[] = [];
242
+ const orgStub = await _getOrgStub();
243
+ if (orgStub?.__kuratchiGetActivity) {
244
+ const result = await orgStub.__kuratchiGetActivity({ limit: options?.limit, action: options?.action });
245
+ rows = Array.isArray(result) ? result : [];
246
+ if (options?.userId !== undefined) {
247
+ rows = rows.filter((row: any) => row?.userId === options.userId);
248
+ }
249
+ } else {
250
+ const db = _getDb();
251
+ if (!db) return { success: false, data: [], error: '[kuratchi/auth] No DB binding found.' };
252
+
253
+ let sql = `SELECT * FROM ${_table}`;
254
+ const params: any[] = [];
255
+ const clauses: string[] = [];
256
+
257
+ if (options?.userId !== undefined) {
258
+ clauses.push('userId = ?');
259
+ params.push(options.userId);
260
+ }
261
+ if (options?.action) {
262
+ clauses.push('action = ?');
263
+ params.push(options.action);
264
+ }
265
+ if (clauses.length > 0) sql += ' WHERE ' + clauses.join(' AND ');
266
+ sql += ' ORDER BY createdAt DESC';
267
+ if (options?.limit) {
268
+ sql += ' LIMIT ?';
269
+ params.push(options.limit);
270
+ }
271
+
272
+ const result = await db.prepare(sql).bind(...params).all();
273
+ rows = result?.results ?? [];
274
+ }
275
+
276
+ // Enrich with definitions if available
277
+ if (_definitions) {
278
+ rows = rows.map((row: any) => {
279
+ const def = _definitions![row.action];
280
+ return def
281
+ ? { ...row, label: def.label, severity: def.severity ?? 'info', category: def.category }
282
+ : { ...row, label: row.action, severity: 'info' };
283
+ });
284
+ }
285
+
286
+ return { success: true, data: rows };
287
+ } catch (err: any) {
288
+ console.warn('[kuratchi/auth activity] getActivity failed:', err);
289
+ return { success: false, data: [], error: err.message };
290
+ }
291
+ }
292
+
293
+
294
+