create-nexwire 4.2.2 → 4.2.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.
Files changed (52) hide show
  1. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  2. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  3. package/express/src/framework/database/app-schema.ts +37 -0
  4. package/express/src/framework/database/connection.ts +198 -196
  5. package/express/src/framework/database/seed.ts +35 -22
  6. package/express/src/framework/facade.ts +24 -5
  7. package/express/src/framework/kernel.ts +54 -52
  8. package/express/src/framework/maker-cli/index.mjs +2 -0
  9. package/express/src/framework/maker-cli/module/core.mjs +30 -0
  10. package/express/src/framework/maker-cli/module/index.mjs +10 -1
  11. package/express/src/framework/maker-cli/provider/core.mjs +49 -0
  12. package/express/src/framework/maker-cli/provider/index.mjs +12 -0
  13. package/express/src/framework/maker-cli/stubs/notification/controller.ts.stub +77 -77
  14. package/express/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
  15. package/express/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
  16. package/express/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
  17. package/express/src/framework/maker-cli/utils/help.mjs +6 -1
  18. package/express/src/framework/notification/index.ts +63 -7
  19. package/express/src/framework/providers/adapter.ts +125 -0
  20. package/express/src/framework/providers/contracts.ts +56 -0
  21. package/express/src/framework/providers/loader.ts +105 -0
  22. package/express/src/framework/providers/registry.ts +62 -0
  23. package/express/src/middlewares/auth-middleware.ts +65 -38
  24. package/express/src/providers/README.md +107 -0
  25. package/express/src/storage/database/nexwire.sqlite +0 -0
  26. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  27. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  28. package/hono/src/framework/database/app-schema.ts +37 -0
  29. package/hono/src/framework/database/connection.ts +198 -196
  30. package/hono/src/framework/database/seed.ts +35 -22
  31. package/hono/src/framework/facade.ts +48 -29
  32. package/hono/src/framework/kernel.ts +50 -48
  33. package/hono/src/framework/maker-cli/index.mjs +2 -0
  34. package/hono/src/framework/maker-cli/module/core.mjs +30 -0
  35. package/hono/src/framework/maker-cli/module/index.mjs +10 -1
  36. package/hono/src/framework/maker-cli/provider/core.mjs +49 -0
  37. package/hono/src/framework/maker-cli/provider/index.mjs +12 -0
  38. package/hono/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -85
  39. package/hono/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
  40. package/hono/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
  41. package/hono/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
  42. package/hono/src/framework/maker-cli/utils/help.mjs +6 -1
  43. package/hono/src/framework/notification/index.ts +63 -7
  44. package/hono/src/framework/providers/adapter.ts +125 -0
  45. package/hono/src/framework/providers/contracts.ts +56 -0
  46. package/hono/src/framework/providers/loader.ts +105 -0
  47. package/hono/src/framework/providers/registry.ts +62 -0
  48. package/hono/src/middlewares/auth-middleware.ts +65 -38
  49. package/hono/src/providers/README.md +107 -0
  50. package/hono/src/storage/database/nexwire.sqlite +0 -0
  51. package/package.json +1 -1
  52. package/src/index.js +14 -6
@@ -0,0 +1,125 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db } from "@/framework/database/connection.js";
3
+ import type { AuthProvider } from "./contracts.js";
4
+ import { loadProviders } from "./loader.js";
5
+ import { getAuthProvider } from "./registry.js";
6
+
7
+ let defaultProvider: AuthProvider | null = null;
8
+ let defaultLoadAttempted = false;
9
+
10
+ /**
11
+ * Why: Builds the built-in auth provider from the default auth module.
12
+ * When: No custom provider was registered by the application.
13
+ * Where: Auth provider adapter.
14
+ * How: Lazily imports the default user/refresh-token models. When the default
15
+ * auth module was deleted or replaced, no provider is produced.
16
+ */
17
+ async function loadDefaultProvider(): Promise<AuthProvider | null> {
18
+ if (defaultProvider) return defaultProvider;
19
+ if (defaultLoadAttempted) return null;
20
+ defaultLoadAttempted = true;
21
+ try {
22
+ const userMod = await import("@/modules/auth/database/models/user.js").catch(() => null);
23
+ if (!userMod?.users || !userMod?.refreshTokens) return null;
24
+
25
+ const users = userMod.users;
26
+ const refreshTokens = userMod.refreshTokens;
27
+
28
+ defaultProvider = {
29
+ async getUserById(id: number) {
30
+ try {
31
+ const user = await (db.query.users.findFirst as any)?.({
32
+ where: eq(users.id, id),
33
+ with: { role: true }
34
+ });
35
+ if (user) return user;
36
+ } catch {}
37
+ try {
38
+ const rows = await db.select().from(users).where(eq(users.id, id));
39
+ return rows?.[0] ?? null;
40
+ } catch {
41
+ return null;
42
+ }
43
+ },
44
+ async findRefreshToken(jti: string) {
45
+ try {
46
+ const token = await (db.query.refreshTokens.findFirst as any)?.({
47
+ where: eq(refreshTokens.jti, jti)
48
+ });
49
+ if (token) return token;
50
+ } catch {}
51
+ try {
52
+ const rows = await db.select().from(refreshTokens).where(eq(refreshTokens.jti, jti));
53
+ return rows?.[0] ?? null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ },
58
+ async deleteRefreshToken(tokenId: number) {
59
+ try {
60
+ await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId));
61
+ } catch {}
62
+ },
63
+ async revokeRefreshToken(tokenId: number) {
64
+ try {
65
+ await db.delete(refreshTokens).where(eq(refreshTokens.id, tokenId));
66
+ } catch {}
67
+ },
68
+ async getAuthContext(user: any, payload?: any) {
69
+ try {
70
+ if (user?.role && typeof user.role === "object") {
71
+ return {
72
+ ...(payload || {}),
73
+ id: user.id,
74
+ email: user.email,
75
+ roleId: user.role?.id ?? null,
76
+ role: user.role?.name ?? null,
77
+ ...user
78
+ };
79
+ }
80
+ } catch {}
81
+ return {
82
+ ...(payload || {}),
83
+ id: user.id,
84
+ email: user.email,
85
+ roleId: user.roleId ?? null,
86
+ role: user.role ?? null,
87
+ ...user
88
+ };
89
+ },
90
+ async getUserContact(userId: number) {
91
+ try {
92
+ const user = await this.getUserById(userId);
93
+ if (!user) return null;
94
+ return {
95
+ email: user.email ?? null,
96
+ name: user.name ?? user.fullName ?? user.username ?? null,
97
+ phone: user.phone ?? user.phoneNumber ?? null
98
+ };
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ };
104
+ return defaultProvider;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Why: Resolves the auth provider the framework should use.
112
+ * When: The auth middleware or the notification mailer needs user data.
113
+ * Where: Auth provider adapter.
114
+ * How: Application providers are loaded first, so a file in `src/providers/`
115
+ * wins. Otherwise the built-in provider from the default auth module is
116
+ * used. When neither exists the framework degrades gracefully.
117
+ */
118
+ export async function resolveAuthProvider(): Promise<AuthProvider | null> {
119
+ await loadProviders();
120
+ const registered = getAuthProvider();
121
+ if (registered) return registered;
122
+ const def = await loadDefaultProvider();
123
+ if (def) return def;
124
+ return null;
125
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Why: Defines the shapes a pluggable provider has to satisfy.
3
+ * When: A developer replaces a framework default (auth today, payments later).
4
+ * Where: Imported through the framework facade, never from framework internals.
5
+ * How: Interfaces stay deliberately loose so custom user shapes pass through.
6
+ */
7
+
8
+ /** Registry key of the built-in auth provider contract. */
9
+ export const AUTH_PROVIDER_KEY = "auth";
10
+
11
+ /**
12
+ * Registry key for any provider implementation.
13
+ *
14
+ * `string & {}` keeps the literal keys above discoverable by editors while
15
+ * still accepting any custom key, so a project can register its own contracts
16
+ * (payments, otp, storage, ...) without the framework having to know them.
17
+ */
18
+ export type ProviderKey = typeof AUTH_PROVIDER_KEY | (string & {});
19
+
20
+ export interface AuthProviderUser {
21
+ id: number;
22
+ email?: string | null;
23
+ [key: string]: any;
24
+ }
25
+
26
+ export interface AuthProviderRefreshToken {
27
+ id: number;
28
+ jti: string;
29
+ revoked?: boolean | null;
30
+ expiresAt?: Date | string | null;
31
+ userId?: number | null;
32
+ [key: string]: any;
33
+ }
34
+
35
+ export interface AuthUserContext {
36
+ id: number;
37
+ email?: string | null;
38
+ roleId?: number | null;
39
+ role?: string | null;
40
+ [key: string]: any;
41
+ }
42
+
43
+ export interface AuthUserContact {
44
+ email?: string | null;
45
+ name?: string | null;
46
+ phone?: string | null;
47
+ }
48
+
49
+ export interface AuthProvider {
50
+ getUserById(id: number): Promise<AuthProviderUser | null>;
51
+ findRefreshToken(jti: string): Promise<AuthProviderRefreshToken | null>;
52
+ revokeRefreshToken?(tokenId: number): Promise<void>;
53
+ deleteRefreshToken?(tokenId: number): Promise<void>;
54
+ getAuthContext?(user: AuthProviderUser, payload?: any): Promise<AuthUserContext | any>;
55
+ getUserContact?(userId: number): Promise<AuthUserContact | null>;
56
+ }
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { glob } from "glob";
5
+ import { discoverModuleFiles } from "@/framework/modules/discover.js";
6
+ import { logger } from "@/framework/support/logger.js";
7
+
8
+ /**
9
+ * Why: Finds the app-level provider folder for the running code.
10
+ * When: Providers are loaded at boot.
11
+ * Where: Provider loader.
12
+ * How: Mirrors the module discovery rule — `src/providers` in development,
13
+ * `dist/src/providers` when the compiled server is the entrypoint.
14
+ */
15
+ function resolveProvidersPath() {
16
+ const cwd = process.cwd();
17
+ const srcProviders = path.resolve(cwd, "src/providers");
18
+ const distProviders = path.resolve(cwd, "dist/src/providers");
19
+ const entry = String(process.argv[1] || "");
20
+ const runningDist = /[\\/]dist[\\/]/.test(entry);
21
+
22
+ if (runningDist && fs.existsSync(distProviders)) return distProviders;
23
+ return srcProviders;
24
+ }
25
+
26
+ export const providersPath = resolveProvidersPath();
27
+
28
+ let loadPromise: Promise<number> | null = null;
29
+ let loading = false;
30
+
31
+ /**
32
+ * Why: Collects every provider file that should run at boot.
33
+ * When: The loader runs.
34
+ * Where: Provider loader.
35
+ * How: Globs `src/providers/**` for app-wide providers, and
36
+ * `src/modules/**\/providers.ts` plus `src/modules/**\/providers/*.ts` for
37
+ * module-scoped ones. Tests and declaration files are ignored.
38
+ */
39
+ async function collectProviderFiles(): Promise<string[]> {
40
+ const files: string[] = [];
41
+
42
+ if (fs.existsSync(providersPath)) {
43
+ const appFiles = await glob("**/*.{ts,js,mjs}", {
44
+ cwd: providersPath,
45
+ absolute: true,
46
+ nodir: true,
47
+ ignore: ["**/__tests__/**", "**/*.d.ts", "**/*.test.*", "**/*.spec.*"],
48
+ windowsPathsNoEscape: true
49
+ });
50
+ files.push(...appFiles);
51
+ }
52
+
53
+ try {
54
+ // A module plugs its own implementation in either from a single file
55
+ // (providers.ts) or from a folder (providers/*.ts). Both are imported.
56
+ const moduleFiles = await discoverModuleFiles("**/providers.{ts,js}");
57
+ const moduleProviderFiles = await discoverModuleFiles("**/providers/*.{ts,js}");
58
+ files.push(...moduleFiles, ...moduleProviderFiles);
59
+ } catch {}
60
+
61
+ return [...new Set(files)].sort();
62
+ }
63
+
64
+ /**
65
+ * Why: Imports each discovered provider file so its registration runs.
66
+ * When: The loader runs.
67
+ * Where: Provider loader.
68
+ * How: Imports in sorted order; a broken file is logged and skipped instead of
69
+ * taking the whole application down.
70
+ */
71
+ async function loadProviderFiles(): Promise<number> {
72
+ const files = await collectProviderFiles();
73
+
74
+ for (const file of files) {
75
+ try {
76
+ await import(pathToFileURL(file).href);
77
+ } catch (error) {
78
+ logger.warn("Provider file failed to load", {
79
+ file,
80
+ error: error instanceof Error ? error.message : String(error)
81
+ });
82
+ }
83
+ }
84
+
85
+ return files.length;
86
+ }
87
+
88
+ /**
89
+ * Why: Runs the application's provider files exactly once.
90
+ * When: The kernel boots, and on the first provider lookup.
91
+ * Where: Kernel startup and the auth provider adapter.
92
+ * How: Caches the promise so repeated lookups cost nothing. A provider file
93
+ * that itself asks for a provider during load gets an immediate answer
94
+ * instead of deadlocking on its own load.
95
+ */
96
+ export async function loadProviders(): Promise<number> {
97
+ if (loading) return 0;
98
+ if (!loadPromise) {
99
+ loading = true;
100
+ loadPromise = loadProviderFiles().finally(() => {
101
+ loading = false;
102
+ });
103
+ }
104
+ return loadPromise;
105
+ }
@@ -0,0 +1,62 @@
1
+ import { AUTH_PROVIDER_KEY, type AuthProvider, type ProviderKey } from "./contracts.js";
2
+
3
+ /**
4
+ * Why: Holds the app-wide provider implementations in one place.
5
+ * When: A provider file in `src/providers/` (or a module's `providers.ts`)
6
+ * registers its implementation at boot.
7
+ * Where: Read by the framework through `resolve*Provider()` helpers.
8
+ * How: A plain key/value map. No container, no decorators, no auto-wiring.
9
+ */
10
+ const registry = new Map<string, unknown>();
11
+
12
+ /**
13
+ * Why: Registers a provider implementation under a key.
14
+ * When: A provider file runs at boot and claims a contract.
15
+ * Where: Application code, through the framework facade.
16
+ * How: Stores the implementation and returns it so it can be inlined.
17
+ */
18
+ export function registerProvider<T>(key: ProviderKey, implementation: T): T {
19
+ registry.set(key, implementation);
20
+ return implementation;
21
+ }
22
+
23
+ /** Returns the registered implementation for a key, or null when absent. */
24
+ export function getProvider<T = unknown>(key: ProviderKey): T | null {
25
+ const found = registry.get(key);
26
+ return (found as T) ?? null;
27
+ }
28
+
29
+ /** Reports whether a provider is registered for a key. */
30
+ export function hasProvider(key: ProviderKey): boolean {
31
+ return registry.has(key);
32
+ }
33
+
34
+ /** Removes a registered provider. Mainly useful in tests. */
35
+ export function clearProvider(key: ProviderKey): void {
36
+ registry.delete(key);
37
+ }
38
+
39
+ /** Lists every registered provider key, sorted for stable output. */
40
+ export function listProviders(): string[] {
41
+ return [...registry.keys()].sort();
42
+ }
43
+
44
+ /** Registers the auth provider that the framework's auth middleware uses. */
45
+ export function registerAuthProvider(provider: AuthProvider): AuthProvider {
46
+ return registerProvider(AUTH_PROVIDER_KEY, provider);
47
+ }
48
+
49
+ /** Returns the registered auth provider, or null when none was registered. */
50
+ export function getAuthProvider(): AuthProvider | null {
51
+ return getProvider<AuthProvider>(AUTH_PROVIDER_KEY);
52
+ }
53
+
54
+ /** Reports whether a custom auth provider is registered. */
55
+ export function hasAuthProvider(): boolean {
56
+ return hasProvider(AUTH_PROVIDER_KEY);
57
+ }
58
+
59
+ /** Removes the registered auth provider. Mainly useful in tests. */
60
+ export function clearAuthProvider(): void {
61
+ clearProvider(AUTH_PROVIDER_KEY);
62
+ }
@@ -1,19 +1,23 @@
1
- import { eq } from "drizzle-orm";
2
1
  import { type NextFunction, type Request, type Response } from "express";
3
- import { cookie, db, HttpStatusCodes, jwt } from "@/framework/facade.js";
4
- import { refreshTokens, users } from "@/modules/auth/database/models/user.js";
2
+ import { cookie, HttpStatusCodes, jwt } from "@/framework/facade.js";
3
+ import { resolveAuthProvider } from "@/framework/providers/adapter.js";
5
4
 
6
5
  export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
6
+ const provider = await resolveAuthProvider();
7
+ if (!provider) {
8
+ console.warn(
9
+ "[auth-middleware] auth provider not available; skipping auth. Register one in src/providers/ or restore src/modules/auth."
10
+ );
11
+ return next();
12
+ }
13
+
7
14
  const accessToken = await cookie.getAuth(req);
8
15
 
9
16
  if (accessToken) {
10
- const accessPayload = await jwt.verifyToken(accessToken, "access");
17
+ const accessPayload: any = await jwt.verifyToken(accessToken, "access");
11
18
 
12
19
  if (accessPayload) {
13
- const user = await db.query.users.findFirst({
14
- where: eq(users.id, accessPayload.id as number),
15
- with: { role: true }
16
- });
20
+ const user = await provider.getUserById((accessPayload as any)?.id);
17
21
 
18
22
  if (!user) {
19
23
  cookie.deleteAuth(res);
@@ -21,13 +25,20 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
21
25
  return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Unauthorized" });
22
26
  }
23
27
 
24
- res.locals.auth = {
25
- ...accessPayload,
26
- id: user.id,
27
- email: user.email,
28
- roleId: user.role?.id ?? null,
29
- role: user.role?.name ?? null
30
- };
28
+ let authCtx: any;
29
+ if (provider.getAuthContext) {
30
+ authCtx = await provider.getAuthContext(user, accessPayload);
31
+ } else {
32
+ authCtx = {
33
+ ...(accessPayload || {}),
34
+ id: (user as any).id,
35
+ email: (user as any).email,
36
+ roleId: (user as any).role?.id ?? (user as any).roleId ?? null,
37
+ role: (user as any).role?.name ?? (user as any).role ?? null,
38
+ ...(user as any)
39
+ };
40
+ }
41
+ res.locals.auth = authCtx;
31
42
  return next();
32
43
  }
33
44
  }
@@ -40,7 +51,7 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
40
51
  return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Unauthorized" });
41
52
  }
42
53
 
43
- const refreshPayload = await jwt.verifyToken(refreshToken, "refresh");
54
+ const refreshPayload: any = await jwt.verifyToken(refreshToken, "refresh");
44
55
 
45
56
  if (!refreshPayload?.jti) {
46
57
  cookie.deleteAuth(res);
@@ -48,27 +59,27 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
48
59
  return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Invalid token" });
49
60
  }
50
61
 
51
- const storedToken = await db.query.refreshTokens.findFirst({
52
- where: eq(refreshTokens.jti, refreshPayload.jti as string)
53
- });
62
+ const storedToken = await provider.findRefreshToken((refreshPayload as any)?.jti);
54
63
 
55
- if (!storedToken || storedToken.revoked) {
64
+ if (!storedToken || (storedToken as any).revoked) {
56
65
  cookie.deleteAuth(res);
57
66
  cookie.deleteRefresh(res);
58
67
  return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Invalid token" });
59
68
  }
60
69
 
61
- if (storedToken.expiresAt.getTime() < Date.now()) {
62
- await db.delete(refreshTokens).where(eq(refreshTokens.id, storedToken.id));
63
- cookie.deleteAuth(res);
64
- cookie.deleteRefresh(res);
65
- return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Invalid token" });
70
+ const expiresAt = (storedToken as any).expiresAt;
71
+ if (expiresAt) {
72
+ const exp = expiresAt instanceof Date ? expiresAt.getTime() : new Date(expiresAt).getTime();
73
+ if (!Number.isNaN(exp) && exp < Date.now()) {
74
+ if (provider.deleteRefreshToken) await provider.deleteRefreshToken((storedToken as any).id);
75
+ else if (provider.revokeRefreshToken) await provider.revokeRefreshToken((storedToken as any).id);
76
+ cookie.deleteAuth(res);
77
+ cookie.deleteRefresh(res);
78
+ return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Invalid token" });
79
+ }
66
80
  }
67
81
 
68
- const user = await db.query.users.findFirst({
69
- where: eq(users.id, refreshPayload.id as number),
70
- with: { role: true }
71
- });
82
+ const user = await provider.getUserById((refreshPayload as any)?.id);
72
83
 
73
84
  if (!user) {
74
85
  cookie.deleteAuth(res);
@@ -76,23 +87,39 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
76
87
  return res.status(HttpStatusCodes.UNAUTHORIZED).json({ message: "Unauthorized" });
77
88
  }
78
89
 
90
+ let baseCtx: any;
91
+ if (provider.getAuthContext) {
92
+ baseCtx = await provider.getAuthContext(user, refreshPayload);
93
+ } else {
94
+ baseCtx = {
95
+ ...(refreshPayload || {}),
96
+ id: (user as any).id,
97
+ email: (user as any).email,
98
+ roleId: (user as any).role?.id ?? (user as any).roleId ?? null,
99
+ role: (user as any).role?.name ?? (user as any).role ?? null,
100
+ ...(user as any)
101
+ };
102
+ }
103
+
79
104
  const newAccessToken = await jwt.generateToken(
80
105
  {
81
- id: user.id,
82
- email: user.email,
83
- roleId: user.role?.id ?? null,
84
- role: user.role?.name ?? null
106
+ id: (user as any).id,
107
+ email: (user as any).email,
108
+ roleId: baseCtx?.roleId ?? (user as any).role?.id ?? (user as any).roleId ?? null,
109
+ role: baseCtx?.role ?? (user as any).role?.name ?? (user as any).role ?? null
85
110
  },
86
111
  "access"
87
112
  );
88
113
 
89
114
  await cookie.setAuth(res, newAccessToken.token);
90
115
  res.locals.auth = {
91
- id: user.id,
92
- email: user.email,
93
- roleId: user.role?.id ?? null,
94
- role: user.role?.name ?? null,
95
- type: "access"
116
+ ...(baseCtx || {}),
117
+ id: (user as any).id,
118
+ email: (user as any).email,
119
+ roleId: baseCtx?.roleId ?? (user as any).role?.id ?? (user as any).roleId ?? null,
120
+ role: baseCtx?.role ?? (user as any).role?.name ?? (user as any).role ?? null,
121
+ type: "access",
122
+ ...(user as any)
96
123
  };
97
124
  return next();
98
125
  }
@@ -0,0 +1,107 @@
1
+ # Providers
2
+
3
+ Provider files in this folder are imported automatically when the application
4
+ boots. Use them for anything the **whole app** needs — a replacement auth
5
+ implementation, a payment gateway, an SMS driver, a storage backend.
6
+
7
+ There is no wiring step: drop a file in here and it runs.
8
+
9
+ ## How it works
10
+
11
+ 1. Every `src/providers/**/*.{ts,js,mjs}` file is imported at boot.
12
+ 2. Every `src/modules/<module>/providers.ts` file is imported at boot too, so a
13
+ module can plug itself into the app without the app knowing about it.
14
+ 3. Every `src/modules/<module>/providers/*.ts` file is imported at boot as well,
15
+ for a module that offers more than one implementation.
16
+ 4. A provider file claims a contract by registering an implementation.
17
+
18
+ ```ts
19
+ // src/providers/payment.ts
20
+ import { registerProvider } from "@/framework/facade.js";
21
+
22
+ registerProvider("payment", {
23
+ async charge(order) {
24
+ return { id: "ch_123", status: "paid" };
25
+ }
26
+ });
27
+ ```
28
+
29
+ Read it back the same way:
30
+
31
+ ```ts
32
+ import { getProvider } from "@/framework/facade.js";
33
+
34
+ const payment = getProvider<any>("payment");
35
+ ```
36
+
37
+ ## Generating one
38
+
39
+ ```bash
40
+ npm run maker provider:make payment
41
+ npm run maker module:make-provider billing stripe
42
+ ```
43
+
44
+ `provider:make` writes `src/providers/<name>.ts` for the whole app.
45
+ `module:make-provider` writes `src/modules/<module>/providers/<name>.ts` for a
46
+ single module. Both files register themselves when they are imported.
47
+
48
+ ## Replacing the built-in auth
49
+
50
+ The framework ships with an auth module under `src/modules/auth`. To replace
51
+ it, delete that folder and register your own auth provider:
52
+
53
+ ```ts
54
+ // src/providers/auth.ts
55
+ import { registerAuthProvider } from "@/framework/facade.js";
56
+ import type { AuthProvider } from "@/framework/facade.js";
57
+
58
+ const authProvider: AuthProvider = {
59
+ async getUserById(id) {
60
+ // Return any user shape you like. Extra fields (tenantId, plan, status, ...)
61
+ // are passed straight through to `c.get("auth")` / `res.locals.auth`.
62
+ return { id, email: "user@example.com", tenantId: 42, plan: "pro" };
63
+ },
64
+ async findRefreshToken(jti) {
65
+ // Return the stored refresh token, or null when it is unknown.
66
+ return { id: 1, jti, revoked: false, expiresAt: new Date() };
67
+ },
68
+ async revokeRefreshToken(tokenId) {
69
+ // Optional. Called when a refresh token is expired or revoked.
70
+ },
71
+ async getAuthContext(user, payload) {
72
+ // Optional. Shapes what handlers receive as the auth context.
73
+ return { id: user.id, email: user.email, tenantId: user.tenantId };
74
+ },
75
+ async getUserContact(userId) {
76
+ // Optional. Used by `notify()` to find the email address of a user.
77
+ return { email: "user@example.com", name: "User" };
78
+ }
79
+ };
80
+
81
+ registerAuthProvider(authProvider);
82
+ ```
83
+
84
+ Import `registerAuthProvider` and the `AuthProvider` type from
85
+ `@/framework/facade.js` — never from `@/framework/...` internals. The facade is
86
+ the public API of the framework.
87
+
88
+ ## Module-to-module
89
+
90
+ Modules do not import each other. They talk through:
91
+
92
+ - `src/modules/<module>/facade.ts` — the public API of a module.
93
+ - `dispatchEvent` / `dispatchCommand` — for cross-module notifications.
94
+ - `src/providers/` — for app-wide implementations.
95
+
96
+ ## Contract reference
97
+
98
+ | Method | Required | Purpose |
99
+ | ----------------------------------- | -------- | -------------------------------------------------------------- |
100
+ | `getUserById(id)` | Yes | Loads a user. Any extra fields are passed through to the auth context. |
101
+ | `findRefreshToken(jti)` | Yes | Loads a stored refresh token by its JWT id. |
102
+ | `revokeRefreshToken(id)` | No | Revokes a refresh token. `deleteRefreshToken(id)` also works. |
103
+ | `getAuthContext(user, payload)` | No | Shapes the object handlers receive as `auth`. |
104
+ | `getUserContact(userId)` | No | Finds the email/name of a user, used by notifications. |
105
+
106
+ When no provider is registered and `src/modules/auth` was deleted, the auth
107
+ middleware logs a warning and lets the request through instead of crashing.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "6",
3
3
  "dialect": "sqlite",
4
- "id": "e2276ee8-13c8-4baf-ab07-32e843a089ac",
4
+ "id": "a2d9b389-ddab-4eec-bd8c-e0cca638e287",
5
5
  "prevId": "00000000-0000-0000-0000-000000000000",
6
6
  "tables": {
7
7
  "notifications": {
@@ -5,7 +5,7 @@
5
5
  {
6
6
  "idx": 0,
7
7
  "version": "6",
8
- "when": 1791141088797,
8
+ "when": 1792609267184,
9
9
  "tag": "0000_init",
10
10
  "breakpoints": true
11
11
  }
@@ -0,0 +1,37 @@
1
+ import { discoverModuleFiles, importFile } from "@/framework/modules/discover.js";
2
+
3
+ /**
4
+ * Why: Builds the Drizzle schema from the modules that actually exist.
5
+ * When: The database connection is initialised.
6
+ * Where: Framework database connection.
7
+ * How: Discovers `src/modules/**\/database/models/*` and merges their exports.
8
+ * The generated barrel at `src/database/schema.ts` is only used by
9
+ * drizzle-kit, so deleting or replacing a module never leaves the
10
+ * framework importing a file that is no longer there.
11
+ */
12
+ let cachedSchema: Record<string, unknown> | null = null;
13
+
14
+ export async function loadAppSchema(): Promise<Record<string, unknown>> {
15
+ if (cachedSchema) return cachedSchema;
16
+
17
+ const schema: Record<string, unknown> = {};
18
+ const files = await discoverModuleFiles("**/database/models/*.{ts,js}");
19
+
20
+ for (const file of files) {
21
+ try {
22
+ const models = await importFile(file);
23
+ Object.assign(schema, models);
24
+ } catch {
25
+ // A model file that fails to load is skipped: the rest of the schema
26
+ // still works, and the framework keeps booting.
27
+ }
28
+ }
29
+
30
+ cachedSchema = schema;
31
+ return schema;
32
+ }
33
+
34
+ /** Clears the cached schema. Mainly useful in tests. */
35
+ export function clearAppSchema(): void {
36
+ cachedSchema = null;
37
+ }