@anteros/core 0.0.40 → 0.0.41

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.
Files changed (59) hide show
  1. package/database/audit.ts +193 -0
  2. package/database/collection.ts +3 -3
  3. package/database/deleteLog.ts +34 -0
  4. package/database/file.ts +3 -3
  5. package/database/mongodbadapter.ts +321 -147
  6. package/database/replication.ts +780 -0
  7. package/database/schema.ts +140 -125
  8. package/database/tenant.ts +4 -0
  9. package/database/vars.ts +405 -0
  10. package/database/workflow.ts +6 -6
  11. package/index.ts +10 -0
  12. package/lib/audit.ts +271 -0
  13. package/lib/auditResult.ts +120 -0
  14. package/lib/define.ts +20 -1
  15. package/lib/files.ts +64 -11
  16. package/lib/lifecycle.ts +141 -0
  17. package/lib/redact.ts +117 -0
  18. package/lib/workflow.ts +1 -1
  19. package/package.json +1 -1
  20. package/server/api.ts +109 -25
  21. package/server/boot.ts +185 -53
  22. package/server/flags.ts +136 -0
  23. package/server/hono.ts +1 -1
  24. package/tests/audit-file.test.ts +296 -0
  25. package/tests/audit-operations.test.ts +184 -0
  26. package/tests/audit-redact.test.ts +233 -0
  27. package/tests/audit-results.test.ts +188 -0
  28. package/tests/audit-retention-config.test.ts +76 -0
  29. package/tests/audit-run-action.test.ts +120 -0
  30. package/tests/audit.test.ts +320 -0
  31. package/tests/db-cache.test.ts +7 -7
  32. package/tests/file-collection-replication.test.ts +98 -0
  33. package/tests/fixtures/lc-disabled/lifecycle.ts +11 -0
  34. package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
  35. package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
  36. package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
  37. package/tests/fixtures/repl-only/app.ts +26 -0
  38. package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
  39. package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
  40. package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -0
  41. package/tests/fixtures/vars-tenant/vars/config.var.ts +23 -0
  42. package/tests/fixtures/vars-tenant/vars/private.var.ts +6 -0
  43. package/tests/lifecycle.test.ts +96 -0
  44. package/tests/replication-control.test.ts +128 -0
  45. package/tests/replication-initial-sync.test.ts +86 -0
  46. package/tests/replication-only.test.ts +358 -0
  47. package/tests/replication.test.ts +188 -0
  48. package/tests/upload.test.ts +102 -1
  49. package/tests/vars-api.test.ts +123 -0
  50. package/tests/vars-replication.test.ts +95 -0
  51. package/tests/vars.test.ts +216 -0
  52. package/types/collection.d.ts +3 -0
  53. package/types/config.d.ts +64 -0
  54. package/types/file.d.ts +4 -1
  55. package/types/lifecycle.d.ts +71 -0
  56. package/types/replication.d.ts +199 -0
  57. package/types/tenant.d.ts +23 -0
  58. package/types/vars.d.ts +147 -0
  59. package/utils/func.ts +67 -52
@@ -0,0 +1,141 @@
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+ import { cfg } from "../server/config";
4
+ import { useRest } from "../database/rest";
5
+ import { logger } from "../utils/logger";
6
+ import {
7
+ replicationNow,
8
+ getReplicationState,
9
+ resetReplication,
10
+ seedReplication,
11
+ } from "../database/replication";
12
+ import type { Lifecycle, LifecycleReason } from "../types/lifecycle";
13
+ import type { Tenant } from "../types/tenant";
14
+ import type { TenantReplicationApi } from "../types/replication";
15
+
16
+ /**
17
+ * Tenant-scoped replication API — like `rest`, the tenant is implicit. Built per
18
+ * tenant for the lifecycle context, so hooks never pass a tenant id.
19
+ */
20
+ function tenantReplication(tenantId: string): TenantReplicationApi {
21
+ return {
22
+ now: () => replicationNow(tenantId),
23
+ state: (destinationId?: string, collection?: string) => getReplicationState(tenantId, destinationId, collection),
24
+ reset: (opts) => resetReplication(tenantId, opts),
25
+ seed: (value, opts) => seedReplication(tenantId, value, opts),
26
+ };
27
+ }
28
+
29
+ /** Lifecycles by tenant id — one `{tenant.dir}/lifecycle.ts` per tenant. */
30
+ const lifecycles = new Map<string, Lifecycle>();
31
+ /** Ensures `onDestroy` runs at most once per process. */
32
+ let destroyed = false;
33
+
34
+ /**
35
+ * Load `{tenant.dir}/lifecycle.ts` for every tenant. A missing file is fine —
36
+ * a tenant simply has no lifecycle.
37
+ */
38
+ async function loadLifecycles(): Promise<void> {
39
+ lifecycles.clear();
40
+ destroyed = false;
41
+ for (const tenant of cfg.tenants ?? []) {
42
+ try {
43
+ const file = path.join(process.cwd(), tenant.dir, "lifecycle.ts");
44
+ if (!(await fs.exists(file))) continue;
45
+ const module = await import(file);
46
+ const lifecycle = module?.default;
47
+ if (lifecycle?._isLifecycle_) {
48
+ lifecycles.set(tenant.id, { ...lifecycle, _tenant_: tenant.id });
49
+ }
50
+ } catch (err: any) {
51
+ logger.file("error", "lifecycle: failed to load", { tenant: tenant.id, error: err?.message });
52
+ }
53
+ }
54
+ }
55
+
56
+ /** Tenant-scoped context passed to every hook. */
57
+ function buildContext(tenant: Tenant, extra?: Record<string, any>) {
58
+ return {
59
+ tenant,
60
+ rest: new useRest({ tenant_id: tenant.id }),
61
+ logger,
62
+ cfg,
63
+ replication: tenantReplication(tenant.id),
64
+ ...(extra ?? {}),
65
+ } as any;
66
+ }
67
+
68
+ /**
69
+ * Run every `beforeBoot` hook, sequentially in `cfg.tenants` order. Blocking:
70
+ * a throwing hook aborts the boot (fail-fast).
71
+ */
72
+ async function runBeforeBoot(): Promise<void> {
73
+ for (const tenant of cfg.tenants ?? []) {
74
+ const lifecycle = lifecycles.get(tenant.id);
75
+ if (!lifecycle?.enabled || !lifecycle.beforeBoot) continue;
76
+ try {
77
+ await lifecycle.beforeBoot(buildContext(tenant));
78
+ logger.file("lifecycle: beforeBoot", { tenant: tenant.id });
79
+ } catch (err: any) {
80
+ logger.error(`lifecycle beforeBoot failed for tenant '${tenant.id}'`, { error: err?.message, stack: err?.stack });
81
+ throw new Error(`[${tenant.id}] lifecycle beforeBoot failed: ${err?.message ?? err}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Run every `afterBoot` hook once the server is listening. Errors are logged
88
+ * but never fatal — the server is already serving.
89
+ */
90
+ async function runAfterBoot(server: any, io: any): Promise<void> {
91
+ for (const tenant of cfg.tenants ?? []) {
92
+ const lifecycle = lifecycles.get(tenant.id);
93
+ if (!lifecycle?.enabled || !lifecycle.afterBoot) continue;
94
+ try {
95
+ await lifecycle.afterBoot(buildContext(tenant, { io, server }));
96
+ logger.file("lifecycle: afterBoot", { tenant: tenant.id });
97
+ } catch (err: any) {
98
+ logger.error(`lifecycle afterBoot failed for tenant '${tenant.id}'`, { error: err?.message });
99
+ }
100
+ }
101
+ }
102
+
103
+ /** Bound a promise with a timeout that rejects. */
104
+ function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
105
+ let timer: ReturnType<typeof setTimeout>;
106
+ const timeout = new Promise<never>((_, reject) => {
107
+ timer = setTimeout(() => reject(new Error(message)), ms);
108
+ });
109
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)) as Promise<T>;
110
+ }
111
+
112
+ /**
113
+ * Run every `onDestroy` hook on shutdown (once per process), before replication
114
+ * stops and DB clients close. Each hook is bounded by `destroyTimeout`.
115
+ */
116
+ async function runOnDestroy(reason: LifecycleReason): Promise<void> {
117
+ if (destroyed) return;
118
+ destroyed = true;
119
+
120
+ for (const tenant of cfg.tenants ?? []) {
121
+ const lifecycle = lifecycles.get(tenant.id);
122
+ if (!lifecycle?.enabled || !lifecycle.onDestroy) continue;
123
+ try {
124
+ await withTimeout(
125
+ Promise.resolve(lifecycle.onDestroy(buildContext(tenant, { reason }))),
126
+ lifecycle.destroyTimeout ?? 10_000,
127
+ `lifecycle onDestroy timed out after ${lifecycle.destroyTimeout ?? 10_000}ms`,
128
+ );
129
+ logger.file("lifecycle: onDestroy", { tenant: tenant.id, reason });
130
+ } catch (err: any) {
131
+ logger.error(`lifecycle onDestroy failed for tenant '${tenant.id}'`, { error: err?.message });
132
+ }
133
+ }
134
+ }
135
+
136
+ export {
137
+ loadLifecycles,
138
+ runBeforeBoot,
139
+ runAfterBoot,
140
+ runOnDestroy,
141
+ };
package/lib/redact.ts ADDED
@@ -0,0 +1,117 @@
1
+ import { cfg } from "../server/config";
2
+
3
+ /**
4
+ * Keys whose value is replaced by `[redacted]` before an audit entry is stored
5
+ * (in `_audit_` **and** in the local file backup), at any depth.
6
+ *
7
+ * Override with `server.audit.redact: ['…']`, disable with `server.audit.redact: false`.
8
+ */
9
+ const DEFAULT_REDACT_KEYS = [
10
+ 'password',
11
+ 'passwd',
12
+ 'pwd',
13
+ 'token',
14
+ 'auth_token',
15
+ 'access_token',
16
+ 'refresh_token',
17
+ 'id_token',
18
+ 'authorization',
19
+ 'apikey',
20
+ 'api_key',
21
+ 'key',
22
+ 'secret',
23
+ 'cookie',
24
+ 'session_id',
25
+ 'otp',
26
+ 'pin',
27
+ ]
28
+
29
+ const REDACTED = '[redacted]'
30
+
31
+ /** Depth guard — a pathological payload must never cost more than a bounded walk */
32
+ const MAX_DEPTH = 8
33
+
34
+ // Cache initialised for the default state (`redact` unset) — a sentinel like
35
+ // `undefined` would be indistinguishable from the real, unset configuration.
36
+ let cachedConfig: unknown = undefined
37
+ let cachedKeys: Set<string> | null = new Set(DEFAULT_REDACT_KEYS.map((key) => key.toLowerCase()))
38
+
39
+ /** Lower-cased redaction keys, or `null` when redaction is disabled */
40
+ function redactKeys(): Set<string> | null {
41
+ const configured = cfg.server?.audit?.redact
42
+ if (configured !== cachedConfig) {
43
+ cachedConfig = configured
44
+ cachedKeys = configured === false
45
+ ? null
46
+ : new Set((configured ?? DEFAULT_REDACT_KEYS).map((key) => key.toLowerCase()))
47
+ }
48
+ return cachedKeys
49
+ }
50
+
51
+ /**
52
+ * Is this key sensitive? Exact match, case-insensitive, `-`/`.` normalised to `_`
53
+ * (`api-key` → `api_key`), plus the usual header prefix (`x-api-key` → `api_key`).
54
+ *
55
+ * Deliberately **not** a substring match: `key` must not match `monkey`.
56
+ */
57
+ function isSensitiveKey(name: string, keys: Set<string>): boolean {
58
+ const normalized = name.toLowerCase().replace(/[-.]/g, '_')
59
+ if (keys.has(normalized)) return true
60
+ if (normalized.startsWith('x_')) return keys.has(normalized.slice(2))
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * Replace the value of every sensitive key (see `isSensitiveKey`), at any depth.
66
+ * Arrays are traversed; **non-plain objects** (`Date`, `ObjectId`, `Buffer`,
67
+ * `Map`…) are left untouched so stored types are never altered.
68
+ *
69
+ * Returns the **same reference** when nothing matched — the common case (no
70
+ * sensitive key in the payload) costs the walk but no allocation.
71
+ */
72
+ function redact(value: any, keys: Set<string>, depth = 0): any {
73
+ if (value === null || typeof value !== 'object' || depth > MAX_DEPTH) return value
74
+
75
+ if (Array.isArray(value)) {
76
+ let changed = false
77
+ const out = value.map((item) => {
78
+ const next = redact(item, keys, depth + 1)
79
+ if (next !== item) changed = true
80
+ return next
81
+ })
82
+ return changed ? out : value
83
+ }
84
+
85
+ const proto = Object.getPrototypeOf(value)
86
+ if (proto !== Object.prototype && proto !== null) return value
87
+
88
+ let changed = false
89
+ const out: Record<string, any> = {}
90
+ for (const [name, item] of Object.entries(value)) {
91
+ if (isSensitiveKey(name, keys)) {
92
+ out[name] = REDACTED
93
+ changed = true
94
+ continue
95
+ }
96
+ const next = redact(item, keys, depth + 1)
97
+ if (next !== item) changed = true
98
+ out[name] = next
99
+ }
100
+ return changed ? out : value
101
+ }
102
+
103
+ /** Redact an audit payload — `input`, `result`, `meta` or `request` */
104
+ function redactPayload(value: any): any {
105
+ const keys = redactKeys()
106
+ if (!keys || value === undefined || value === null) return value
107
+ return redact(value, keys)
108
+ }
109
+
110
+ export {
111
+ DEFAULT_REDACT_KEYS,
112
+ REDACTED,
113
+ redactKeys,
114
+ isSensitiveKey,
115
+ redactPayload,
116
+ redact,
117
+ }
package/lib/workflow.ts CHANGED
@@ -28,7 +28,7 @@ async function syncWorkflows() {
28
28
  }
29
29
  }
30
30
 
31
- // Créer les indexes sur _workflows_ pour les contextes définis
31
+ // Create the indexes on _workflows_ for the defined contexts
32
32
  for (const [key, wf] of workflows) {
33
33
  const tenantId = key.split(':')[0]
34
34
  if (!wf.context || !tenantId) continue
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anteros/core",
3
3
  "module": "index.ts",
4
- "version": "0.0.40",
4
+ "version": "0.0.41",
5
5
  "type": "module",
6
6
  "description": "Anteros Core",
7
7
  "private": false,
package/server/api.ts CHANGED
@@ -16,8 +16,11 @@ import type { ActivityInput } from "../types/activity";
16
16
  import * as os from 'node:os';
17
17
  import { basename } from 'node:path';
18
18
  import { handleUpload, handleServe, handleDelete, type FileResult } from "../lib/files";
19
+ import { getVarsDefinition } from "../database/vars";
20
+ import { getTenant } from "../database/tenant";
19
21
  const API_PREFIX = '/api/:tenant_id/:collection/:action';
20
22
  const SERVICE_PREFIX = '/services/:tenant_id/:service/:action';
23
+ const VARS_PREFIX = '/vars/:tenant_id/:action';
21
24
  const UPLOAD_PREFIX = '/upload/:tenant_id/:collection';
22
25
  const FILES_PREFIX = '/files/:tenant_id/:collection/:file';
23
26
 
@@ -346,15 +349,14 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
346
349
  }
347
350
 
348
351
  // ── Access control ──
349
- // Faille 7: single implementation via evaluateAccess (checks expired/forged tokens)
352
+ // Flaw 7: single implementation via evaluateAccess (checks expired/forged tokens)
350
353
  await evaluateAccess(col?.api?.access as any, action, rest, 'Action', c);
351
354
 
352
355
  // ── Execute action ──
353
356
  if (col.actions?.[action]) {
354
- response = await col.actions[action]({
355
- rest, data: body?.data, error: fn.error,
356
- io, jwt: func.jwt, token: accessToken,
357
- });
357
+ // Delegated to `rest.runAction` so the invocation is audited once,
358
+ // with the same context (`rest`, `io`, `data`, `token`, …)
359
+ response = await rest.runAction(collection, action, body?.data);
358
360
  } else {
359
361
  const handler = crudHandlers[action];
360
362
  if (!handler) throw new AppError('Action `' + action + '` not found', { status: 400, code: 'ACTION_NOT_FOUND' });
@@ -382,6 +384,97 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
382
384
  }
383
385
  });
384
386
 
387
+ // ─── Vars API ───────────────────────────────────────────────────────
388
+ app.post(VARS_PREFIX, async (c) => {
389
+ let rest: InstanceType<typeof useRest> | undefined;
390
+ let tenant_id = '';
391
+ let action = '';
392
+ let ns = '';
393
+ let body: any;
394
+ const logStart = Date.now();
395
+ try {
396
+ const ContentType = c.req.header('Content-Type');
397
+ if (ContentType?.includes('application/json')) {
398
+ try { body = await c.req.json(); } catch {
399
+ throw new AppError('Invalid JSON body', { status: 400, code: 'INVALID_JSON_BODY' });
400
+ }
401
+ } else {
402
+ body = {};
403
+ }
404
+
405
+ const params = c.req.param() as { tenant_id: string; action: string };
406
+ tenant_id = params.tenant_id;
407
+ action = params.action;
408
+ ns = body?.ns;
409
+
410
+ if (!tenant_id) throw new AppError('Tenant ID is required', { status: 400, code: 'TENANT_ID_REQUIRED' });
411
+ if (!action) throw new AppError('Action is required', { status: 400, code: 'ACTION_REQUIRED' });
412
+ if (!ns) throw new AppError('Variables namespace is required', { status: 400, code: 'NAMESPACE_REQUIRED' });
413
+ if (!getTenant(tenant_id)) throw new AppError('Tenant not found', { status: 400, code: 'TENANT_NOT_FOUND' });
414
+
415
+ requestCtxStorage.set('tenant_id', tenant_id);
416
+
417
+ // Only namespaces declared with `define.Vars` are reachable, and their
418
+ // `api.access` gates every action (no rules = denied).
419
+ const definition = getVarsDefinition(tenant_id, ns);
420
+ if (!definition) throw new AppError(`Variables namespace '${ns}' not found`, { status: 400, code: 'NAMESPACE_NOT_FOUND' });
421
+
422
+ rest = new useRest({ internal: false, tenant_id });
423
+ await evaluateAccess(definition.api?.access as any, action, rest, `vars:${ns}`, c);
424
+
425
+ const vars = rest.vars;
426
+ const { key, value, meta, ttl, scope, where, entries, by } = body ?? {};
427
+ let response: any;
428
+
429
+ switch (action) {
430
+ case 'set': await vars.set(ns, key, value, { meta, ttl, scope }); response = { ok: true }; break;
431
+ case 'setMany': await vars.setMany(ns, entries ?? {}, { meta, ttl, scope }); response = { ok: true }; break;
432
+ case 'get': response = { value: await vars.get(ns, key, { scope }) }; break;
433
+ case 'entry': response = { entry: await vars.entry(ns, key, { scope }) }; break;
434
+ case 'entries': response = { entries: await vars.entries(ns, { scope, where }) }; break;
435
+ case 'all': response = { vars: await vars.all(ns, { scope, where }) }; break;
436
+ case 'del': response = { ok: await vars.del(ns, key, { scope }) }; break;
437
+ case 'has': response = { exists: await vars.has(ns, key, { scope }) }; break;
438
+ case 'incr': response = { value: await vars.incr(ns, key, by, { scope, ttl }) }; break;
439
+ case 'expire': response = { ok: await vars.expire(ns, key, ttl, { scope }) }; break;
440
+ case 'clear': response = { deleted: await vars.clear(ns, { scope }) }; break;
441
+ default: throw new AppError(`Action '${action}' not found`, { status: 400, code: 'ACTION_NOT_FOUND' });
442
+ }
443
+
444
+ // Audit — never store `value` / `meta` (they may hold secrets).
445
+ const accessToken = getAccessToken(c);
446
+ await logActivity({
447
+ rest,
448
+ tenant_id,
449
+ action,
450
+ collection: `_vars_:${ns}`,
451
+ status: 'success',
452
+ input: { ns, key, scope, ttl },
453
+ result: ['get', 'all', 'entry', 'entries'].includes(action) ? response : undefined,
454
+ duration: Date.now() - logStart,
455
+ token: { decoded: accessToken.decoded, value: null, provided: true, expired: false },
456
+ });
457
+ return c.json(response);
458
+ } catch (err: any) {
459
+ if (cfg?.debug) console.error(err);
460
+ if (rest && tenant_id && ns) {
461
+ const accessToken = getAccessToken(c);
462
+ await logActivity({
463
+ rest,
464
+ tenant_id,
465
+ action,
466
+ collection: `_vars_:${ns}`,
467
+ status: 'error',
468
+ input: { ns },
469
+ error: { message: err?.message, code: err?.code || 'INTERNAL_VARS_ERROR' },
470
+ duration: Date.now() - logStart,
471
+ token: { decoded: accessToken.decoded, value: null, provided: true, expired: false },
472
+ }).catch(() => {});
473
+ }
474
+ return errorResponse(c, err);
475
+ }
476
+ });
477
+
385
478
  // ─── Service API ────────────────────────────────────────────────────
386
479
  app.post(SERVICE_PREFIX, async (c) => {
387
480
  try {
@@ -415,21 +508,10 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
415
508
  const serviceAccess = serviceInstance.api?.access as Record<string, boolean | Function> | undefined;
416
509
  await evaluateAccess(serviceAccess as any, action, rest, action, c);
417
510
 
418
- // Re-read accessToken for the action handler
419
- const accessToken = getAccessToken(c);
420
-
421
- const logStart = Date.now();
422
- try {
423
- const response = await serviceInstance.actions[action]({
424
- data: body?.data, error: fn.error, io, jwt: func.jwt, token: accessToken, rest,
425
- });
426
-
427
- await logActivity({ rest, tenant_id, action, collection: service, status: 'success', input: body?.data, result: response, duration: Date.now() - logStart, token: { decoded: accessToken.decoded, value: null, provided: true, expired: false } });
428
- return c.json(response);
429
- } catch (err: any) {
430
- await logActivity({ rest, tenant_id, action, collection: service, status: 'error', input: body?.data, error: { message: err?.message, code: err?.code || 'INTERNAL_SERVICE_ERROR' }, duration: Date.now() - logStart, token: { decoded: accessToken.decoded, value: null, provided: true, expired: false } }).catch(() => {});
431
- throw err;
432
- }
511
+ // No `logActivity` here: the call is delegated to `rest.runService()`, which
512
+ // audits it once (`action: 'runService'`, `internal: false`, params only)
513
+ const response = await rest.runService(service, action, body?.data);
514
+ return c.json(response);
433
515
  } catch (err: any) {
434
516
  if (cfg?.debug) console.error(err)
435
517
  return errorResponse(c, err);
@@ -469,12 +551,14 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
469
551
  });
470
552
  }
471
553
 
472
- const formData = await c.req.parseBody({ all: true }); // all: true → every value is an array (several files can share the same field name)
554
+ const formData = await c.req.parseBody({ all: true }); // repeated fields → arrays; a single occurrence stays a single value
473
555
 
474
556
  // Collect files from both accepted field names (single or multiple)
475
557
  const files: File[] = [];
476
558
  for (const key of ['file', 'upload']) {
477
- for (const value of formData[key] ?? []) {
559
+ const raw = formData[key];
560
+ const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
561
+ for (const value of values) {
478
562
  if (value instanceof File) files.push(value);
479
563
  }
480
564
  }
@@ -521,7 +605,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
521
605
  }
522
606
  await checkFileAccess(colServe?.api?.access as any, 'read', tenant_id, c);
523
607
 
524
- // Faille 18: validate the RAW path before basename (checks were dead code)
608
+ // Flaw 18: validate the RAW path before basename (checks were dead code)
525
609
  const rawFile = c.req.param('file') as string;
526
610
  if (!rawFile || rawFile.startsWith('.') || rawFile.includes('..') || rawFile.includes('/') || rawFile.includes('\\')) {
527
611
  throw new AppError('Invalid filename', { status: 400, code: 'INVALID_FILENAME' });
@@ -547,7 +631,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
547
631
  c.header('Content-Type', result.mimetype);
548
632
  if (result.size) c.header('Content-Length', String(result.size));
549
633
  c.header('Cache-Control', 'public, max-age=31536000, immutable');
550
- // Faille 4: SVG served as attachment (no inline execution)
634
+ // Flaw 4: SVG served as attachment (no inline execution)
551
635
  if (result.attachment) {
552
636
  c.header('Content-Disposition', `attachment; filename="${filename}"`);
553
637
  }
@@ -590,7 +674,7 @@ function initializeApi(app: Hono<{ Variables: HonoVariables }>) {
590
674
  // ─── Public config ──────────────────────────────────────────────────
591
675
  app.get('/_dnax/config/:tenant_id', (c) => {
592
676
  const t = c.get('token');
593
- // Faille 6: require a VERIFIED, non-expired token (not just any provided value)
677
+ // Flaw 6: require a VERIFIED, non-expired token (not just any provided value)
594
678
  if (!t?.decoded || t?.expired || !t?.provided) {
595
679
  return c.json({ message: 'Authentication required', code: 'AUTH_REQUIRED' }, 401);
596
680
  }