@objectstack/plugin-auth 2.0.2 → 2.0.3

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