@bi-analytics/arness-back 0.0.0-stage → 0.3.0
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 +85 -0
- package/README.md +156 -2
- package/bin/arness.mjs +710 -0
- package/harness/AGENTS.md +238 -0
- package/harness/agents/implementador.md +42 -0
- package/harness/agents/lider.md +72 -0
- package/harness/agents/revisor.md +62 -0
- package/harness/decisiones/README.md +17 -0
- package/harness/decisiones/_plantilla.md +24 -0
- package/harness/init.sh +743 -0
- package/harness/playbooks/add-database.md +16 -0
- package/harness/playbooks/new-endpoint.md +88 -0
- package/harness/playbooks/new-module.md +21 -0
- package/harness/playbooks/new-monitor-widget.md +31 -0
- package/harness/playbooks/review.md +41 -0
- package/harness/playbooks/update-node-lts.md +46 -0
- package/harness/reference/configuracion.md +111 -0
- package/harness/reference/contrato-http.md +169 -0
- package/harness/reference/novedades.json +158 -0
- package/harness/reference/progress-plantilla.md +50 -0
- package/harness/skills/commit-hygiene/SKILL.md +137 -0
- package/harness/skills/nest-base/SKILL.md +168 -0
- package/harness/skills/nest-base/templates/config/auth.spec.ts +32 -0
- package/harness/skills/nest-base/templates/config/auth.ts +80 -0
- package/harness/skills/nest-base/templates/config/configuration.spec.ts +85 -0
- package/harness/skills/nest-base/templates/config/configuration.ts +137 -0
- package/harness/skills/nest-base/templates/cors/cors.spec.ts +53 -0
- package/harness/skills/nest-base/templates/cors/cors.ts +75 -0
- package/harness/skills/nest-base/templates/docs/EVOLUTIVO.md +27 -0
- package/harness/skills/nest-base/templates/docs/PENDIENTES.md +20 -0
- package/harness/skills/nest-base/templates/docs/TECNICO.md +43 -0
- package/harness/skills/nest-base/templates/e2e/cors.e2e-spec.ts +60 -0
- package/harness/skills/nest-base/templates/e2e/monitor.e2e-spec.ts +151 -0
- package/harness/skills/nest-base/templates/editorconfig +12 -0
- package/harness/skills/nest-base/templates/eslint.boundaries.mjs +65 -0
- package/harness/skills/nest-base/templates/eslint.config.mjs +82 -0
- package/harness/skills/nest-base/templates/logging/logger-options.spec.ts +57 -0
- package/harness/skills/nest-base/templates/logging/logger-options.ts +85 -0
- package/harness/skills/nest-base/templates/monitor/monitor-login-redirect.filter.ts +16 -0
- package/harness/skills/nest-base/templates/monitor/monitor-login.dto.ts +8 -0
- package/harness/skills/nest-base/templates/monitor/monitor-page.spec.ts +41 -0
- package/harness/skills/nest-base/templates/monitor/monitor-page.ts +190 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.guard.spec.ts +46 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.guard.ts +36 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.service.spec.ts +57 -0
- package/harness/skills/nest-base/templates/monitor/monitor-session.service.ts +77 -0
- package/harness/skills/nest-base/templates/monitor/monitor-widget.interface.ts +18 -0
- package/harness/skills/nest-base/templates/monitor/monitor.constants.ts +25 -0
- package/harness/skills/nest-base/templates/monitor/monitor.controller.ts +84 -0
- package/harness/skills/nest-base/templates/monitor/monitor.decorators.ts +11 -0
- package/harness/skills/nest-base/templates/monitor/monitor.module.ts +30 -0
- package/harness/skills/nest-base/templates/monitor/monitor.service.spec.ts +55 -0
- package/harness/skills/nest-base/templates/monitor/monitor.service.ts +49 -0
- package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.spec.ts +51 -0
- package/harness/skills/nest-base/templates/monitor/widgets/endpoints-total.widget.ts +53 -0
- package/harness/skills/nest-base/templates/prettierignore +6 -0
- package/harness/skills/nest-base/templates/prettierrc.json +8 -0
- package/harness/skills/nest-base/templates/security/login-attempts.spec.ts +48 -0
- package/harness/skills/nest-base/templates/security/login-attempts.ts +90 -0
- package/harness/skills/nest-base/templates/security/security-headers.spec.ts +27 -0
- package/harness/skills/nest-base/templates/security/security-headers.ts +21 -0
- package/harness/skills/nest-base/templates/security/security.module.ts +20 -0
- package/harness/skills/nest-base/templates/security/session-lifetime.spec.ts +21 -0
- package/harness/skills/nest-base/templates/security/session-lifetime.ts +28 -0
- package/package.json +44 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Plantilla del harness → src/common/security/security-headers.spec.ts
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import { API_SECURITY_HEADERS, securityHeaders } from './security-headers';
|
|
5
|
+
|
|
6
|
+
describe('securityHeaders', () => {
|
|
7
|
+
const app = express();
|
|
8
|
+
app.use(securityHeaders());
|
|
9
|
+
app.get('/api/v1/ping', (_req, res) => {
|
|
10
|
+
res.json({ data: 'pong' });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('sends every security header and hides X-Powered-By', async () => {
|
|
14
|
+
const res = await request(app).get('/api/v1/ping');
|
|
15
|
+
expect(res.status).toBe(200);
|
|
16
|
+
for (const [name, value] of Object.entries(API_SECURITY_HEADERS)) {
|
|
17
|
+
expect(res.headers[name.toLowerCase()]).toBe(value);
|
|
18
|
+
}
|
|
19
|
+
expect(res.headers['x-powered-by']).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('also covers responses that are not handled by a route', async () => {
|
|
23
|
+
const res = await request(app).get('/missing');
|
|
24
|
+
expect(res.status).toBe(404);
|
|
25
|
+
expect(res.headers['x-frame-options']).toBe('DENY');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Plantilla del harness → src/common/security/security-headers.ts (AGENTS.md §9, contrato-http.md "Cabeceras de seguridad").
|
|
2
|
+
// Security headers for every response, without helmet. The monitor overwrites the CSP with its own.
|
|
3
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
4
|
+
|
|
5
|
+
export const API_SECURITY_HEADERS: Readonly<Record<string, string>> = Object.freeze({
|
|
6
|
+
'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none'; base-uri 'none'",
|
|
7
|
+
'X-Content-Type-Options': 'nosniff',
|
|
8
|
+
'X-Frame-Options': 'DENY',
|
|
9
|
+
'Referrer-Policy': 'no-referrer',
|
|
10
|
+
'Strict-Transport-Security': 'max-age=31536000',
|
|
11
|
+
'Cache-Control': 'no-store',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** Express middleware: app.use(securityHeaders()) in src/main.ts, before any route. */
|
|
15
|
+
export function securityHeaders(): (req: Request, res: Response, next: NextFunction) => void {
|
|
16
|
+
return (_req, res, next) => {
|
|
17
|
+
res.removeHeader('X-Powered-By');
|
|
18
|
+
res.set(API_SECURITY_HEADERS);
|
|
19
|
+
next();
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Plantilla del harness → src/common/security/security.module.ts
|
|
2
|
+
// Global: AuthController and MonitorController inject the same LoginAttemptLimiter.
|
|
3
|
+
import { Global, Module } from '@nestjs/common';
|
|
4
|
+
import { ConfigService } from '@nestjs/config';
|
|
5
|
+
import type { AppConfig } from '../../config/configuration';
|
|
6
|
+
import { LoginAttemptLimiter } from './login-attempts';
|
|
7
|
+
|
|
8
|
+
@Global()
|
|
9
|
+
@Module({
|
|
10
|
+
providers: [
|
|
11
|
+
{
|
|
12
|
+
provide: LoginAttemptLimiter,
|
|
13
|
+
useFactory: (config: ConfigService<AppConfig, true>): LoginAttemptLimiter =>
|
|
14
|
+
new LoginAttemptLimiter(config.get('auth.login', { infer: true })),
|
|
15
|
+
inject: [ConfigService],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
exports: [LoginAttemptLimiter],
|
|
19
|
+
})
|
|
20
|
+
export class SecurityModule {}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Plantilla del harness → src/common/security/session-lifetime.spec.ts
|
|
2
|
+
import { AUTH_TIME_CLAIM, isSessionExpired, nowInSeconds, sessionClaims } from './session-lifetime';
|
|
3
|
+
|
|
4
|
+
describe('session lifetime', () => {
|
|
5
|
+
const maxAge = 3600;
|
|
6
|
+
|
|
7
|
+
it('keeps the session alive until it reaches the max age', () => {
|
|
8
|
+
const claims = sessionClaims(1000);
|
|
9
|
+
expect(claims).toEqual({ [AUTH_TIME_CLAIM]: 1000 });
|
|
10
|
+
expect(isSessionExpired(claims, maxAge, 1000 + maxAge - 1)).toBe(false);
|
|
11
|
+
expect(isSessionExpired(claims, maxAge, 1000 + maxAge)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('treats tokens without auth_time as expired', () => {
|
|
15
|
+
expect(isSessionExpired({}, maxAge, 1000)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('converts milliseconds to JWT seconds', () => {
|
|
19
|
+
expect(nowInSeconds(1_700_000_000_999)).toBe(1_700_000_000);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Plantilla del harness → src/common/security/session-lifetime.ts (AGENTS.md §8, contrato-http.md "Autenticación").
|
|
2
|
+
// A session starts at /auth/login and lasts at most AUTH_SESSION_MAX_AGE_SECONDS, however many
|
|
3
|
+
// times it is refreshed: a stolen refresh token stops working when the session reaches that age.
|
|
4
|
+
|
|
5
|
+
/** Claim with the login time (seconds since epoch). The refresh copies it into the new pair. */
|
|
6
|
+
export const AUTH_TIME_CLAIM = 'auth_time';
|
|
7
|
+
|
|
8
|
+
export interface SessionClaims {
|
|
9
|
+
[AUTH_TIME_CLAIM]?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Current time in seconds, as JWT iat/exp. */
|
|
13
|
+
export const nowInSeconds = (now: number = Date.now()): number => Math.floor(now / 1000);
|
|
14
|
+
|
|
15
|
+
/** Claims for a new pair: login starts a session; refresh keeps the original auth_time. */
|
|
16
|
+
export function sessionClaims(authTime: number): Required<SessionClaims> {
|
|
17
|
+
return { [AUTH_TIME_CLAIM]: authTime };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True if the refresh must be rejected with 401 SESSION_EXPIRED. Tokens without auth_time count as expired. */
|
|
21
|
+
export function isSessionExpired(
|
|
22
|
+
claims: SessionClaims,
|
|
23
|
+
maxAgeSeconds: number,
|
|
24
|
+
now: number = nowInSeconds(),
|
|
25
|
+
): boolean {
|
|
26
|
+
const authTime = claims[AUTH_TIME_CLAIM];
|
|
27
|
+
return typeof authTime !== 'number' || now - authTime >= maxAgeSeconds;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bi-analytics/arness-back",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Harness de agentes IA (Claude Code, Codex, Gemini CLI, Cursor) para backends NestJS de analítica",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/bi-analytics-linktic/harness-back.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"homepage": "https://github.com/bi-analytics-linktic/harness-back#readme",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org/"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"arness": "bin/arness.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"harness/",
|
|
21
|
+
"!harness/progress.md",
|
|
22
|
+
"!harness/.arness.json",
|
|
23
|
+
"!harness/arness.config.json",
|
|
24
|
+
"!harness/.arness-backup/",
|
|
25
|
+
"!harness/decisiones/[0-9]*.md",
|
|
26
|
+
"CHANGELOG.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"harness",
|
|
33
|
+
"agents",
|
|
34
|
+
"claude-code",
|
|
35
|
+
"codex",
|
|
36
|
+
"nestjs"
|
|
37
|
+
],
|
|
38
|
+
"license": "UNLICENSED",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"eslint": "^9.39.5"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test",
|
|
44
|
+
"verify": "./harness/init.sh",
|
|
45
|
+
"pack:check": "pnpm pack --dry-run"
|
|
46
|
+
}
|
|
6
47
|
}
|