@soulbatical/tetra-dev-toolkit 1.20.11 → 1.20.13

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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Security E2E tests — input validation, injection prevention, error handling.
3
+ * Tetra golden standard — tests beyond auth walls.
4
+ *
5
+ * CUSTOMIZE: Update endpoints and payloads for your project.
6
+ */
7
+ import { describe, it, expect, beforeAll } from 'vitest';
8
+ import { get, post, del, api } from './helpers/api-client';
9
+ import { getTestContext, type TestContext } from './helpers/test-users';
10
+
11
+ let ctx: TestContext;
12
+
13
+ beforeAll(async () => {
14
+ ctx = await getTestContext();
15
+ }, 60000);
16
+
17
+ /* ---------- Input validation ---------- */
18
+
19
+ describe('Security: Input validation', () => {
20
+ // CUSTOMIZE: Use your project's main create endpoint
21
+ const createEndpoint = '/api/resources'; // TODO: change this
22
+
23
+ it('rejects SQL injection in query params', async () => {
24
+ const res = await get<any>(
25
+ `${createEndpoint}?search='; DROP TABLE users; --`,
26
+ ctx.admin.token
27
+ );
28
+ // Should not crash — return 200 with empty results or 400
29
+ expect([200, 400]).toContain(res.status);
30
+ });
31
+
32
+ it('rejects XSS in create body', async () => {
33
+ const res = await post<any>(createEndpoint, {
34
+ name: '<script>alert("xss")</script>',
35
+ }, ctx.admin.token);
36
+ // Should create (and sanitize on render) or reject — never 500
37
+ expect(res.status).toBeLessThan(500);
38
+ });
39
+
40
+ it('handles oversized field values', async () => {
41
+ const res = await post<any>(createEndpoint, {
42
+ name: 'x'.repeat(10000),
43
+ }, ctx.admin.token);
44
+ // Should reject with 400/422, never 500
45
+ expect(res.status).toBeLessThan(500);
46
+ });
47
+ });
48
+
49
+ /* ---------- Error handling ---------- */
50
+
51
+ describe('Security: Error handling', () => {
52
+ it('401 does not leak stack traces', async () => {
53
+ const res = await get<any>('/api/admin/users');
54
+ expect(res.status).toBe(401);
55
+ const body = JSON.stringify(res.data);
56
+ expect(body).not.toContain('node_modules');
57
+ expect(body).not.toContain('.ts:');
58
+ expect(body).not.toContain('at Object.');
59
+ });
60
+
61
+ it('404 for unknown route does not leak internals', async () => {
62
+ const res = await get<any>('/api/nonexistent-e2e-route', ctx.admin.token);
63
+ expect(res.status).toBeGreaterThanOrEqual(400);
64
+ const body = JSON.stringify(res.data);
65
+ expect(body).not.toContain('node_modules');
66
+ });
67
+ });
68
+
69
+ /* ---------- CORS ---------- */
70
+
71
+ describe('Security: CORS', () => {
72
+ it('OPTIONS request does not return 500', async () => {
73
+ const res = await api('/api/health', {
74
+ method: 'OPTIONS',
75
+ headers: {
76
+ 'Origin': 'https://example.com',
77
+ 'Access-Control-Request-Method': 'GET',
78
+ },
79
+ });
80
+ expect(res.status).toBeLessThan(500);
81
+ });
82
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Global setup — logs in ONCE and writes tokens to a temp file.
3
+ * Test workers read from this file instead of logging in separately.
4
+ *
5
+ * PREREQUISITES:
6
+ * 1. Backend running (npm run dev or npm run dev:local)
7
+ * 2. Test user exists in database (seed or manual create)
8
+ * 3. Set E2E_BASE_URL, E2E_ADMIN_EMAIL, E2E_PASSWORD (via env or Doppler)
9
+ */
10
+
11
+ import { writeFileSync, unlinkSync } from 'fs';
12
+ import { join } from 'path';
13
+
14
+ const BASE_URL = process.env.E2E_BASE_URL || 'http://localhost:3003';
15
+ const EMAIL = process.env.E2E_ADMIN_EMAIL || 'e2e-admin@example.com';
16
+ const PASSWORD = process.env.E2E_PASSWORD || 'TestPass1234';
17
+ const TEST_KEY = process.env.RATE_LIMIT_BYPASS_KEY || '';
18
+
19
+ export const TOKEN_FILE = join(__dirname, '.e2e-tokens.json');
20
+
21
+ async function apiPost(path: string, body: Record<string, unknown>) {
22
+ const headers: Record<string, string> = {
23
+ 'Content-Type': 'application/json',
24
+ };
25
+ if (TEST_KEY) {
26
+ headers['X-Test-Key'] = TEST_KEY;
27
+ }
28
+
29
+ const res = await fetch(`${BASE_URL}${path}`, {
30
+ method: 'POST',
31
+ headers,
32
+ body: JSON.stringify(body),
33
+ });
34
+
35
+ const data = await res.json() as any;
36
+ return { status: res.status, data };
37
+ }
38
+
39
+ export async function setup() {
40
+ const loginRes = await apiPost('/api/public/auth/login', {
41
+ email: EMAIL,
42
+ password: PASSWORD,
43
+ });
44
+
45
+ const accessToken = loginRes.data?.data?.accessToken
46
+ || loginRes.data?.data?.access_token;
47
+ const refreshToken = loginRes.data?.data?.refreshToken
48
+ || loginRes.data?.data?.refresh_token;
49
+
50
+ if (!accessToken) {
51
+ console.error('\n ❌ Admin login failed:', loginRes.status, JSON.stringify(loginRes.data));
52
+ console.error(' Prerequisites:');
53
+ console.error(` 1. Backend running on ${BASE_URL}`);
54
+ console.error(` 2. Test user ${EMAIL} exists in database`);
55
+ console.error(' 3. Set E2E_ADMIN_EMAIL + E2E_PASSWORD env vars\n');
56
+ throw new Error(`Admin login failed (${loginRes.status}). Ensure ${EMAIL} exists.`);
57
+ }
58
+
59
+ const tokens = {
60
+ id: loginRes.data.data.user?.id,
61
+ email: EMAIL,
62
+ token: accessToken,
63
+ refreshToken: refreshToken,
64
+ organizationId: loginRes.data.data.user?.active_organization_id,
65
+ };
66
+
67
+ writeFileSync(TOKEN_FILE, JSON.stringify(tokens));
68
+ console.log(`\n E2E setup: logged in as ${EMAIL} (id: ${tokens.id})\n`);
69
+ }
70
+
71
+ export async function teardown() {
72
+ try { unlinkSync(TOKEN_FILE); } catch {}
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.20.11",
3
+ "version": "1.20.13",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },
@@ -39,7 +39,8 @@
39
39
  "tetra-check-peers": "./bin/tetra-check-peers.js",
40
40
  "tetra-security-gate": "./bin/tetra-security-gate.js",
41
41
  "tetra-smoke": "./bin/tetra-smoke.js",
42
- "tetra-init-tests": "./bin/tetra-init-tests.js"
42
+ "tetra-init-tests": "./bin/tetra-init-tests.js",
43
+ "tetra-test-audit": "./bin/tetra-test-audit.js"
43
44
  },
44
45
  "files": [
45
46
  "bin/",
@@ -1,93 +0,0 @@
1
- /**
2
- * Security E2E tests — auth walls + role-based access.
3
- * Tetra golden standard — auto-reads protected routes from project config.
4
- *
5
- * CUSTOMIZE: Update protectedRoutes and role access tests for your project.
6
- */
7
- import { describe, it, expect, beforeAll } from 'vitest';
8
- import { get, post, del, api } from './helpers/api-client';
9
- import { getTestContext, type TestContext } from './helpers/test-users';
10
-
11
- let ctx: TestContext;
12
-
13
- beforeAll(async () => {
14
- ctx = await getTestContext();
15
- }, 60000);
16
-
17
- // ── Auth walls: all protected routes require token ────────────
18
- // CUSTOMIZE: add all your protected API routes here
19
-
20
- const protectedRoutes = [
21
- '/api/tracks',
22
- '/api/users',
23
- '/api/admin/dashboard',
24
- '/api/permissions/my',
25
- // Add project-specific routes below:
26
- ];
27
-
28
- describe('Security: Auth walls', () => {
29
- for (const route of protectedRoutes) {
30
- it(`${route} returns 401 without auth`, async () => {
31
- const res = await get(route);
32
- expect(res.status).toBe(401);
33
- });
34
- }
35
- });
36
-
37
- describe('Security: Invalid tokens', () => {
38
- it('rejects malformed JWT', async () => {
39
- const res = await get('/api/tracks', 'eyJhbGciOiJIUzI1NiJ9.fake.fake');
40
- expect(res.status).toBe(401);
41
- });
42
-
43
- it('rejects empty Authorization header', async () => {
44
- const res = await api('/api/tracks', { headers: { 'Authorization': '' } });
45
- expect(res.status).toBe(401);
46
- });
47
- });
48
-
49
- // ── Role-based access ─────────────────────────────────────────
50
- // CUSTOMIZE: update for your project's roles and admin endpoints
51
-
52
- describe('Security: Basic user cannot access admin endpoints', () => {
53
- it('basic CANNOT access /api/admin/dashboard', async () => {
54
- const res = await get('/api/admin/dashboard', ctx.basic.token);
55
- expect(res.status).toBe(403);
56
- });
57
-
58
- it('basic CANNOT list users', async () => {
59
- const res = await get('/api/users', ctx.basic.token);
60
- expect(res.status).toBe(403);
61
- });
62
-
63
- it('basic CANNOT delete another user', async () => {
64
- const res = await del(`/api/users/${ctx.admin.id}`, ctx.basic.token);
65
- expect(res.status).toBe(403);
66
- });
67
- });
68
-
69
- describe('Security: Elevated user cannot access admin-only endpoints', () => {
70
- it('elevated CANNOT access /api/admin/dashboard', async () => {
71
- const res = await get('/api/admin/dashboard', ctx.elevated.token);
72
- expect(res.status).toBe(403);
73
- });
74
-
75
- it('elevated CANNOT delete another user', async () => {
76
- const res = await del(`/api/users/${ctx.admin.id}`, ctx.elevated.token);
77
- expect(res.status).toBe(403);
78
- });
79
- });
80
-
81
- // ── Public routes: accessible without auth ────────────────────
82
-
83
- describe('Security: Public routes', () => {
84
- it('/api/health is public', async () => {
85
- const res = await get('/api/health');
86
- expect(res.status).toBe(200);
87
- });
88
-
89
- it('/api/version is public', async () => {
90
- const res = await get('/api/version');
91
- expect(res.status).toBe(200);
92
- });
93
- });