@pattern-stack/codegen 0.8.0 → 0.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pattern-stack/codegen",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Entity-driven code generation for full-stack TypeScript applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -112,5 +112,13 @@ export {
112
112
  // Controller
113
113
  export { AuthController } from './controllers/auth.controller';
114
114
 
115
+ // Middleware — RequesterContext boundary (bridges auth → ambient tenant scope)
116
+ export {
117
+ installRequesterContext,
118
+ makeRequesterContextMiddleware,
119
+ resolveRequesterContext,
120
+ type RequesterContextOptions,
121
+ } from './middleware/requester-context';
122
+
115
123
  // Module
116
124
  export { AuthModule, type AuthModuleOptions } from './auth.module';
@@ -0,0 +1,141 @@
1
+ /**
2
+ * RequesterContext boundary install — bridges authentication to ambient
3
+ * tenant scoping.
4
+ *
5
+ * This is the missing link that makes `BaseRepository`'s ambient scoping
6
+ * (see `base-classes/tenant-context.ts`) actually engage on HTTP requests:
7
+ * it reads the requester off each request (via the consumer-bound
8
+ * `IUserContext`) and runs the rest of the request inside `withRequester(...)`,
9
+ * so every downstream repository read/write is automatically scoped — no
10
+ * threaded `userId`.
11
+ *
12
+ * ## Wiring (one line in your bootstrap)
13
+ *
14
+ * In `main.ts`, after `NestFactory.create`:
15
+ *
16
+ * ```ts
17
+ * import { installRequesterContext } from './shared/subsystems/auth/middleware/requester-context';
18
+ * const app = await NestFactory.create(AppModule);
19
+ * installRequesterContext(app); // no-op + warn if AUTH_USER_CONTEXT is unbound
20
+ * ```
21
+ *
22
+ * `installRequesterContext` resolves `AUTH_USER_CONTEXT` from the root DI
23
+ * container (so it sees the binding the consumer provides in AppModule) and
24
+ * registers a global Express middleware. Pairs with Swagger's `@ApiBearerAuth`
25
+ * "Authorize" button: paste a token there and every request it sends now flows
26
+ * through this boundary into a scoped repository call.
27
+ *
28
+ * ## Trust + failure model
29
+ *
30
+ * - The middleware TRUSTS whatever `IUserContext` returns — authentication and
31
+ * authorization (validating the token, deciding which scope a requester may
32
+ * claim) are the `IUserContext` implementation's job, exactly as for a
33
+ * hand-threaded `userId`.
34
+ * - When the requester cannot be resolved (no/invalid credentials — e.g. a
35
+ * public route, or the OAuth callback itself), the request proceeds WITHOUT
36
+ * an ambient context (`onUnresolved: 'unscoped'`, the default). A
37
+ * `userTracking` repo in lenient mode then runs unscoped; in strict mode it
38
+ * throws downstream — which is correct: unauthenticated callers must not
39
+ * reach scoped data. Set `onUnresolved: 'reject'` to fail the request at the
40
+ * boundary instead.
41
+ */
42
+ import type { INestApplication } from '@nestjs/common';
43
+ import {
44
+ withRequester,
45
+ type RequesterContext,
46
+ } from '../../../base-classes/tenant-context';
47
+ import { AUTH_USER_CONTEXT } from '../auth.tokens';
48
+ import type { IUserContext } from '../protocols/user-context';
49
+
50
+ /** Minimal Express-style middleware signature (avoids an `express` dep). */
51
+ type NextFn = (err?: unknown) => void;
52
+ type RequestHandler = (req: unknown, res: unknown, next: NextFn) => void;
53
+
54
+ export interface RequesterContextOptions {
55
+ /**
56
+ * What to do when `IUserContext` cannot resolve a requester (throws, or
57
+ * returns no `userId`).
58
+ * - `'unscoped'` (default): proceed without a context — public routes work;
59
+ * scoped repos run unscoped (lenient) or throw downstream (strict).
60
+ * - `'reject'`: fail the request at the boundary (`next(error)`).
61
+ */
62
+ onUnresolved?: 'unscoped' | 'reject';
63
+ }
64
+
65
+ /**
66
+ * Resolve the ambient context for a request: prefer the richer
67
+ * `resolveRequester` (org/superuser), else derive plain `'user'` scope from
68
+ * `getCurrentUserId`. Returns `undefined` when no requester can be determined.
69
+ */
70
+ export async function resolveRequesterContext(
71
+ userContext: IUserContext,
72
+ req: unknown,
73
+ ): Promise<RequesterContext | undefined> {
74
+ if (typeof userContext.resolveRequester === 'function') {
75
+ const ctx = await userContext.resolveRequester(req);
76
+ return ctx?.userId ? ctx : undefined;
77
+ }
78
+ const userId = await userContext.getCurrentUserId(req);
79
+ return userId ? { userId, organizationId: null } : undefined;
80
+ }
81
+
82
+ /**
83
+ * Build the global middleware. Runs the remainder of the request inside
84
+ * `withRequester(...)` so the ambient context propagates through every `await`
85
+ * to downstream repositories.
86
+ */
87
+ export function makeRequesterContextMiddleware(
88
+ userContext: IUserContext,
89
+ options: RequesterContextOptions = {},
90
+ ): RequestHandler {
91
+ const onUnresolved = options.onUnresolved ?? 'unscoped';
92
+ return (req, _res, next) => {
93
+ resolveRequesterContext(userContext, req).then(
94
+ (ctx) => {
95
+ if (!ctx) {
96
+ next();
97
+ return;
98
+ }
99
+ // als.run executes its callback synchronously; Express dispatches the
100
+ // rest of the pipeline inside next(), so all downstream handlers (and
101
+ // their awaits) inherit this context.
102
+ withRequester(ctx, async () => {
103
+ next();
104
+ });
105
+ },
106
+ (err) => {
107
+ if (onUnresolved === 'reject') {
108
+ next(err);
109
+ return;
110
+ }
111
+ next();
112
+ },
113
+ );
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Register the requester-context boundary on a Nest app. Resolves
119
+ * `AUTH_USER_CONTEXT` from the root container (so it sees the consumer's
120
+ * AppModule binding) and installs the global middleware. No-ops with a warning
121
+ * when `AUTH_USER_CONTEXT` is not bound, so calling it unconditionally in
122
+ * bootstrap is safe.
123
+ */
124
+ export function installRequesterContext(
125
+ app: INestApplication,
126
+ options: RequesterContextOptions = {},
127
+ ): void {
128
+ const userContext = app.get<IUserContext>(AUTH_USER_CONTEXT, {
129
+ strict: false,
130
+ });
131
+ if (!userContext) {
132
+ // eslint-disable-next-line no-console
133
+ console.warn(
134
+ '[auth] installRequesterContext: AUTH_USER_CONTEXT is not bound — ' +
135
+ 'request scoping NOT installed. Provide an IUserContext under ' +
136
+ 'AUTH_USER_CONTEXT in your AppModule to enable ambient tenant scoping.',
137
+ );
138
+ return;
139
+ }
140
+ app.use(makeRequesterContextMiddleware(userContext, options));
141
+ }
@@ -17,6 +17,23 @@
17
17
  * dependency on `express` / `fastify` / NestJS request types. The concrete
18
18
  * adapter narrows it (e.g. via a `Request` import).
19
19
  */
20
+ import type { RequesterContext } from '../../../base-classes/tenant-context';
21
+
20
22
  export interface IUserContext {
21
23
  getCurrentUserId(req: unknown): Promise<string>;
24
+ /**
25
+ * Optional richer resolution of the full ambient requester context — the
26
+ * org/superuser dimensions on top of `userId`. When implemented, the
27
+ * `RequesterContextMiddleware` (see `../middleware/requester-context`) uses
28
+ * it verbatim to scope reads/writes; when omitted, the middleware falls back
29
+ * to `{ userId: await getCurrentUserId(req), organizationId: null }` (plain
30
+ * `'user'` scope).
31
+ *
32
+ * Implement this when the app supports org-shared (`'org'`) or admin
33
+ * (`'superuser'`) data visibility — resolve `organizationId` + the
34
+ * `orgUserIds` member list here, at the trust boundary, so repositories stay
35
+ * single-table. AUTHORIZATION (which scope a requester may claim) is the
36
+ * implementation's responsibility; the repo trusts what this returns.
37
+ */
38
+ resolveRequester?(req: unknown): Promise<RequesterContext>;
22
39
  }