@zintrust/core 0.1.46 → 0.1.49
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/README.md +1 -1
- package/app/Controllers/AuthController.d.ts.map +1 -1
- package/app/Controllers/AuthController.js +26 -4
- package/app/Middleware/index.js +5 -5
- package/app/Types/controller.d.ts +2 -0
- package/app/Types/controller.d.ts.map +1 -1
- package/app/Types/controller.js +1 -1
- package/config/storage.d.ts.map +1 -1
- package/config/storage.js +1 -1
- package/package.json +1 -1
- package/routes/api.js +13 -6
- package/src/cli/CLI.d.ts.map +1 -1
- package/src/cli/CLI.js +2 -0
- package/src/cli/commands/AddCommand.js +2 -2
- package/src/cli/commands/BulletproofKeyGenerateCommand.d.ts +10 -0
- package/src/cli/commands/BulletproofKeyGenerateCommand.d.ts.map +1 -0
- package/src/cli/commands/BulletproofKeyGenerateCommand.js +139 -0
- package/src/cli/commands/JwtDevCommand.d.ts.map +1 -1
- package/src/cli/commands/JwtDevCommand.js +51 -32
- package/src/cli/scaffolding/ControllerGenerator.d.ts +1 -1
- package/src/cli/scaffolding/ControllerGenerator.d.ts.map +1 -1
- package/src/cli/scaffolding/ControllerGenerator.js +8 -79
- package/src/config/SecretsManager.d.ts +0 -1
- package/src/config/SecretsManager.d.ts.map +1 -1
- package/src/config/SecretsManager.js +0 -1
- package/src/config/middleware.d.ts +1 -0
- package/src/config/middleware.d.ts.map +1 -1
- package/src/config/middleware.js +3 -0
- package/src/http/error-pages/ErrorPageRenderer.js +7 -1
- package/src/index.d.ts +1 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +4 -3
- package/src/middleware/BulletproofAuthMiddleware.d.ts +92 -0
- package/src/middleware/BulletproofAuthMiddleware.d.ts.map +1 -0
- package/src/middleware/BulletproofAuthMiddleware.js +421 -0
- package/src/middleware/CsrfMiddleware.d.ts +0 -1
- package/src/middleware/CsrfMiddleware.d.ts.map +1 -1
- package/src/middleware/CsrfMiddleware.js +8 -1
- package/src/middleware/JwtAuthMiddleware.d.ts.map +1 -1
- package/src/middleware/JwtAuthMiddleware.js +11 -5
- package/src/orm/Database.d.ts.map +1 -1
- package/src/orm/Database.js +48 -39
- package/src/orm/QueryBuilder.d.ts.map +1 -1
- package/src/orm/QueryBuilder.js +0 -2
- package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/MySQLProxyAdapter.js +54 -35
- package/src/orm/adapters/PostgreSQLProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/PostgreSQLProxyAdapter.js +126 -103
- package/src/orm/adapters/SqlProxyHttpAdapterShared.d.ts +30 -0
- package/src/orm/adapters/SqlProxyHttpAdapterShared.d.ts.map +1 -0
- package/src/orm/adapters/SqlProxyHttpAdapterShared.js +64 -0
- package/src/orm/adapters/SqlServerProxyAdapter.d.ts.map +1 -1
- package/src/orm/adapters/SqlServerProxyAdapter.js +54 -37
- package/src/orm/migrations/MigrationStore.d.ts.map +1 -1
- package/src/orm/migrations/MigrationStore.js +22 -1
- package/src/routes/doc.js +1 -1
- package/src/routes/errorPages.d.ts.map +1 -1
- package/src/routes/errorPages.js +9 -2
- package/src/security/CsrfTokenManager.d.ts.map +1 -1
- package/src/security/CsrfTokenManager.js +57 -23
- package/src/security/JwtManager.d.ts +4 -1
- package/src/security/JwtManager.d.ts.map +1 -1
- package/src/security/JwtManager.js +24 -10
- package/src/security/JwtSessions.d.ts +12 -0
- package/src/security/JwtSessions.d.ts.map +1 -0
- package/src/security/JwtSessions.js +556 -0
- package/src/security/NonceReplay.d.ts +24 -0
- package/src/security/NonceReplay.d.ts.map +1 -0
- package/src/security/NonceReplay.js +42 -0
- package/src/security/TokenRevocation.d.ts.map +1 -1
- package/src/security/TokenRevocation.js +1 -0
- package/src/tools/http/Http.d.ts +5 -0
- package/src/tools/http/Http.d.ts.map +1 -1
- package/src/tools/http/Http.js +25 -9
- package/src/tools/queue/QueueReliabilityOrchestrator.d.ts.map +1 -1
- package/src/tools/queue/QueueReliabilityOrchestrator.js +18 -6
- package/src/validation/Validator.d.ts.map +1 -1
- package/src/validation/Validator.js +4 -2
package/README.md
CHANGED
|
@@ -230,7 +230,7 @@ ZinTrust uses a proven layered architecture:
|
|
|
230
230
|
- 📚 [Documentation](https://zintrust.com)
|
|
231
231
|
- 💬 [Discord Community](https://discord.gg/zintrust)
|
|
232
232
|
- 🐦 [Follow on X](https://x.com/zintrust)
|
|
233
|
-
- 🐛 [Issue Tracker](https://github.com/ZinTrust
|
|
233
|
+
- 🐛 [Issue Tracker](https://github.com/ZinTrust/ZinTrust/issues)
|
|
234
234
|
- 🤝 [Contributing Guide](./contributing.md)
|
|
235
235
|
|
|
236
236
|
## License
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthController.d.ts","sourceRoot":"","sources":["../../../app/Controllers/AuthController.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"AuthController.d.ts","sourceRoot":"","sources":["../../../app/Controllers/AuthController.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,iBAAiB,EAAuB,MAAM,uBAAuB,CAAC;AAsOpF,eAAO,MAAM,cAAc;cACf,iBAAiB;EAS3B,CAAC;AAEH,eAAe,cAAc,CAAC"}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Minimal, real auth endpoints backing the example API routes.
|
|
4
4
|
*/
|
|
5
5
|
import { Auth } from '../../src/auth/Auth.js';
|
|
6
|
+
import { isUndefinedOrNull } from '../../src/helper/index.js';
|
|
6
7
|
import { User } from '../Models/User.js';
|
|
7
8
|
import { getString } from '../../src/common/utility.js';
|
|
8
9
|
import { Logger } from '../../src/config/logger.js';
|
|
9
10
|
import { getValidatedBody } from '../../src/http/ValidationHelper.js';
|
|
10
11
|
import { JwtManager } from '../../src/security/JwtManager.js';
|
|
11
|
-
import { TokenRevocation } from '../../src/security/TokenRevocation.js';
|
|
12
12
|
const pickPublicUser = (row) => {
|
|
13
13
|
return {
|
|
14
14
|
id: row.id,
|
|
@@ -66,9 +66,14 @@ async function login(req, res) {
|
|
|
66
66
|
return String(id);
|
|
67
67
|
return undefined;
|
|
68
68
|
})();
|
|
69
|
-
|
|
69
|
+
// Bulletproof Auth (device binding) expects a device id header to match a JWT claim.
|
|
70
|
+
// For the example app, we mint a stable device id derived from the subject.
|
|
71
|
+
// Production apps should issue a per-device id and manage a per-device signing secret.
|
|
72
|
+
const deviceId = isUndefinedOrNull(subject) ? undefined : `dev-${subject}`;
|
|
73
|
+
const token = await JwtManager.signAccessToken({
|
|
70
74
|
sub: subject,
|
|
71
75
|
email,
|
|
76
|
+
...(isUndefinedOrNull(deviceId) ? {} : { deviceId }),
|
|
72
77
|
});
|
|
73
78
|
Logger.info('AuthController.login: successful login', {
|
|
74
79
|
userId: subject,
|
|
@@ -79,6 +84,7 @@ async function login(req, res) {
|
|
|
79
84
|
res.json({
|
|
80
85
|
token,
|
|
81
86
|
token_type: 'Bearer',
|
|
87
|
+
...(isUndefinedOrNull(deviceId) ? {} : { deviceId }),
|
|
82
88
|
user,
|
|
83
89
|
});
|
|
84
90
|
}
|
|
@@ -168,9 +174,24 @@ async function register(req, res) {
|
|
|
168
174
|
*/
|
|
169
175
|
async function logout(req, res) {
|
|
170
176
|
const authHeader = typeof req.getHeader === 'function' ? req.getHeader('authorization') : undefined;
|
|
171
|
-
await
|
|
177
|
+
await JwtManager.logout(authHeader);
|
|
172
178
|
res.json({ message: 'Logged out' });
|
|
173
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Logs out the current user from all devices by removing all active sessions for their subject.
|
|
182
|
+
*
|
|
183
|
+
* With session allowlist enforcement, deleting a user's session records causes any previously issued
|
|
184
|
+
* tokens to become unauthorized (401) immediately.
|
|
185
|
+
*/
|
|
186
|
+
async function logoutAll(req, res) {
|
|
187
|
+
const sub = typeof req.user?.sub === 'string' ? req.user.sub.trim() : '';
|
|
188
|
+
if (sub === '') {
|
|
189
|
+
res.setStatus(401).json({ error: 'Unauthorized' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await JwtManager.logoutAll(sub);
|
|
193
|
+
res.json({ message: 'Logged out everywhere' });
|
|
194
|
+
}
|
|
174
195
|
/**
|
|
175
196
|
* Refreshes the user's JWT access token.
|
|
176
197
|
* Generates a new token with the same claims as the current user.
|
|
@@ -185,7 +206,7 @@ async function refresh(req, res) {
|
|
|
185
206
|
res.setStatus(401).json({ error: 'Unauthorized' });
|
|
186
207
|
return;
|
|
187
208
|
}
|
|
188
|
-
const token = JwtManager.signAccessToken(user);
|
|
209
|
+
const token = await JwtManager.signAccessToken(user);
|
|
189
210
|
res.json({ token, token_type: 'Bearer' });
|
|
190
211
|
}
|
|
191
212
|
export const AuthController = Object.freeze({
|
|
@@ -194,6 +215,7 @@ export const AuthController = Object.freeze({
|
|
|
194
215
|
login,
|
|
195
216
|
register,
|
|
196
217
|
logout,
|
|
218
|
+
logoutAll,
|
|
197
219
|
refresh,
|
|
198
220
|
};
|
|
199
221
|
},
|
package/app/Middleware/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Common middleware patterns for ZinTrust
|
|
4
4
|
*/
|
|
5
5
|
import { Logger } from '../../src/config/logger.js';
|
|
6
|
-
import {
|
|
6
|
+
import { JwtSessions } from '../../src/security/JwtSessions.js';
|
|
7
7
|
import { XssProtection } from '../../src/security/XssProtection.js';
|
|
8
8
|
import { Validator } from '../../src/validation/Validator.js';
|
|
9
9
|
const resolveJwtManager = (jwtManager) => 'verify' in jwtManager ? jwtManager : jwtManager.create();
|
|
@@ -118,11 +118,11 @@ export const jwtMiddleware = (jwtManager, algorithm = 'HS256') => {
|
|
|
118
118
|
res.setStatus(401).json({ error: 'Invalid authorization header format' });
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
|
-
if (await TokenRevocation.isRevoked(token)) {
|
|
122
|
-
res.setStatus(401).json({ error: 'Invalid or expired token' });
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
121
|
try {
|
|
122
|
+
if (!(await JwtSessions.isActive(token))) {
|
|
123
|
+
res.setStatus(401).json({ error: 'Invalid or expired token' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
126
|
const payload = resolveJwtManager(jwtManager).verify(token, algorithm);
|
|
127
127
|
// Store in request context (TypeScript allows dynamic properties)
|
|
128
128
|
req.user = payload;
|
|
@@ -20,6 +20,7 @@ export type AuthControllerApi = {
|
|
|
20
20
|
login(req: IRequest, res: IResponse): Promise<void>;
|
|
21
21
|
register(req: IRequest, res: IResponse): Promise<void>;
|
|
22
22
|
logout(req: IRequest, res: IResponse): Promise<void>;
|
|
23
|
+
logoutAll(req: IRequest, res: IResponse): Promise<void>;
|
|
23
24
|
refresh(req: IRequest, res: IResponse): Promise<void>;
|
|
24
25
|
};
|
|
25
26
|
export type ValidationErrorLike = {
|
|
@@ -39,4 +40,5 @@ export interface IUserController {
|
|
|
39
40
|
update(req: IRequest, res: IResponse): Promise<void>;
|
|
40
41
|
destroy(req: IRequest, res: IResponse): Promise<void>;
|
|
41
42
|
}
|
|
43
|
+
export declare const __controllerTypesRuntime = 1;
|
|
42
44
|
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../app/Types/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AACF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC3C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD"}
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../app/Types/controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AACF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,SAAS,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC3C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvD;AAED,eAAO,MAAM,wBAAwB,IAAI,CAAC"}
|
package/app/Types/controller.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const __controllerTypesRuntime = 1;
|
package/config/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../config/storage.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../config/storage.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,wBAiDmC"}
|
package/config/storage.js
CHANGED
package/package.json
CHANGED
package/routes/api.js
CHANGED
|
@@ -69,6 +69,9 @@ function registerApiV1Routes(router, authController, userController) {
|
|
|
69
69
|
Router.post(r, '/auth/logout', authController.logout, {
|
|
70
70
|
middleware: ['auth', 'jwt'],
|
|
71
71
|
});
|
|
72
|
+
Router.post(r, '/auth/logout-all', authController.logoutAll, {
|
|
73
|
+
middleware: ['auth', 'jwt'],
|
|
74
|
+
});
|
|
72
75
|
Router.post(r, '/auth/refresh', authController.refresh, {
|
|
73
76
|
middleware: ['auth', 'jwt'],
|
|
74
77
|
});
|
|
@@ -82,10 +85,14 @@ function registerApiV1Routes(router, authController, userController) {
|
|
|
82
85
|
update: userController.update,
|
|
83
86
|
destroy: userController.destroy,
|
|
84
87
|
}, {
|
|
85
|
-
middleware: ['auth', '
|
|
86
|
-
store: {
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
middleware: ['auth', 'bulletproof'],
|
|
89
|
+
store: {
|
|
90
|
+
middleware: ['auth', 'bulletproof', 'userMutationRateLimit', 'validateUserStore'],
|
|
91
|
+
},
|
|
92
|
+
update: {
|
|
93
|
+
middleware: ['auth', 'bulletproof', 'userMutationRateLimit', 'validateUserUpdate'],
|
|
94
|
+
},
|
|
95
|
+
destroy: { middleware: ['auth', 'bulletproof', 'userMutationRateLimit'] },
|
|
89
96
|
});
|
|
90
97
|
Router.post(pr, '/users/fill', userController.fill, {
|
|
91
98
|
middleware: ['auth', 'jwt', 'fillRateLimit', 'validateUserFill'],
|
|
@@ -100,10 +107,10 @@ function registerApiV1Routes(router, authController, userController) {
|
|
|
100
107
|
// Custom user routes
|
|
101
108
|
Router.get(pr, '/profile', async (__req, res) => {
|
|
102
109
|
res.json({ message: 'Get user profile' });
|
|
103
|
-
}, { middleware: ['auth', '
|
|
110
|
+
}, { middleware: ['auth', 'bulletproof'] });
|
|
104
111
|
Router.put(pr, '/profile', async (__req, res) => {
|
|
105
112
|
res.json({ message: 'Update user profile' });
|
|
106
|
-
}, { middleware: ['auth', '
|
|
113
|
+
}, { middleware: ['auth', 'bulletproof'] });
|
|
107
114
|
// Posts resource
|
|
108
115
|
Router.get(r, '/posts', async (_req, res) => {
|
|
109
116
|
res.json({ data: [] });
|
package/src/cli/CLI.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CLI.d.ts","sourceRoot":"","sources":["../../../src/cli/CLI.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"CLI.d.ts","sourceRoot":"","sources":["../../../src/cli/CLI.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,MAAM,WAAW,IAAI;IACnB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,UAAU,IAAI,OAAO,CAAC;CACvB;AA4PD;;;;;;;GAOG;AACH,eAAO,MAAM,GAAG;cACJ,IAAI;EAed,CAAC"}
|
package/src/cli/CLI.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { AddCommand } from './commands/AddCommand.js';
|
|
6
6
|
import { BroadcastWorkCommand } from './commands/BroadcastWorkCommand.js';
|
|
7
|
+
import { BulletproofKeyGenerateCommand } from './commands/BulletproofKeyGenerateCommand.js';
|
|
7
8
|
import { ConfigCommand } from './commands/ConfigCommand.js';
|
|
8
9
|
import { ContainerProxiesCommand } from './commands/ContainerProxiesCommand.js';
|
|
9
10
|
import { ContainerWorkersCommand } from './commands/ContainerWorkersCommand.js';
|
|
@@ -119,6 +120,7 @@ const buildCommandRegistry = () => {
|
|
|
119
120
|
QACommand(),
|
|
120
121
|
FixCommand.create(),
|
|
121
122
|
KeyGenerateCommand.create(),
|
|
123
|
+
BulletproofKeyGenerateCommand.create(),
|
|
122
124
|
SimulateCommand,
|
|
123
125
|
TemplatesCommand,
|
|
124
126
|
MakeMailTemplateCommand.create(),
|
|
@@ -33,7 +33,7 @@ const addOptions = (command) => {
|
|
|
33
33
|
.option('--port <number>', 'Service port - for services')
|
|
34
34
|
.option('--service <path>', 'Service path (relative to project root) - for features')
|
|
35
35
|
.option('--with-test', 'Generate test files - for features')
|
|
36
|
-
.option('--controller-type <type>', 'Controller type: crud, resource, api, graphql, websocket
|
|
36
|
+
.option('--controller-type <type>', 'Controller type: crud, resource, api, graphql, websocket - for controllers')
|
|
37
37
|
.option('--soft-delete', 'Add soft delete to model')
|
|
38
38
|
.option('--timestamps', 'Add timestamps to model (default: true)')
|
|
39
39
|
.option('--resource', 'Generate resource routes')
|
|
@@ -304,7 +304,7 @@ const promptControllerConfig = async () => {
|
|
|
304
304
|
type: 'list',
|
|
305
305
|
name: 'type',
|
|
306
306
|
message: 'Controller type:',
|
|
307
|
-
choices: ['crud', 'resource', 'api', 'graphql', 'websocket'
|
|
307
|
+
choices: ['crud', 'resource', 'api', 'graphql', 'websocket'],
|
|
308
308
|
default: 'crud',
|
|
309
309
|
},
|
|
310
310
|
]);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulletproof Signing Secret Generate Command
|
|
3
|
+
* Generates and sets BULLETPROOF_SIGNING_SECRET (with rotation backups)
|
|
4
|
+
*/
|
|
5
|
+
import type { IBaseCommand } from '../BaseCommand';
|
|
6
|
+
export declare const BulletproofKeyGenerateCommand: Readonly<{
|
|
7
|
+
create(): IBaseCommand;
|
|
8
|
+
}>;
|
|
9
|
+
export default BulletproofKeyGenerateCommand;
|
|
10
|
+
//# sourceMappingURL=BulletproofKeyGenerateCommand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BulletproofKeyGenerateCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/BulletproofKeyGenerateCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgBrE,eAAO,MAAM,6BAA6B;cAC9B,YAAY;EAkEtB,CAAC;AAoFH,eAAe,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulletproof Signing Secret Generate Command
|
|
3
|
+
* Generates and sets BULLETPROOF_SIGNING_SECRET (with rotation backups)
|
|
4
|
+
*/
|
|
5
|
+
import { BaseCommand } from '../BaseCommand.js';
|
|
6
|
+
import { Logger } from '../../config/logger.js';
|
|
7
|
+
import * as crypto from '../../node-singletons/crypto.js';
|
|
8
|
+
import { fsPromises as fs } from '../../node-singletons/fs.js';
|
|
9
|
+
import * as path from '../../node-singletons/path.js';
|
|
10
|
+
const ENV_KEY = 'BULLETPROOF_SIGNING_SECRET';
|
|
11
|
+
const ENV_BK_KEY = 'BULLETPROOF_SIGNING_SECRET_BK';
|
|
12
|
+
export const BulletproofKeyGenerateCommand = Object.freeze({
|
|
13
|
+
create() {
|
|
14
|
+
return BaseCommand.create({
|
|
15
|
+
name: 'key:bulletproof',
|
|
16
|
+
description: 'Generate/rotate BULLETPROOF_SIGNING_SECRET (signed-request proof key)',
|
|
17
|
+
aliases: ['bulletproof:key', 'key:signer'],
|
|
18
|
+
addOptions: (command) => {
|
|
19
|
+
command.option('--show', 'Display the key (and suggested env) instead of modifying files');
|
|
20
|
+
command.option('--max-backups <n>', 'Max secrets to keep in BULLETPROOF_SIGNING_SECRET_BK (default: 5)', '5');
|
|
21
|
+
},
|
|
22
|
+
execute: async (options) => {
|
|
23
|
+
const key = generateRandomKey();
|
|
24
|
+
const maxBackups = parseMaxBackups(options.maxBackups);
|
|
25
|
+
if (options.show === true) {
|
|
26
|
+
Logger.info(`${ENV_KEY}=${key}`);
|
|
27
|
+
Logger.info(`${ENV_BK_KEY}=[]`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const envPath = path.resolve(process.cwd(), '.env');
|
|
31
|
+
try {
|
|
32
|
+
let envContent = '';
|
|
33
|
+
try {
|
|
34
|
+
envContent = await fs.readFile(envPath, 'utf-8');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
Logger.warn('Could not read .env file, attempting to create from example', { error });
|
|
38
|
+
const examplePath = path.resolve(process.cwd(), '.env.example');
|
|
39
|
+
try {
|
|
40
|
+
envContent = await fs.readFile(examplePath, 'utf-8');
|
|
41
|
+
await fs.writeFile(envPath, envContent);
|
|
42
|
+
Logger.info('.env file created from .env.example');
|
|
43
|
+
}
|
|
44
|
+
catch (copyError) {
|
|
45
|
+
Logger.error('Failed to create .env from example', { error: copyError });
|
|
46
|
+
Logger.warn('.env file not found and .env.example not found. Creating new .env file.');
|
|
47
|
+
envContent = '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const currentSecret = readEnvLineValue(envContent, ENV_KEY);
|
|
51
|
+
const currentBackups = parseBackups(readEnvLineValue(envContent, ENV_BK_KEY));
|
|
52
|
+
const nextBackups = rotateBackups({
|
|
53
|
+
currentSecret,
|
|
54
|
+
currentBackups,
|
|
55
|
+
maxBackups,
|
|
56
|
+
});
|
|
57
|
+
envContent = upsertEnvLine(envContent, ENV_BK_KEY, JSON.stringify(nextBackups));
|
|
58
|
+
envContent = upsertEnvLine(envContent, ENV_KEY, key);
|
|
59
|
+
await fs.writeFile(envPath, envContent);
|
|
60
|
+
Logger.info(`Bulletproof signing secret set successfully. [${key}]`);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
Logger.error('Failed to update .env file', error);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const parseMaxBackups = (raw) => {
|
|
70
|
+
const n = typeof raw === 'string' ? Number.parseInt(raw, 10) : Number.NaN;
|
|
71
|
+
if (!Number.isFinite(n) || n < 0)
|
|
72
|
+
return 5;
|
|
73
|
+
return Math.min(50, n);
|
|
74
|
+
};
|
|
75
|
+
const generateRandomKey = () => {
|
|
76
|
+
// 32 bytes = 256-bit (same as APP_KEY default strength).
|
|
77
|
+
return 'base64:' + crypto.randomBytes(32).toString('base64');
|
|
78
|
+
};
|
|
79
|
+
const readEnvLineValue = (envContent, key) => {
|
|
80
|
+
const re = new RegExp(`^${escapeRegExp(key)}=(.*)$`, 'm');
|
|
81
|
+
const match = re.exec(envContent);
|
|
82
|
+
return typeof match?.[1] === 'string' ? match[1].trim() : '';
|
|
83
|
+
};
|
|
84
|
+
const upsertEnvLine = (envContent, key, value) => {
|
|
85
|
+
const line = `${key}=${value}`;
|
|
86
|
+
const re = new RegExp(`^${escapeRegExp(key)}=.*$`, 'm');
|
|
87
|
+
if (re.test(envContent)) {
|
|
88
|
+
return envContent.replace(re, line);
|
|
89
|
+
}
|
|
90
|
+
const trimmed = envContent.trimEnd();
|
|
91
|
+
if (trimmed === '')
|
|
92
|
+
return `${line}\n`;
|
|
93
|
+
return `${trimmed}\n${line}\n`;
|
|
94
|
+
};
|
|
95
|
+
const escapeRegExp = (value) => value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
96
|
+
const parseBackups = (raw) => {
|
|
97
|
+
const value = raw.trim();
|
|
98
|
+
if (value === '')
|
|
99
|
+
return [];
|
|
100
|
+
if (value.startsWith('[')) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(value);
|
|
103
|
+
if (!Array.isArray(parsed))
|
|
104
|
+
return [];
|
|
105
|
+
return parsed
|
|
106
|
+
.filter((v) => typeof v === 'string')
|
|
107
|
+
.map((s) => s.trim())
|
|
108
|
+
.filter((s) => s !== '');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return value
|
|
115
|
+
.split(',')
|
|
116
|
+
.map((s) => s.trim())
|
|
117
|
+
.filter((s) => s !== '');
|
|
118
|
+
};
|
|
119
|
+
const rotateBackups = (params) => {
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
const out = [];
|
|
122
|
+
const pushUnique = (secret) => {
|
|
123
|
+
const s = secret.trim();
|
|
124
|
+
if (s === '')
|
|
125
|
+
return;
|
|
126
|
+
if (seen.has(s))
|
|
127
|
+
return;
|
|
128
|
+
seen.add(s);
|
|
129
|
+
out.push(s);
|
|
130
|
+
};
|
|
131
|
+
if (params.currentSecret !== '') {
|
|
132
|
+
pushUnique(params.currentSecret);
|
|
133
|
+
}
|
|
134
|
+
for (const s of params.currentBackups) {
|
|
135
|
+
pushUnique(s);
|
|
136
|
+
}
|
|
137
|
+
return out.slice(0, Math.max(0, params.maxBackups));
|
|
138
|
+
};
|
|
139
|
+
export default BulletproofKeyGenerateCommand;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JwtDevCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/JwtDevCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"JwtDevCommand.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/JwtDevCommand.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAyHvF,eAAO,MAAM,aAAa,EAAE,YAgD3B,CAAC;AAEF,eAAe,aAAa,CAAC"}
|
|
@@ -2,11 +2,54 @@
|
|
|
2
2
|
* JWT Dev Command
|
|
3
3
|
* Mint a local development JWT for quick manual API testing.
|
|
4
4
|
*/
|
|
5
|
+
import { isUndefinedOrNull } from '../../helper/index.js';
|
|
5
6
|
import { BaseCommand } from '../BaseCommand.js';
|
|
6
7
|
import { appConfig } from '../../config/app.js';
|
|
7
8
|
import { securityConfig } from '../../config/security.js';
|
|
8
9
|
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
|
|
10
|
+
import * as crypto from '../../node-singletons/crypto.js';
|
|
9
11
|
import { JwtManager } from '../../security/JwtManager.js';
|
|
12
|
+
const sha256Hex = (value) => {
|
|
13
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
14
|
+
};
|
|
15
|
+
const optionalTrimmed = (value) => {
|
|
16
|
+
if (typeof value !== 'string')
|
|
17
|
+
return undefined;
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed === '' ? undefined : trimmed;
|
|
20
|
+
};
|
|
21
|
+
const buildPayload = (options) => {
|
|
22
|
+
const payload = {};
|
|
23
|
+
const sub = optionalTrimmed(options.sub);
|
|
24
|
+
if (!isUndefinedOrNull(sub))
|
|
25
|
+
payload.sub = sub;
|
|
26
|
+
const email = optionalTrimmed(options.email);
|
|
27
|
+
if (!isUndefinedOrNull(email))
|
|
28
|
+
payload['email'] = email;
|
|
29
|
+
const role = optionalTrimmed(options.role);
|
|
30
|
+
if (!isUndefinedOrNull(role))
|
|
31
|
+
payload['role'] = role;
|
|
32
|
+
const deviceId = optionalTrimmed(options.deviceId);
|
|
33
|
+
if (!isUndefinedOrNull(deviceId))
|
|
34
|
+
payload['deviceId'] = deviceId;
|
|
35
|
+
const tenantId = optionalTrimmed(options.tenantId);
|
|
36
|
+
if (!isUndefinedOrNull(tenantId))
|
|
37
|
+
payload['tenantId'] = tenantId;
|
|
38
|
+
const tz = optionalTrimmed(options.tz);
|
|
39
|
+
if (!isUndefinedOrNull(tz))
|
|
40
|
+
payload['tz'] = tz;
|
|
41
|
+
const uaHash = optionalTrimmed(options.uaHash);
|
|
42
|
+
if (isUndefinedOrNull(uaHash)) {
|
|
43
|
+
const ua = optionalTrimmed(options.ua);
|
|
44
|
+
if (ua !== undefined) {
|
|
45
|
+
payload['uaHash'] = sha256Hex(ua);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
payload['uaHash'] = uaHash;
|
|
50
|
+
}
|
|
51
|
+
return payload;
|
|
52
|
+
};
|
|
10
53
|
const parseExpiresToSeconds = (value) => {
|
|
11
54
|
const raw = typeof value === 'string' ? value.trim() : '';
|
|
12
55
|
if (raw === '')
|
|
@@ -47,25 +90,6 @@ const assertNotProduction = (allowProduction) => {
|
|
|
47
90
|
return;
|
|
48
91
|
throw ErrorFactory.createCliError("Refusing to mint a dev JWT in production. Use --allow-production only if you know what you're doing.");
|
|
49
92
|
};
|
|
50
|
-
const createJwt = (payload, expiresInSeconds) => {
|
|
51
|
-
const algorithm = securityConfig.jwt.algorithm;
|
|
52
|
-
const secret = securityConfig.jwt.secret;
|
|
53
|
-
const jwt = JwtManager.create();
|
|
54
|
-
if (algorithm === 'HS256' || algorithm === 'HS512') {
|
|
55
|
-
jwt.setHmacSecret(secret);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
throw ErrorFactory.createCliError(`JWT algorithm '${algorithm}' is not supported by zin jwt:dev (HS256/HS512 only).`);
|
|
59
|
-
}
|
|
60
|
-
return jwt.sign(payload, {
|
|
61
|
-
algorithm,
|
|
62
|
-
expiresIn: expiresInSeconds,
|
|
63
|
-
issuer: securityConfig.jwt.issuer,
|
|
64
|
-
audience: securityConfig.jwt.audience,
|
|
65
|
-
subject: typeof payload.sub === 'string' ? payload.sub : undefined,
|
|
66
|
-
jwtId: jwt.generateJwtId(),
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
93
|
export const JwtDevCommand = Object.freeze(BaseCommand.create({
|
|
70
94
|
name: 'jwt:dev',
|
|
71
95
|
description: 'Mint a local development JWT (for manual API testing)',
|
|
@@ -75,25 +99,20 @@ export const JwtDevCommand = Object.freeze(BaseCommand.create({
|
|
|
75
99
|
.option('--sub <sub>', 'JWT subject claim (default: 1)', '1')
|
|
76
100
|
.option('--email <email>', 'Email claim')
|
|
77
101
|
.option('--role <role>', 'Role claim')
|
|
102
|
+
.option('--device-id <id>', 'Attach deviceId claim (for bulletproof auth)')
|
|
103
|
+
.option('--tenant-id <id>', 'Attach tenantId claim')
|
|
104
|
+
.option('--tz <tz>', 'Attach timezone claim (tz)')
|
|
105
|
+
.option('--ua <ua>', 'Compute and attach uaHash claim from a User-Agent string')
|
|
106
|
+
.option('--ua-hash <hash>', 'Attach uaHash claim directly (hex)')
|
|
78
107
|
.option('--expires <duration>', "Expiry: seconds or 30m/1h/7d (default: '1h')", '1h')
|
|
79
108
|
.option('--json', 'Output machine-readable JSON')
|
|
80
109
|
.option('--allow-production', 'Allow running in production (dangerous)');
|
|
81
110
|
},
|
|
82
|
-
execute: (options) => {
|
|
111
|
+
execute: async (options) => {
|
|
83
112
|
assertNotProduction(options.allowProduction);
|
|
84
113
|
const expiresInSeconds = parseExpiresToSeconds(options.expires);
|
|
85
|
-
const payload =
|
|
86
|
-
|
|
87
|
-
? { sub: options.sub.trim() }
|
|
88
|
-
: {}),
|
|
89
|
-
...(typeof options.email === 'string' && options.email.trim() !== ''
|
|
90
|
-
? { email: options.email.trim() }
|
|
91
|
-
: {}),
|
|
92
|
-
...(typeof options.role === 'string' && options.role.trim() !== ''
|
|
93
|
-
? { role: options.role.trim() }
|
|
94
|
-
: {}),
|
|
95
|
-
};
|
|
96
|
-
const token = createJwt(payload, expiresInSeconds);
|
|
114
|
+
const payload = buildPayload(options);
|
|
115
|
+
const token = await JwtManager.signAccessToken(payload, expiresInSeconds);
|
|
97
116
|
/* eslint-disable no-console */
|
|
98
117
|
if (options.json === true) {
|
|
99
118
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ControllerGenerator - Generate controller files
|
|
3
3
|
* Creates CRUD controllers with validation and error handling
|
|
4
4
|
*/
|
|
5
|
-
export type ControllerType = 'crud' | 'resource' | 'api' | 'graphql' | 'websocket'
|
|
5
|
+
export type ControllerType = 'crud' | 'resource' | 'api' | 'graphql' | 'websocket';
|
|
6
6
|
export interface ControllerOptions {
|
|
7
7
|
name: string;
|
|
8
8
|
controllerPath: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ControllerGenerator.d.ts","sourceRoot":"","sources":["../../../../src/cli/scaffolding/ControllerGenerator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,WAAW,
|
|
1
|
+
{"version":3,"file":"ControllerGenerator.d.ts","sourceRoot":"","sources":["../../../../src/cli/scaffolding/ControllerGenerator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,WAAW,CAAC;AAEnF,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;CACjB;AAaD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAgChG;AAED;;GAEG;AAEH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CA2CjG;AAsaD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,EAAE,CAEpD;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;EAI9B,CAAC"}
|