@objectstack/plugin-auth 2.0.2 → 2.0.5

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/dist/index.mjs CHANGED
@@ -1,3 +1,210 @@
1
+ // src/auth-manager.ts
2
+ import { betterAuth } from "better-auth";
3
+
4
+ // src/objectql-adapter.ts
5
+ function createObjectQLAdapter(dataEngine) {
6
+ function convertWhere(where) {
7
+ const filter = {};
8
+ for (const condition of where) {
9
+ const fieldName = condition.field;
10
+ if (condition.operator === "eq") {
11
+ filter[fieldName] = condition.value;
12
+ } else if (condition.operator === "ne") {
13
+ filter[fieldName] = { $ne: condition.value };
14
+ } else if (condition.operator === "in") {
15
+ filter[fieldName] = { $in: condition.value };
16
+ } else if (condition.operator === "gt") {
17
+ filter[fieldName] = { $gt: condition.value };
18
+ } else if (condition.operator === "gte") {
19
+ filter[fieldName] = { $gte: condition.value };
20
+ } else if (condition.operator === "lt") {
21
+ filter[fieldName] = { $lt: condition.value };
22
+ } else if (condition.operator === "lte") {
23
+ filter[fieldName] = { $lte: condition.value };
24
+ } else if (condition.operator === "contains") {
25
+ filter[fieldName] = { $regex: condition.value };
26
+ }
27
+ }
28
+ return filter;
29
+ }
30
+ return {
31
+ create: async ({ model, data, select: _select }) => {
32
+ const objectName = model;
33
+ const result = await dataEngine.insert(objectName, data);
34
+ return result;
35
+ },
36
+ findOne: async ({ model, where, select, join: _join }) => {
37
+ const objectName = model;
38
+ const filter = convertWhere(where);
39
+ const result = await dataEngine.findOne(objectName, {
40
+ filter,
41
+ select
42
+ });
43
+ return result ? result : null;
44
+ },
45
+ findMany: async ({ model, where, limit, offset, sortBy, join: _join }) => {
46
+ const objectName = model;
47
+ const filter = where ? convertWhere(where) : {};
48
+ const sort = sortBy ? [{
49
+ field: sortBy.field,
50
+ order: sortBy.direction
51
+ }] : void 0;
52
+ const results = await dataEngine.find(objectName, {
53
+ filter,
54
+ limit: limit || 100,
55
+ skip: offset,
56
+ sort
57
+ });
58
+ return results;
59
+ },
60
+ count: async ({ model, where }) => {
61
+ const objectName = model;
62
+ const filter = where ? convertWhere(where) : {};
63
+ return await dataEngine.count(objectName, { filter });
64
+ },
65
+ update: async ({ model, where, update }) => {
66
+ const objectName = model;
67
+ const filter = convertWhere(where);
68
+ const record = await dataEngine.findOne(objectName, { filter });
69
+ if (!record) {
70
+ return null;
71
+ }
72
+ const result = await dataEngine.update(objectName, {
73
+ ...update,
74
+ id: record.id
75
+ });
76
+ return result ? result : null;
77
+ },
78
+ updateMany: async ({ model, where, update }) => {
79
+ const objectName = model;
80
+ const filter = convertWhere(where);
81
+ const records = await dataEngine.find(objectName, { filter });
82
+ for (const record of records) {
83
+ await dataEngine.update(objectName, {
84
+ ...update,
85
+ id: record.id
86
+ });
87
+ }
88
+ return records.length;
89
+ },
90
+ delete: async ({ model, where }) => {
91
+ const objectName = model;
92
+ const filter = convertWhere(where);
93
+ const record = await dataEngine.findOne(objectName, { filter });
94
+ if (!record) {
95
+ return;
96
+ }
97
+ await dataEngine.delete(objectName, { filter: { id: record.id } });
98
+ },
99
+ deleteMany: async ({ model, where }) => {
100
+ const objectName = model;
101
+ const filter = convertWhere(where);
102
+ const records = await dataEngine.find(objectName, { filter });
103
+ for (const record of records) {
104
+ await dataEngine.delete(objectName, { filter: { id: record.id } });
105
+ }
106
+ return records.length;
107
+ }
108
+ };
109
+ }
110
+
111
+ // src/auth-manager.ts
112
+ var AuthManager = class {
113
+ constructor(config) {
114
+ this.auth = null;
115
+ this.config = config;
116
+ if (config.authInstance) {
117
+ this.auth = config.authInstance;
118
+ }
119
+ }
120
+ /**
121
+ * Get or create the better-auth instance (lazy initialization)
122
+ */
123
+ getOrCreateAuth() {
124
+ if (!this.auth) {
125
+ this.auth = this.createAuthInstance();
126
+ }
127
+ return this.auth;
128
+ }
129
+ /**
130
+ * Create a better-auth instance from configuration
131
+ */
132
+ createAuthInstance() {
133
+ const betterAuthConfig = {
134
+ // Base configuration
135
+ secret: this.config.secret || this.generateSecret(),
136
+ baseURL: this.config.baseUrl || "http://localhost:3000",
137
+ // Database adapter configuration
138
+ // For now, we configure a basic setup that will be enhanced
139
+ // when database URL is provided and drizzle-orm is available
140
+ database: this.createDatabaseConfig(),
141
+ // Email configuration
142
+ emailAndPassword: {
143
+ enabled: true
144
+ },
145
+ // Session configuration
146
+ session: {
147
+ expiresIn: this.config.session?.expiresIn || 60 * 60 * 24 * 7,
148
+ // 7 days default
149
+ updateAge: this.config.session?.updateAge || 60 * 60 * 24
150
+ // 1 day default
151
+ }
152
+ };
153
+ return betterAuth(betterAuthConfig);
154
+ }
155
+ /**
156
+ * Create database configuration using ObjectQL adapter
157
+ */
158
+ createDatabaseConfig() {
159
+ if (this.config.dataEngine) {
160
+ return createObjectQLAdapter(this.config.dataEngine);
161
+ }
162
+ console.warn(
163
+ "\u26A0\uFE0F WARNING: No dataEngine provided to AuthManager! Using in-memory storage. This is NOT suitable for production. Please provide a dataEngine instance (e.g., ObjectQL) in AuthManagerOptions."
164
+ );
165
+ return void 0;
166
+ }
167
+ /**
168
+ * Generate a secure secret if not provided
169
+ */
170
+ generateSecret() {
171
+ const envSecret = process.env.AUTH_SECRET;
172
+ if (!envSecret) {
173
+ const fallbackSecret = "dev-secret-" + Date.now();
174
+ console.warn(
175
+ "\u26A0\uFE0F WARNING: No AUTH_SECRET environment variable set! Using a temporary development secret. This is NOT secure for production use. Please set AUTH_SECRET in your environment variables."
176
+ );
177
+ return fallbackSecret;
178
+ }
179
+ return envSecret;
180
+ }
181
+ /**
182
+ * Get the underlying better-auth instance
183
+ * Useful for advanced use cases
184
+ */
185
+ getAuthInstance() {
186
+ return this.getOrCreateAuth();
187
+ }
188
+ /**
189
+ * Handle an authentication request
190
+ * Forwards the request directly to better-auth's universal handler
191
+ *
192
+ * @param request - Web standard Request object
193
+ * @returns Web standard Response object
194
+ */
195
+ async handleRequest(request) {
196
+ const auth = this.getOrCreateAuth();
197
+ return await auth.handler(request);
198
+ }
199
+ /**
200
+ * Get the better-auth API for programmatic access
201
+ * Use this for server-side operations (e.g., creating users, checking sessions)
202
+ */
203
+ get api() {
204
+ return this.getOrCreateAuth().api;
205
+ }
206
+ };
207
+
1
208
  // src/auth-plugin.ts
2
209
  var AuthPlugin = class {
3
210
  constructor(options = {}) {
@@ -17,7 +224,14 @@ var AuthPlugin = class {
17
224
  if (!this.options.secret) {
18
225
  throw new Error("AuthPlugin: secret is required");
19
226
  }
20
- this.authManager = new AuthManager(this.options);
227
+ const dataEngine = ctx.getService("data");
228
+ if (!dataEngine) {
229
+ ctx.logger.warn("No data engine service found - auth will use in-memory storage");
230
+ }
231
+ this.authManager = new AuthManager({
232
+ ...this.options,
233
+ dataEngine
234
+ });
21
235
  ctx.registerService("auth", this.authManager);
22
236
  ctx.logger.info("Auth Plugin initialized successfully");
23
237
  }
@@ -37,6 +251,20 @@ var AuthPlugin = class {
37
251
  throw err;
38
252
  }
39
253
  }
254
+ try {
255
+ const ql = ctx.getService("objectql");
256
+ if (ql && typeof ql.registerMiddleware === "function") {
257
+ ql.registerMiddleware(async (opCtx, next) => {
258
+ if (opCtx.context?.userId || opCtx.context?.isSystem) {
259
+ return next();
260
+ }
261
+ await next();
262
+ });
263
+ ctx.logger.info("Auth middleware registered on ObjectQL engine");
264
+ }
265
+ } catch (_e) {
266
+ ctx.logger.debug("ObjectQL engine not available, skipping auth middleware registration");
267
+ }
40
268
  ctx.logger.info("Auth Plugin started successfully");
41
269
  }
42
270
  async destroy() {
@@ -44,95 +272,357 @@ var AuthPlugin = class {
44
272
  }
45
273
  /**
46
274
  * Register authentication routes with HTTP server
275
+ *
276
+ * Uses better-auth's universal handler for all authentication requests.
277
+ * This forwards all requests under basePath to better-auth, which handles:
278
+ * - Email/password authentication
279
+ * - OAuth providers (Google, GitHub, etc.)
280
+ * - Session management
281
+ * - Password reset
282
+ * - Email verification
283
+ * - 2FA, passkeys, magic links (if enabled)
47
284
  */
48
285
  registerAuthRoutes(httpServer, ctx) {
49
286
  if (!this.authManager) return;
50
287
  const basePath = this.options.basePath || "/api/v1/auth";
51
- httpServer.post(`${basePath}/login`, async (req, res) => {
52
- try {
53
- const body = req.body;
54
- const result = await this.authManager.login(body);
55
- res.status(200).json(result);
56
- } catch (error) {
57
- const err = error instanceof Error ? error : new Error(String(error));
58
- ctx.logger.error("Login error:", err);
59
- res.status(401).json({
60
- success: false,
61
- error: err.message
62
- });
63
- }
64
- });
65
- httpServer.post(`${basePath}/register`, async (req, res) => {
66
- try {
67
- const body = req.body;
68
- const result = await this.authManager.register(body);
69
- res.status(201).json(result);
70
- } catch (error) {
71
- const err = error instanceof Error ? error : new Error(String(error));
72
- ctx.logger.error("Registration error:", err);
73
- res.status(400).json({
74
- success: false,
75
- error: err.message
76
- });
77
- }
78
- });
79
- httpServer.post(`${basePath}/logout`, async (req, res) => {
288
+ if (!("getRawApp" in httpServer) || typeof httpServer.getRawApp !== "function") {
289
+ ctx.logger.error("HTTP server does not support getRawApp() - wildcard routing requires Hono server");
290
+ throw new Error(
291
+ "AuthPlugin requires HonoServerPlugin for wildcard routing support. Please ensure HonoServerPlugin is loaded before AuthPlugin."
292
+ );
293
+ }
294
+ const rawApp = httpServer.getRawApp();
295
+ rawApp.all(`${basePath}/*`, async (c) => {
80
296
  try {
81
- const authHeader = req.headers["authorization"];
82
- const token = typeof authHeader === "string" ? authHeader.replace("Bearer ", "") : void 0;
83
- await this.authManager.logout(token);
84
- res.status(200).json({ success: true });
85
- } catch (error) {
86
- const err = error instanceof Error ? error : new Error(String(error));
87
- ctx.logger.error("Logout error:", err);
88
- res.status(400).json({
89
- success: false,
90
- error: err.message
297
+ const request = c.req.raw;
298
+ const url = new URL(request.url);
299
+ const authPath = url.pathname.replace(basePath, "");
300
+ const rewrittenUrl = new URL(authPath || "/", url.origin);
301
+ rewrittenUrl.search = url.search;
302
+ const rewrittenRequest = new Request(rewrittenUrl, {
303
+ method: request.method,
304
+ headers: request.headers,
305
+ body: request.body,
306
+ duplex: "half"
307
+ // Required for Request with body
91
308
  });
92
- }
93
- });
94
- httpServer.get(`${basePath}/session`, async (req, res) => {
95
- try {
96
- const authHeader = req.headers["authorization"];
97
- const token = typeof authHeader === "string" ? authHeader.replace("Bearer ", "") : void 0;
98
- const session = await this.authManager.getSession(token);
99
- res.status(200).json({ success: true, data: session });
309
+ const response = await this.authManager.handleRequest(rewrittenRequest);
310
+ return response;
100
311
  } catch (error) {
101
312
  const err = error instanceof Error ? error : new Error(String(error));
102
- res.status(401).json({
103
- success: false,
104
- error: err.message
105
- });
313
+ ctx.logger.error("Auth request error:", err);
314
+ return new Response(
315
+ JSON.stringify({
316
+ success: false,
317
+ error: err.message
318
+ }),
319
+ {
320
+ status: 500,
321
+ headers: { "Content-Type": "application/json" }
322
+ }
323
+ );
106
324
  }
107
325
  });
108
- ctx.logger.debug("Auth routes registered:", {
109
- basePath,
110
- routes: [
111
- `POST ${basePath}/login`,
112
- `POST ${basePath}/register`,
113
- `POST ${basePath}/logout`,
114
- `GET ${basePath}/session`
115
- ]
116
- });
326
+ ctx.logger.info(`Auth routes registered: All requests under ${basePath}/* forwarded to better-auth`);
117
327
  }
118
328
  };
119
- var AuthManager = class {
120
- constructor(_config) {
121
- }
122
- async login(_credentials) {
123
- throw new Error("Login not yet implemented");
124
- }
125
- async register(_userData) {
126
- throw new Error("Registration not yet implemented");
329
+
330
+ // src/objects/auth-user.object.ts
331
+ import { ObjectSchema, Field } from "@objectstack/spec/data";
332
+ var AuthUser = ObjectSchema.create({
333
+ name: "user",
334
+ label: "User",
335
+ pluralLabel: "Users",
336
+ icon: "user",
337
+ description: "User accounts for authentication",
338
+ titleFormat: "{name} ({email})",
339
+ compactLayout: ["name", "email", "emailVerified"],
340
+ fields: {
341
+ // ID is auto-generated by ObjectQL
342
+ id: Field.text({
343
+ label: "User ID",
344
+ required: true,
345
+ readonly: true
346
+ }),
347
+ createdAt: Field.datetime({
348
+ label: "Created At",
349
+ defaultValue: "NOW()",
350
+ readonly: true
351
+ }),
352
+ updatedAt: Field.datetime({
353
+ label: "Updated At",
354
+ defaultValue: "NOW()",
355
+ readonly: true
356
+ }),
357
+ email: Field.email({
358
+ label: "Email",
359
+ required: true,
360
+ searchable: true
361
+ }),
362
+ emailVerified: Field.boolean({
363
+ label: "Email Verified",
364
+ defaultValue: false
365
+ }),
366
+ name: Field.text({
367
+ label: "Name",
368
+ required: true,
369
+ searchable: true,
370
+ maxLength: 255
371
+ }),
372
+ image: Field.url({
373
+ label: "Profile Image",
374
+ required: false
375
+ })
376
+ },
377
+ // Database indexes for performance
378
+ indexes: [
379
+ { fields: ["email"], unique: true },
380
+ { fields: ["createdAt"], unique: false }
381
+ ],
382
+ // Enable features
383
+ enable: {
384
+ trackHistory: true,
385
+ searchable: true,
386
+ apiEnabled: true,
387
+ apiMethods: ["get", "list", "create", "update", "delete"],
388
+ trash: true,
389
+ mru: true
390
+ },
391
+ // Validation Rules
392
+ validations: [
393
+ {
394
+ name: "email_unique",
395
+ type: "unique",
396
+ severity: "error",
397
+ message: "Email must be unique",
398
+ fields: ["email"],
399
+ caseSensitive: false
400
+ }
401
+ ]
402
+ });
403
+
404
+ // src/objects/auth-session.object.ts
405
+ import { ObjectSchema as ObjectSchema2, Field as Field2 } from "@objectstack/spec/data";
406
+ var AuthSession = ObjectSchema2.create({
407
+ name: "session",
408
+ label: "Session",
409
+ pluralLabel: "Sessions",
410
+ icon: "key",
411
+ description: "Active user sessions",
412
+ titleFormat: "Session {token}",
413
+ compactLayout: ["userId", "expiresAt", "ipAddress"],
414
+ fields: {
415
+ id: Field2.text({
416
+ label: "Session ID",
417
+ required: true,
418
+ readonly: true
419
+ }),
420
+ createdAt: Field2.datetime({
421
+ label: "Created At",
422
+ defaultValue: "NOW()",
423
+ readonly: true
424
+ }),
425
+ updatedAt: Field2.datetime({
426
+ label: "Updated At",
427
+ defaultValue: "NOW()",
428
+ readonly: true
429
+ }),
430
+ userId: Field2.text({
431
+ label: "User ID",
432
+ required: true
433
+ }),
434
+ expiresAt: Field2.datetime({
435
+ label: "Expires At",
436
+ required: true
437
+ }),
438
+ token: Field2.text({
439
+ label: "Session Token",
440
+ required: true
441
+ }),
442
+ ipAddress: Field2.text({
443
+ label: "IP Address",
444
+ required: false,
445
+ maxLength: 45
446
+ // Support IPv6
447
+ }),
448
+ userAgent: Field2.textarea({
449
+ label: "User Agent",
450
+ required: false
451
+ })
452
+ },
453
+ // Database indexes for performance
454
+ indexes: [
455
+ { fields: ["token"], unique: true },
456
+ { fields: ["userId"], unique: false },
457
+ { fields: ["expiresAt"], unique: false }
458
+ ],
459
+ // Enable features
460
+ enable: {
461
+ trackHistory: false,
462
+ // Sessions don't need history tracking
463
+ searchable: false,
464
+ apiEnabled: true,
465
+ apiMethods: ["get", "list", "create", "delete"],
466
+ // No update for sessions
467
+ trash: false,
468
+ // Sessions should be hard deleted
469
+ mru: false
127
470
  }
128
- async logout(_token) {
129
- throw new Error("Logout not yet implemented");
471
+ });
472
+
473
+ // src/objects/auth-account.object.ts
474
+ import { ObjectSchema as ObjectSchema3, Field as Field3 } from "@objectstack/spec/data";
475
+ var AuthAccount = ObjectSchema3.create({
476
+ name: "account",
477
+ label: "Account",
478
+ pluralLabel: "Accounts",
479
+ icon: "link",
480
+ description: "OAuth and authentication provider accounts",
481
+ titleFormat: "{providerId} - {accountId}",
482
+ compactLayout: ["providerId", "userId", "accountId"],
483
+ fields: {
484
+ id: Field3.text({
485
+ label: "Account ID",
486
+ required: true,
487
+ readonly: true
488
+ }),
489
+ createdAt: Field3.datetime({
490
+ label: "Created At",
491
+ defaultValue: "NOW()",
492
+ readonly: true
493
+ }),
494
+ updatedAt: Field3.datetime({
495
+ label: "Updated At",
496
+ defaultValue: "NOW()",
497
+ readonly: true
498
+ }),
499
+ providerId: Field3.text({
500
+ label: "Provider ID",
501
+ required: true,
502
+ description: "OAuth provider identifier (google, github, etc.)"
503
+ }),
504
+ accountId: Field3.text({
505
+ label: "Provider Account ID",
506
+ required: true,
507
+ description: "User's ID in the provider's system"
508
+ }),
509
+ userId: Field3.text({
510
+ label: "User ID",
511
+ required: true,
512
+ description: "Link to user table"
513
+ }),
514
+ accessToken: Field3.textarea({
515
+ label: "Access Token",
516
+ required: false
517
+ }),
518
+ refreshToken: Field3.textarea({
519
+ label: "Refresh Token",
520
+ required: false
521
+ }),
522
+ idToken: Field3.textarea({
523
+ label: "ID Token",
524
+ required: false
525
+ }),
526
+ accessTokenExpiresAt: Field3.datetime({
527
+ label: "Access Token Expires At",
528
+ required: false
529
+ }),
530
+ refreshTokenExpiresAt: Field3.datetime({
531
+ label: "Refresh Token Expires At",
532
+ required: false
533
+ }),
534
+ scope: Field3.text({
535
+ label: "OAuth Scope",
536
+ required: false
537
+ }),
538
+ password: Field3.text({
539
+ label: "Password Hash",
540
+ required: false,
541
+ description: "Hashed password for email/password provider"
542
+ })
543
+ },
544
+ // Database indexes for performance
545
+ indexes: [
546
+ { fields: ["userId"], unique: false },
547
+ { fields: ["providerId", "accountId"], unique: true }
548
+ ],
549
+ // Enable features
550
+ enable: {
551
+ trackHistory: false,
552
+ searchable: false,
553
+ apiEnabled: true,
554
+ apiMethods: ["get", "list", "create", "update", "delete"],
555
+ trash: true,
556
+ mru: false
130
557
  }
131
- async getSession(_token) {
132
- throw new Error("Session retrieval not yet implemented");
558
+ });
559
+
560
+ // src/objects/auth-verification.object.ts
561
+ import { ObjectSchema as ObjectSchema4, Field as Field4 } from "@objectstack/spec/data";
562
+ var AuthVerification = ObjectSchema4.create({
563
+ name: "verification",
564
+ label: "Verification",
565
+ pluralLabel: "Verifications",
566
+ icon: "shield-check",
567
+ description: "Email and phone verification tokens",
568
+ titleFormat: "Verification for {identifier}",
569
+ compactLayout: ["identifier", "expiresAt", "createdAt"],
570
+ fields: {
571
+ id: Field4.text({
572
+ label: "Verification ID",
573
+ required: true,
574
+ readonly: true
575
+ }),
576
+ createdAt: Field4.datetime({
577
+ label: "Created At",
578
+ defaultValue: "NOW()",
579
+ readonly: true
580
+ }),
581
+ updatedAt: Field4.datetime({
582
+ label: "Updated At",
583
+ defaultValue: "NOW()",
584
+ readonly: true
585
+ }),
586
+ value: Field4.text({
587
+ label: "Verification Token",
588
+ required: true,
589
+ description: "Token or code for verification"
590
+ }),
591
+ expiresAt: Field4.datetime({
592
+ label: "Expires At",
593
+ required: true
594
+ }),
595
+ identifier: Field4.text({
596
+ label: "Identifier",
597
+ required: true,
598
+ description: "Email address or phone number"
599
+ })
600
+ },
601
+ // Database indexes for performance
602
+ indexes: [
603
+ { fields: ["value"], unique: true },
604
+ { fields: ["identifier"], unique: false },
605
+ { fields: ["expiresAt"], unique: false }
606
+ ],
607
+ // Enable features
608
+ enable: {
609
+ trackHistory: false,
610
+ searchable: false,
611
+ apiEnabled: true,
612
+ apiMethods: ["get", "create", "delete"],
613
+ // No list or update
614
+ trash: false,
615
+ // Hard delete expired tokens
616
+ mru: false
133
617
  }
134
- };
618
+ });
135
619
  export {
136
- AuthPlugin
620
+ AuthAccount,
621
+ AuthManager,
622
+ AuthPlugin,
623
+ AuthSession,
624
+ AuthUser,
625
+ AuthVerification,
626
+ createObjectQLAdapter
137
627
  };
138
628
  //# sourceMappingURL=index.mjs.map