@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.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
  }
@@ -63,6 +283,20 @@ var AuthPlugin = class {
63
283
  throw err;
64
284
  }
65
285
  }
286
+ try {
287
+ const ql = ctx.getService("objectql");
288
+ if (ql && typeof ql.registerMiddleware === "function") {
289
+ ql.registerMiddleware(async (opCtx, next) => {
290
+ if (opCtx.context?.userId || opCtx.context?.isSystem) {
291
+ return next();
292
+ }
293
+ await next();
294
+ });
295
+ ctx.logger.info("Auth middleware registered on ObjectQL engine");
296
+ }
297
+ } catch (_e) {
298
+ ctx.logger.debug("ObjectQL engine not available, skipping auth middleware registration");
299
+ }
66
300
  ctx.logger.info("Auth Plugin started successfully");
67
301
  }
68
302
  async destroy() {
@@ -70,96 +304,358 @@ var AuthPlugin = class {
70
304
  }
71
305
  /**
72
306
  * Register authentication routes with HTTP server
307
+ *
308
+ * Uses better-auth's universal handler for all authentication requests.
309
+ * This forwards all requests under basePath to better-auth, which handles:
310
+ * - Email/password authentication
311
+ * - OAuth providers (Google, GitHub, etc.)
312
+ * - Session management
313
+ * - Password reset
314
+ * - Email verification
315
+ * - 2FA, passkeys, magic links (if enabled)
73
316
  */
74
317
  registerAuthRoutes(httpServer, ctx) {
75
318
  if (!this.authManager) return;
76
319
  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) => {
320
+ if (!("getRawApp" in httpServer) || typeof httpServer.getRawApp !== "function") {
321
+ ctx.logger.error("HTTP server does not support getRawApp() - wildcard routing requires Hono server");
322
+ throw new Error(
323
+ "AuthPlugin requires HonoServerPlugin for wildcard routing support. Please ensure HonoServerPlugin is loaded before AuthPlugin."
324
+ );
325
+ }
326
+ const rawApp = httpServer.getRawApp();
327
+ rawApp.all(`${basePath}/*`, async (c) => {
106
328
  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
329
+ const request = c.req.raw;
330
+ const url = new URL(request.url);
331
+ const authPath = url.pathname.replace(basePath, "");
332
+ const rewrittenUrl = new URL(authPath || "/", url.origin);
333
+ rewrittenUrl.search = url.search;
334
+ const rewrittenRequest = new Request(rewrittenUrl, {
335
+ method: request.method,
336
+ headers: request.headers,
337
+ body: request.body,
338
+ duplex: "half"
339
+ // Required for Request with body
117
340
  });
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 });
341
+ const response = await this.authManager.handleRequest(rewrittenRequest);
342
+ return response;
126
343
  } catch (error) {
127
344
  const err = error instanceof Error ? error : new Error(String(error));
128
- res.status(401).json({
129
- success: false,
130
- error: err.message
131
- });
345
+ ctx.logger.error("Auth request error:", err);
346
+ return new Response(
347
+ JSON.stringify({
348
+ success: false,
349
+ error: err.message
350
+ }),
351
+ {
352
+ status: 500,
353
+ headers: { "Content-Type": "application/json" }
354
+ }
355
+ );
132
356
  }
133
357
  });
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
- });
358
+ ctx.logger.info(`Auth routes registered: All requests under ${basePath}/* forwarded to better-auth`);
143
359
  }
144
360
  };
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");
361
+
362
+ // src/objects/auth-user.object.ts
363
+ var import_data = require("@objectstack/spec/data");
364
+ var AuthUser = import_data.ObjectSchema.create({
365
+ name: "user",
366
+ label: "User",
367
+ pluralLabel: "Users",
368
+ icon: "user",
369
+ description: "User accounts for authentication",
370
+ titleFormat: "{name} ({email})",
371
+ compactLayout: ["name", "email", "emailVerified"],
372
+ fields: {
373
+ // ID is auto-generated by ObjectQL
374
+ id: import_data.Field.text({
375
+ label: "User ID",
376
+ required: true,
377
+ readonly: true
378
+ }),
379
+ createdAt: import_data.Field.datetime({
380
+ label: "Created At",
381
+ defaultValue: "NOW()",
382
+ readonly: true
383
+ }),
384
+ updatedAt: import_data.Field.datetime({
385
+ label: "Updated At",
386
+ defaultValue: "NOW()",
387
+ readonly: true
388
+ }),
389
+ email: import_data.Field.email({
390
+ label: "Email",
391
+ required: true,
392
+ searchable: true
393
+ }),
394
+ emailVerified: import_data.Field.boolean({
395
+ label: "Email Verified",
396
+ defaultValue: false
397
+ }),
398
+ name: import_data.Field.text({
399
+ label: "Name",
400
+ required: true,
401
+ searchable: true,
402
+ maxLength: 255
403
+ }),
404
+ image: import_data.Field.url({
405
+ label: "Profile Image",
406
+ required: false
407
+ })
408
+ },
409
+ // Database indexes for performance
410
+ indexes: [
411
+ { fields: ["email"], unique: true },
412
+ { fields: ["createdAt"], unique: false }
413
+ ],
414
+ // Enable features
415
+ enable: {
416
+ trackHistory: true,
417
+ searchable: true,
418
+ apiEnabled: true,
419
+ apiMethods: ["get", "list", "create", "update", "delete"],
420
+ trash: true,
421
+ mru: true
422
+ },
423
+ // Validation Rules
424
+ validations: [
425
+ {
426
+ name: "email_unique",
427
+ type: "unique",
428
+ severity: "error",
429
+ message: "Email must be unique",
430
+ fields: ["email"],
431
+ caseSensitive: false
432
+ }
433
+ ]
434
+ });
435
+
436
+ // src/objects/auth-session.object.ts
437
+ var import_data2 = require("@objectstack/spec/data");
438
+ var AuthSession = import_data2.ObjectSchema.create({
439
+ name: "session",
440
+ label: "Session",
441
+ pluralLabel: "Sessions",
442
+ icon: "key",
443
+ description: "Active user sessions",
444
+ titleFormat: "Session {token}",
445
+ compactLayout: ["userId", "expiresAt", "ipAddress"],
446
+ fields: {
447
+ id: import_data2.Field.text({
448
+ label: "Session ID",
449
+ required: true,
450
+ readonly: true
451
+ }),
452
+ createdAt: import_data2.Field.datetime({
453
+ label: "Created At",
454
+ defaultValue: "NOW()",
455
+ readonly: true
456
+ }),
457
+ updatedAt: import_data2.Field.datetime({
458
+ label: "Updated At",
459
+ defaultValue: "NOW()",
460
+ readonly: true
461
+ }),
462
+ userId: import_data2.Field.text({
463
+ label: "User ID",
464
+ required: true
465
+ }),
466
+ expiresAt: import_data2.Field.datetime({
467
+ label: "Expires At",
468
+ required: true
469
+ }),
470
+ token: import_data2.Field.text({
471
+ label: "Session Token",
472
+ required: true
473
+ }),
474
+ ipAddress: import_data2.Field.text({
475
+ label: "IP Address",
476
+ required: false,
477
+ maxLength: 45
478
+ // Support IPv6
479
+ }),
480
+ userAgent: import_data2.Field.textarea({
481
+ label: "User Agent",
482
+ required: false
483
+ })
484
+ },
485
+ // Database indexes for performance
486
+ indexes: [
487
+ { fields: ["token"], unique: true },
488
+ { fields: ["userId"], unique: false },
489
+ { fields: ["expiresAt"], unique: false }
490
+ ],
491
+ // Enable features
492
+ enable: {
493
+ trackHistory: false,
494
+ // Sessions don't need history tracking
495
+ searchable: false,
496
+ apiEnabled: true,
497
+ apiMethods: ["get", "list", "create", "delete"],
498
+ // No update for sessions
499
+ trash: false,
500
+ // Sessions should be hard deleted
501
+ mru: false
153
502
  }
154
- async logout(_token) {
155
- throw new Error("Logout not yet implemented");
503
+ });
504
+
505
+ // src/objects/auth-account.object.ts
506
+ var import_data3 = require("@objectstack/spec/data");
507
+ var AuthAccount = import_data3.ObjectSchema.create({
508
+ name: "account",
509
+ label: "Account",
510
+ pluralLabel: "Accounts",
511
+ icon: "link",
512
+ description: "OAuth and authentication provider accounts",
513
+ titleFormat: "{providerId} - {accountId}",
514
+ compactLayout: ["providerId", "userId", "accountId"],
515
+ fields: {
516
+ id: import_data3.Field.text({
517
+ label: "Account ID",
518
+ required: true,
519
+ readonly: true
520
+ }),
521
+ createdAt: import_data3.Field.datetime({
522
+ label: "Created At",
523
+ defaultValue: "NOW()",
524
+ readonly: true
525
+ }),
526
+ updatedAt: import_data3.Field.datetime({
527
+ label: "Updated At",
528
+ defaultValue: "NOW()",
529
+ readonly: true
530
+ }),
531
+ providerId: import_data3.Field.text({
532
+ label: "Provider ID",
533
+ required: true,
534
+ description: "OAuth provider identifier (google, github, etc.)"
535
+ }),
536
+ accountId: import_data3.Field.text({
537
+ label: "Provider Account ID",
538
+ required: true,
539
+ description: "User's ID in the provider's system"
540
+ }),
541
+ userId: import_data3.Field.text({
542
+ label: "User ID",
543
+ required: true,
544
+ description: "Link to user table"
545
+ }),
546
+ accessToken: import_data3.Field.textarea({
547
+ label: "Access Token",
548
+ required: false
549
+ }),
550
+ refreshToken: import_data3.Field.textarea({
551
+ label: "Refresh Token",
552
+ required: false
553
+ }),
554
+ idToken: import_data3.Field.textarea({
555
+ label: "ID Token",
556
+ required: false
557
+ }),
558
+ accessTokenExpiresAt: import_data3.Field.datetime({
559
+ label: "Access Token Expires At",
560
+ required: false
561
+ }),
562
+ refreshTokenExpiresAt: import_data3.Field.datetime({
563
+ label: "Refresh Token Expires At",
564
+ required: false
565
+ }),
566
+ scope: import_data3.Field.text({
567
+ label: "OAuth Scope",
568
+ required: false
569
+ }),
570
+ password: import_data3.Field.text({
571
+ label: "Password Hash",
572
+ required: false,
573
+ description: "Hashed password for email/password provider"
574
+ })
575
+ },
576
+ // Database indexes for performance
577
+ indexes: [
578
+ { fields: ["userId"], unique: false },
579
+ { fields: ["providerId", "accountId"], unique: true }
580
+ ],
581
+ // Enable features
582
+ enable: {
583
+ trackHistory: false,
584
+ searchable: false,
585
+ apiEnabled: true,
586
+ apiMethods: ["get", "list", "create", "update", "delete"],
587
+ trash: true,
588
+ mru: false
156
589
  }
157
- async getSession(_token) {
158
- throw new Error("Session retrieval not yet implemented");
590
+ });
591
+
592
+ // src/objects/auth-verification.object.ts
593
+ var import_data4 = require("@objectstack/spec/data");
594
+ var AuthVerification = import_data4.ObjectSchema.create({
595
+ name: "verification",
596
+ label: "Verification",
597
+ pluralLabel: "Verifications",
598
+ icon: "shield-check",
599
+ description: "Email and phone verification tokens",
600
+ titleFormat: "Verification for {identifier}",
601
+ compactLayout: ["identifier", "expiresAt", "createdAt"],
602
+ fields: {
603
+ id: import_data4.Field.text({
604
+ label: "Verification ID",
605
+ required: true,
606
+ readonly: true
607
+ }),
608
+ createdAt: import_data4.Field.datetime({
609
+ label: "Created At",
610
+ defaultValue: "NOW()",
611
+ readonly: true
612
+ }),
613
+ updatedAt: import_data4.Field.datetime({
614
+ label: "Updated At",
615
+ defaultValue: "NOW()",
616
+ readonly: true
617
+ }),
618
+ value: import_data4.Field.text({
619
+ label: "Verification Token",
620
+ required: true,
621
+ description: "Token or code for verification"
622
+ }),
623
+ expiresAt: import_data4.Field.datetime({
624
+ label: "Expires At",
625
+ required: true
626
+ }),
627
+ identifier: import_data4.Field.text({
628
+ label: "Identifier",
629
+ required: true,
630
+ description: "Email address or phone number"
631
+ })
632
+ },
633
+ // Database indexes for performance
634
+ indexes: [
635
+ { fields: ["value"], unique: true },
636
+ { fields: ["identifier"], unique: false },
637
+ { fields: ["expiresAt"], unique: false }
638
+ ],
639
+ // Enable features
640
+ enable: {
641
+ trackHistory: false,
642
+ searchable: false,
643
+ apiEnabled: true,
644
+ apiMethods: ["get", "create", "delete"],
645
+ // No list or update
646
+ trash: false,
647
+ // Hard delete expired tokens
648
+ mru: false
159
649
  }
160
- };
650
+ });
161
651
  // Annotate the CommonJS export names for ESM import in node:
162
652
  0 && (module.exports = {
163
- AuthPlugin
653
+ AuthAccount,
654
+ AuthManager,
655
+ AuthPlugin,
656
+ AuthSession,
657
+ AuthUser,
658
+ AuthVerification,
659
+ createObjectQLAdapter
164
660
  });
165
661
  //# sourceMappingURL=index.js.map