@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.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
  }
@@ -44,95 +258,357 @@ var AuthPlugin = class {
44
258
  }
45
259
  /**
46
260
  * Register authentication routes with HTTP server
261
+ *
262
+ * Uses better-auth's universal handler for all authentication requests.
263
+ * This forwards all requests under basePath to better-auth, which handles:
264
+ * - Email/password authentication
265
+ * - OAuth providers (Google, GitHub, etc.)
266
+ * - Session management
267
+ * - Password reset
268
+ * - Email verification
269
+ * - 2FA, passkeys, magic links (if enabled)
47
270
  */
48
271
  registerAuthRoutes(httpServer, ctx) {
49
272
  if (!this.authManager) return;
50
273
  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) => {
274
+ if (!("getRawApp" in httpServer) || typeof httpServer.getRawApp !== "function") {
275
+ ctx.logger.error("HTTP server does not support getRawApp() - wildcard routing requires Hono server");
276
+ throw new Error(
277
+ "AuthPlugin requires HonoServerPlugin for wildcard routing support. Please ensure HonoServerPlugin is loaded before AuthPlugin."
278
+ );
279
+ }
280
+ const rawApp = httpServer.getRawApp();
281
+ rawApp.all(`${basePath}/*`, async (c) => {
80
282
  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
283
+ const request = c.req.raw;
284
+ const url = new URL(request.url);
285
+ const authPath = url.pathname.replace(basePath, "");
286
+ const rewrittenUrl = new URL(authPath || "/", url.origin);
287
+ rewrittenUrl.search = url.search;
288
+ const rewrittenRequest = new Request(rewrittenUrl, {
289
+ method: request.method,
290
+ headers: request.headers,
291
+ body: request.body,
292
+ duplex: "half"
293
+ // Required for Request with body
91
294
  });
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 });
295
+ const response = await this.authManager.handleRequest(rewrittenRequest);
296
+ return response;
100
297
  } catch (error) {
101
298
  const err = error instanceof Error ? error : new Error(String(error));
102
- res.status(401).json({
103
- success: false,
104
- error: err.message
105
- });
299
+ ctx.logger.error("Auth request error:", err);
300
+ return new Response(
301
+ JSON.stringify({
302
+ success: false,
303
+ error: err.message
304
+ }),
305
+ {
306
+ status: 500,
307
+ headers: { "Content-Type": "application/json" }
308
+ }
309
+ );
106
310
  }
107
311
  });
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
- });
312
+ ctx.logger.info(`Auth routes registered: All requests under ${basePath}/* forwarded to better-auth`);
117
313
  }
118
314
  };
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");
315
+
316
+ // src/objects/auth-user.object.ts
317
+ import { ObjectSchema, Field } from "@objectstack/spec/data";
318
+ var AuthUser = ObjectSchema.create({
319
+ name: "user",
320
+ label: "User",
321
+ pluralLabel: "Users",
322
+ icon: "user",
323
+ description: "User accounts for authentication",
324
+ titleFormat: "{name} ({email})",
325
+ compactLayout: ["name", "email", "emailVerified"],
326
+ fields: {
327
+ // ID is auto-generated by ObjectQL
328
+ id: Field.text({
329
+ label: "User ID",
330
+ required: true,
331
+ readonly: true
332
+ }),
333
+ createdAt: Field.datetime({
334
+ label: "Created At",
335
+ defaultValue: "NOW()",
336
+ readonly: true
337
+ }),
338
+ updatedAt: Field.datetime({
339
+ label: "Updated At",
340
+ defaultValue: "NOW()",
341
+ readonly: true
342
+ }),
343
+ email: Field.email({
344
+ label: "Email",
345
+ required: true,
346
+ searchable: true
347
+ }),
348
+ emailVerified: Field.boolean({
349
+ label: "Email Verified",
350
+ defaultValue: false
351
+ }),
352
+ name: Field.text({
353
+ label: "Name",
354
+ required: true,
355
+ searchable: true,
356
+ maxLength: 255
357
+ }),
358
+ image: Field.url({
359
+ label: "Profile Image",
360
+ required: false
361
+ })
362
+ },
363
+ // Database indexes for performance
364
+ indexes: [
365
+ { fields: ["email"], unique: true },
366
+ { fields: ["createdAt"], unique: false }
367
+ ],
368
+ // Enable features
369
+ enable: {
370
+ trackHistory: true,
371
+ searchable: true,
372
+ apiEnabled: true,
373
+ apiMethods: ["get", "list", "create", "update", "delete"],
374
+ trash: true,
375
+ mru: true
376
+ },
377
+ // Validation Rules
378
+ validations: [
379
+ {
380
+ name: "email_unique",
381
+ type: "unique",
382
+ severity: "error",
383
+ message: "Email must be unique",
384
+ fields: ["email"],
385
+ caseSensitive: false
386
+ }
387
+ ]
388
+ });
389
+
390
+ // src/objects/auth-session.object.ts
391
+ import { ObjectSchema as ObjectSchema2, Field as Field2 } from "@objectstack/spec/data";
392
+ var AuthSession = ObjectSchema2.create({
393
+ name: "session",
394
+ label: "Session",
395
+ pluralLabel: "Sessions",
396
+ icon: "key",
397
+ description: "Active user sessions",
398
+ titleFormat: "Session {token}",
399
+ compactLayout: ["userId", "expiresAt", "ipAddress"],
400
+ fields: {
401
+ id: Field2.text({
402
+ label: "Session ID",
403
+ required: true,
404
+ readonly: true
405
+ }),
406
+ createdAt: Field2.datetime({
407
+ label: "Created At",
408
+ defaultValue: "NOW()",
409
+ readonly: true
410
+ }),
411
+ updatedAt: Field2.datetime({
412
+ label: "Updated At",
413
+ defaultValue: "NOW()",
414
+ readonly: true
415
+ }),
416
+ userId: Field2.text({
417
+ label: "User ID",
418
+ required: true
419
+ }),
420
+ expiresAt: Field2.datetime({
421
+ label: "Expires At",
422
+ required: true
423
+ }),
424
+ token: Field2.text({
425
+ label: "Session Token",
426
+ required: true
427
+ }),
428
+ ipAddress: Field2.text({
429
+ label: "IP Address",
430
+ required: false,
431
+ maxLength: 45
432
+ // Support IPv6
433
+ }),
434
+ userAgent: Field2.textarea({
435
+ label: "User Agent",
436
+ required: false
437
+ })
438
+ },
439
+ // Database indexes for performance
440
+ indexes: [
441
+ { fields: ["token"], unique: true },
442
+ { fields: ["userId"], unique: false },
443
+ { fields: ["expiresAt"], unique: false }
444
+ ],
445
+ // Enable features
446
+ enable: {
447
+ trackHistory: false,
448
+ // Sessions don't need history tracking
449
+ searchable: false,
450
+ apiEnabled: true,
451
+ apiMethods: ["get", "list", "create", "delete"],
452
+ // No update for sessions
453
+ trash: false,
454
+ // Sessions should be hard deleted
455
+ mru: false
127
456
  }
128
- async logout(_token) {
129
- throw new Error("Logout not yet implemented");
457
+ });
458
+
459
+ // src/objects/auth-account.object.ts
460
+ import { ObjectSchema as ObjectSchema3, Field as Field3 } from "@objectstack/spec/data";
461
+ var AuthAccount = ObjectSchema3.create({
462
+ name: "account",
463
+ label: "Account",
464
+ pluralLabel: "Accounts",
465
+ icon: "link",
466
+ description: "OAuth and authentication provider accounts",
467
+ titleFormat: "{providerId} - {accountId}",
468
+ compactLayout: ["providerId", "userId", "accountId"],
469
+ fields: {
470
+ id: Field3.text({
471
+ label: "Account ID",
472
+ required: true,
473
+ readonly: true
474
+ }),
475
+ createdAt: Field3.datetime({
476
+ label: "Created At",
477
+ defaultValue: "NOW()",
478
+ readonly: true
479
+ }),
480
+ updatedAt: Field3.datetime({
481
+ label: "Updated At",
482
+ defaultValue: "NOW()",
483
+ readonly: true
484
+ }),
485
+ providerId: Field3.text({
486
+ label: "Provider ID",
487
+ required: true,
488
+ description: "OAuth provider identifier (google, github, etc.)"
489
+ }),
490
+ accountId: Field3.text({
491
+ label: "Provider Account ID",
492
+ required: true,
493
+ description: "User's ID in the provider's system"
494
+ }),
495
+ userId: Field3.text({
496
+ label: "User ID",
497
+ required: true,
498
+ description: "Link to user table"
499
+ }),
500
+ accessToken: Field3.textarea({
501
+ label: "Access Token",
502
+ required: false
503
+ }),
504
+ refreshToken: Field3.textarea({
505
+ label: "Refresh Token",
506
+ required: false
507
+ }),
508
+ idToken: Field3.textarea({
509
+ label: "ID Token",
510
+ required: false
511
+ }),
512
+ accessTokenExpiresAt: Field3.datetime({
513
+ label: "Access Token Expires At",
514
+ required: false
515
+ }),
516
+ refreshTokenExpiresAt: Field3.datetime({
517
+ label: "Refresh Token Expires At",
518
+ required: false
519
+ }),
520
+ scope: Field3.text({
521
+ label: "OAuth Scope",
522
+ required: false
523
+ }),
524
+ password: Field3.text({
525
+ label: "Password Hash",
526
+ required: false,
527
+ description: "Hashed password for email/password provider"
528
+ })
529
+ },
530
+ // Database indexes for performance
531
+ indexes: [
532
+ { fields: ["userId"], unique: false },
533
+ { fields: ["providerId", "accountId"], unique: true }
534
+ ],
535
+ // Enable features
536
+ enable: {
537
+ trackHistory: false,
538
+ searchable: false,
539
+ apiEnabled: true,
540
+ apiMethods: ["get", "list", "create", "update", "delete"],
541
+ trash: true,
542
+ mru: false
130
543
  }
131
- async getSession(_token) {
132
- throw new Error("Session retrieval not yet implemented");
544
+ });
545
+
546
+ // src/objects/auth-verification.object.ts
547
+ import { ObjectSchema as ObjectSchema4, Field as Field4 } from "@objectstack/spec/data";
548
+ var AuthVerification = ObjectSchema4.create({
549
+ name: "verification",
550
+ label: "Verification",
551
+ pluralLabel: "Verifications",
552
+ icon: "shield-check",
553
+ description: "Email and phone verification tokens",
554
+ titleFormat: "Verification for {identifier}",
555
+ compactLayout: ["identifier", "expiresAt", "createdAt"],
556
+ fields: {
557
+ id: Field4.text({
558
+ label: "Verification ID",
559
+ required: true,
560
+ readonly: true
561
+ }),
562
+ createdAt: Field4.datetime({
563
+ label: "Created At",
564
+ defaultValue: "NOW()",
565
+ readonly: true
566
+ }),
567
+ updatedAt: Field4.datetime({
568
+ label: "Updated At",
569
+ defaultValue: "NOW()",
570
+ readonly: true
571
+ }),
572
+ value: Field4.text({
573
+ label: "Verification Token",
574
+ required: true,
575
+ description: "Token or code for verification"
576
+ }),
577
+ expiresAt: Field4.datetime({
578
+ label: "Expires At",
579
+ required: true
580
+ }),
581
+ identifier: Field4.text({
582
+ label: "Identifier",
583
+ required: true,
584
+ description: "Email address or phone number"
585
+ })
586
+ },
587
+ // Database indexes for performance
588
+ indexes: [
589
+ { fields: ["value"], unique: true },
590
+ { fields: ["identifier"], unique: false },
591
+ { fields: ["expiresAt"], unique: false }
592
+ ],
593
+ // Enable features
594
+ enable: {
595
+ trackHistory: false,
596
+ searchable: false,
597
+ apiEnabled: true,
598
+ apiMethods: ["get", "create", "delete"],
599
+ // No list or update
600
+ trash: false,
601
+ // Hard delete expired tokens
602
+ mru: false
133
603
  }
134
- };
604
+ });
135
605
  export {
136
- AuthPlugin
606
+ AuthAccount,
607
+ AuthManager,
608
+ AuthPlugin,
609
+ AuthSession,
610
+ AuthUser,
611
+ AuthVerification,
612
+ createObjectQLAdapter
137
613
  };
138
614
  //# sourceMappingURL=index.mjs.map