@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/CHANGELOG.md +41 -0
- package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +1 -0
- package/dist/runtime/subsystems/auth/index.d.ts +2 -0
- package/dist/runtime/subsystems/auth/index.js +55 -0
- package/dist/runtime/subsystems/auth/index.js.map +1 -1
- package/dist/runtime/subsystems/auth/middleware/requester-context.d.ts +81 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js +60 -0
- package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -0
- package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +18 -0
- package/dist/runtime/subsystems/index.d.ts +1 -0
- package/dist/runtime/subsystems/index.js +4 -0
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/src/cli/index.js +13 -1
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/auth/index.ts +8 -0
- package/runtime/subsystems/auth/middleware/requester-context.ts +141 -0
- package/runtime/subsystems/auth/protocols/user-context.ts +17 -0
package/package.json
CHANGED
|
@@ -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
|
}
|